Initial commit
This commit is contained in:
@@ -0,0 +1,430 @@
|
||||
//! 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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user