Files
rustytorch/crates/specialized/rtx-neuro-forward/src/sensors.rs
T
osobhandClaude Opus 4.6 02d382d5f6 style: apply rustfmt across all crates and demos
Consistent formatting pass: line wrapping, import sorting, trailing
whitespace removal, let-chain indentation, merged derive attributes,
and unsafe block reformatting.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
2026-04-12 07:01:58 -07:00

429 lines
12 KiB
Rust

//! Sensor definitions for MEG and EEG.
//!
//! This module defines sensor types and sensor arrays for forward modeling.
use crate::{Orientation, Position, normalize};
use nalgebra::Vector3;
use std::f64::consts::PI;
/// Type of sensor
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SensorType {
/// MEG magnetometer
MegMag,
/// MEG gradiometer
MegGrad,
/// EEG electrode
Eeg,
/// Reference sensor
Ref,
}
/// Type of MEG coil
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CoilType {
/// Single loop magnetometer
Magnetometer,
/// Axial gradiometer (two loops, same axis)
AxialGradiometer,
/// Planar gradiometer (two loops, same plane)
PlanarGradiometer,
}
/// MEG coil definition
#[derive(Debug, Clone)]
pub struct MegCoil {
/// Coil type
coil_type: CoilType,
/// Center position
position: Position,
/// Coil normal/orientation
orientation: Orientation,
/// Coil radius in meters
radius: f64,
/// Baseline for gradiometers (distance between coils)
baseline: Option<f64>,
/// Integration points for field computation
integration_points: Vec<(Position, f64)>,
}
impl MegCoil {
/// Create a magnetometer coil
pub fn magnetometer(position: [f64; 3], orientation: [f64; 3], radius: f64) -> Self {
let pos = Vector3::new(position[0], position[1], position[2]);
let ori = normalize(&Vector3::new(
orientation[0],
orientation[1],
orientation[2],
));
// Create integration points on the coil
let integration_points = Self::create_integration_points(&pos, &ori, radius, 8);
Self {
coil_type: CoilType::Magnetometer,
position: pos,
orientation: ori,
radius,
baseline: None,
integration_points,
}
}
/// Create an axial gradiometer
pub fn axial_gradiometer(
position: [f64; 3],
orientation: [f64; 3],
radius: f64,
baseline: f64,
) -> Self {
let pos = Vector3::new(position[0], position[1], position[2]);
let ori = normalize(&Vector3::new(
orientation[0],
orientation[1],
orientation[2],
));
// Integration points for both coils
let mut integration_points = Vec::new();
// Bottom coil (positive)
let bottom_pos = pos - ori * (baseline / 2.0);
for (p, w) in Self::create_integration_points(&bottom_pos, &ori, radius, 8) {
integration_points.push((p, w));
}
// Top coil (negative, reversed orientation)
let top_pos = pos + ori * (baseline / 2.0);
for (p, w) in Self::create_integration_points(&top_pos, &ori, radius, 8) {
integration_points.push((p, -w)); // Negative weight for gradiometer
}
Self {
coil_type: CoilType::AxialGradiometer,
position: pos,
orientation: ori,
radius,
baseline: Some(baseline),
integration_points,
}
}
/// Create integration points on a circular coil
fn create_integration_points(
center: &Position,
normal: &Orientation,
radius: f64,
n_points: usize,
) -> Vec<(Position, f64)> {
let mut points = Vec::with_capacity(n_points + 1);
// Find two orthogonal vectors in the coil plane
let arbitrary = if normal.z.abs() < 0.9 {
Vector3::new(0.0, 0.0, 1.0)
} else {
Vector3::new(1.0, 0.0, 0.0)
};
let u = normalize(&(normal.cross(&arbitrary)));
let v = normal.cross(&u);
// Center point
points.push((*center, 0.5));
// Ring of points
let weight = 0.5 / n_points as f64;
for i in 0..n_points {
let angle = 2.0 * PI * i as f64 / n_points as f64;
let p = center + u * (radius * angle.cos()) + v * (radius * angle.sin());
points.push((p, weight));
}
points
}
/// Get coil type
pub fn coil_type(&self) -> CoilType {
self.coil_type
}
/// Get coil position
pub fn position(&self) -> &Position {
&self.position
}
/// Get coil orientation
pub fn orientation(&self) -> &Orientation {
&self.orientation
}
/// Get integration points
pub fn integration_points(&self) -> &[(Position, f64)] {
&self.integration_points
}
}
/// A single sensor
#[derive(Debug, Clone)]
pub enum Sensor {
/// Magnetometer (point sensor)
Magnetometer {
/// Sensor name
name: String,
/// Sensor type
sensor_type: SensorType,
/// Position
position: Position,
/// Orientation (measurement direction)
orientation: Orientation,
},
/// Gradiometer or coil-based sensor
Gradiometer {
/// Sensor name
name: String,
/// Sensor type
sensor_type: SensorType,
/// Coil definition
coil: MegCoil,
},
}
impl Sensor {
/// Create an EEG electrode
pub fn eeg_electrode(name: &str, position: [f64; 3]) -> Self {
let pos = Vector3::new(position[0], position[1], position[2]);
// EEG electrodes have orientation pointing inward (toward center)
let ori = normalize(&(-pos));
Self::Magnetometer {
name: name.to_string(),
sensor_type: SensorType::Eeg,
position: pos,
orientation: ori,
}
}
/// Create a MEG magnetometer
pub fn meg_magnetometer(name: &str, position: [f64; 3], orientation: [f64; 3]) -> Self {
Self::Magnetometer {
name: name.to_string(),
sensor_type: SensorType::MegMag,
position: Vector3::new(position[0], position[1], position[2]),
orientation: normalize(&Vector3::new(
orientation[0],
orientation[1],
orientation[2],
)),
}
}
/// Create a MEG gradiometer
pub fn meg_gradiometer(
name: &str,
position: [f64; 3],
orientation: [f64; 3],
radius: f64,
baseline: f64,
) -> Self {
Self::Gradiometer {
name: name.to_string(),
sensor_type: SensorType::MegGrad,
coil: MegCoil::axial_gradiometer(position, orientation, radius, baseline),
}
}
/// Get sensor name
pub fn name(&self) -> &str {
match self {
Self::Magnetometer { name, .. } => name,
Self::Gradiometer { name, .. } => name,
}
}
/// Get sensor type
pub fn sensor_type(&self) -> SensorType {
match self {
Self::Magnetometer { sensor_type, .. } => *sensor_type,
Self::Gradiometer { sensor_type, .. } => *sensor_type,
}
}
/// Get sensor position
pub fn position(&self) -> &Position {
match self {
Self::Magnetometer { position, .. } => position,
Self::Gradiometer { coil, .. } => coil.position(),
}
}
}
/// Array of sensors
#[derive(Debug, Clone)]
pub struct SensorArray {
/// Sensors
sensors: Vec<Sensor>,
/// Sensor type (homogeneous arrays)
primary_type: SensorType,
}
impl SensorArray {
/// Create a new empty sensor array
pub fn new(primary_type: SensorType) -> Self {
Self {
sensors: Vec::new(),
primary_type,
}
}
/// Create from a list of sensors
pub fn from_sensors(sensors: Vec<Sensor>) -> Self {
let primary_type = sensors.first().map_or(SensorType::Eeg, Sensor::sensor_type);
Self {
sensors,
primary_type,
}
}
/// Add a sensor
pub fn add(&mut self, sensor: Sensor) {
self.sensors.push(sensor);
}
/// Get number of sensors
pub fn len(&self) -> usize {
self.sensors.len()
}
/// Check if empty
pub fn is_empty(&self) -> bool {
self.sensors.is_empty()
}
/// Iterate over sensors
pub fn iter(&self) -> impl Iterator<Item = &Sensor> {
self.sensors.iter()
}
/// Get a specific sensor
pub fn get(&self, index: usize) -> Option<&Sensor> {
self.sensors.get(index)
}
/// Create a standard 10-20 EEG electrode array
pub fn eeg_10_20(head_radius: f64) -> Self {
let mut sensors = Vec::new();
// Standard 10-20 electrode positions (spherical coordinates)
// (name, theta in degrees, phi in degrees)
let electrodes: [(&str, f64, f64); 19] = [
("Fp1", -18.0, 72.0),
("Fp2", 18.0, 72.0),
("F7", -54.0, 54.0),
("F3", -39.0, 54.0),
("Fz", 0.0, 54.0),
("F4", 39.0, 54.0),
("F8", 54.0, 54.0),
("T3", -90.0, 45.0),
("C3", -45.0, 45.0),
("Cz", 0.0, 0.0),
("C4", 45.0, 45.0),
("T4", 90.0, 45.0),
("T5", -126.0, 54.0),
("P3", -39.0, -54.0),
("Pz", 0.0, -54.0),
("P4", 39.0, -54.0),
("T6", 126.0, 54.0),
("O1", -18.0, -72.0),
("O2", 18.0, -72.0),
];
for (name, theta_deg, phi_deg) in electrodes {
let theta = theta_deg.to_radians();
let phi = (90.0_f64 - phi_deg).to_radians(); // Convert from polar to standard spherical
let x = head_radius * phi.sin() * theta.cos();
let y = head_radius * phi.sin() * theta.sin();
let z = head_radius * phi.cos();
sensors.push(Sensor::eeg_electrode(name, [x, y, z]));
}
Self {
sensors,
primary_type: SensorType::Eeg,
}
}
/// Create a synthetic MEG sensor array (helmet-like)
pub fn meg_helmet(n_sensors: usize, helmet_radius: f64) -> Self {
let mut sensors = Vec::new();
let golden_ratio = f64::midpoint(1.0, 5.0_f64.sqrt());
for i in 0..n_sensors {
let theta = 2.0 * PI * i as f64 / golden_ratio;
// Only cover upper hemisphere (head)
let phi = (1.0 - (i as f64 + 0.5) / n_sensors as f64).acos();
if phi < PI / 2.0 {
// Upper hemisphere only
let x = helmet_radius * phi.sin() * theta.cos();
let y = helmet_radius * phi.sin() * theta.sin();
let z = helmet_radius * phi.cos();
// Orientation points toward center
let ori = normalize(&Vector3::new(-x, -y, -z));
sensors.push(Sensor::meg_magnetometer(
&format!("MEG{:03}", i + 1),
[x, y, z],
[ori.x, ori.y, ori.z],
));
}
}
Self {
sensors,
primary_type: SensorType::MegMag,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_eeg_electrode() {
let sensor = Sensor::eeg_electrode("Cz", [0.0, 0.0, 0.092]);
assert_eq!(sensor.name(), "Cz");
assert_eq!(sensor.sensor_type(), SensorType::Eeg);
}
#[test]
fn test_meg_magnetometer() {
let sensor = Sensor::meg_magnetometer("MEG001", [0.0, 0.1, 0.08], [0.0, -1.0, 0.0]);
assert_eq!(sensor.name(), "MEG001");
assert_eq!(sensor.sensor_type(), SensorType::MegMag);
}
#[test]
fn test_eeg_10_20() {
let array = SensorArray::eeg_10_20(0.092);
assert_eq!(array.len(), 19);
}
#[test]
fn test_meg_helmet() {
let array = SensorArray::meg_helmet(100, 0.12);
assert!(array.len() > 0);
assert!(array.len() <= 100);
}
#[test]
fn test_coil_integration_points() {
let coil = MegCoil::magnetometer([0.0, 0.1, 0.08], [0.0, -1.0, 0.0], 0.01);
assert!(!coil.integration_points().is_empty());
}
}