//! 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, /// 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 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) -> 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 { 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()); } }