//! Source space definitions for source localization. //! //! A source space defines the possible locations and orientations //! of current dipoles in the brain. use crate::{Orientation, Position, norm, normalize}; use nalgebra::Vector3; use std::f64::consts::PI; /// Orientation constraint for sources #[derive(Debug, Clone, Copy, PartialEq)] pub enum SourceOrientation { /// Free orientation (3 DOF per source) Free, /// Fixed orientation perpendicular to cortical surface Fixed, /// Loose constraint (partially constrained) Loose(f64), } /// A single source point (dipole location) #[derive(Debug, Clone)] pub struct SourcePoint { /// Position in meters position: Position, /// Orientation (if fixed) orientation: Option, /// Surface normal (for cortical sources) normal: Option, /// Hemisphere (left=-1, right=1) hemisphere: i8, /// Vertex index (for surface sources) vertex_index: Option, } impl SourcePoint { /// Create a new source point with free orientation pub fn new(position: [f64; 3]) -> Self { Self { position: Vector3::new(position[0], position[1], position[2]), orientation: None, normal: None, hemisphere: 0, vertex_index: None, } } /// Create a source point with fixed orientation pub fn with_orientation(position: [f64; 3], orientation: [f64; 3]) -> Self { let ori = Vector3::new(orientation[0], orientation[1], orientation[2]); Self { position: Vector3::new(position[0], position[1], position[2]), orientation: Some(normalize(&ori)), normal: Some(normalize(&ori)), hemisphere: 0, vertex_index: None, } } /// Create a cortical source with surface normal pub fn cortical( position: [f64; 3], normal: [f64; 3], hemisphere: i8, vertex_index: usize, ) -> Self { let n = Vector3::new(normal[0], normal[1], normal[2]); Self { position: Vector3::new(position[0], position[1], position[2]), orientation: Some(normalize(&n)), normal: Some(normalize(&n)), hemisphere, vertex_index: Some(vertex_index), } } /// Get the source position pub fn position(&self) -> &Position { &self.position } /// Get the source orientation (if fixed) pub fn orientation(&self) -> Option { self.orientation } /// Get the surface normal pub fn normal(&self) -> Option<&Orientation> { self.normal.as_ref() } /// Get hemisphere (-1=left, 1=right, 0=unknown) pub fn hemisphere(&self) -> i8 { self.hemisphere } /// Get vertex index for surface sources pub fn vertex_index(&self) -> Option { self.vertex_index } } /// Source space containing all source locations #[derive(Debug, Clone)] pub struct SourceSpace { /// Source points sources: Vec, /// Orientation constraint orientation: SourceOrientation, /// Subject name subject: Option, } impl SourceSpace { /// Create a new empty source space pub fn new(orientation: SourceOrientation) -> Self { Self { sources: Vec::new(), orientation, subject: None, } } /// Create a source space from points pub fn from_points(points: Vec, orientation: SourceOrientation) -> Self { Self { sources: points, orientation, subject: None, } } /// Add a source point pub fn add_source(&mut self, source: SourcePoint) { self.sources.push(source); } /// Get number of sources pub fn len(&self) -> usize { self.sources.len() } /// Check if empty pub fn is_empty(&self) -> bool { self.sources.is_empty() } /// Check if sources have fixed orientation pub fn is_fixed_orientation(&self) -> bool { matches!(self.orientation, SourceOrientation::Fixed) } /// Get orientation constraint pub fn orientation_constraint(&self) -> SourceOrientation { self.orientation } /// Iterate over sources pub fn iter(&self) -> impl Iterator { self.sources.iter() } /// Get a specific source pub fn get(&self, index: usize) -> Option<&SourcePoint> { self.sources.get(index) } /// Create a volume source space (regular grid) /// /// # Arguments /// * `bounds` - Bounding box [(xmin, xmax), (ymin, ymax), (zmin, zmax)] /// * `spacing` - Grid spacing in meters /// * `exclude_radius` - Optional radius from origin to exclude (for ventricles) pub fn create_volume_grid( bounds: [(f64, f64); 3], spacing: f64, exclude_radius: Option, ) -> Self { let mut sources = Vec::new(); let mut x = bounds[0].0; while x <= bounds[0].1 { let mut y = bounds[1].0; while y <= bounds[1].1 { let mut z = bounds[2].0; while z <= bounds[2].1 { let pos = Vector3::new(x, y, z); // Check exclusion radius if let Some(r) = exclude_radius && norm(&pos) < r { z += spacing; continue; } sources.push(SourcePoint::new([x, y, z])); z += spacing; } y += spacing; } x += spacing; } Self { sources, orientation: SourceOrientation::Free, subject: None, } } /// Create a spherical shell source space /// /// # Arguments /// * `center` - Center of the sphere /// * `radius` - Radius of the shell /// * `n_points` - Approximate number of points pub fn create_spherical_shell(center: [f64; 3], radius: f64, n_points: usize) -> Self { let mut sources = Vec::new(); let center = Vector3::new(center[0], center[1], center[2]); // Use Fibonacci sphere for uniform distribution let golden_ratio = f64::midpoint(1.0, 5.0_f64.sqrt()); for i in 0..n_points { let theta = 2.0 * PI * i as f64 / golden_ratio; let phi = (1.0 - 2.0 * (i as f64 + 0.5) / n_points as f64).acos(); let x = center.x + radius * phi.sin() * theta.cos(); let y = center.y + radius * phi.sin() * theta.sin(); let z = center.z + radius * phi.cos(); // Normal points outward let normal = normalize(&Vector3::new(x - center.x, y - center.y, z - center.z)); let hemisphere = if x < center.x { -1 } else { 1 }; sources.push(SourcePoint { position: Vector3::new(x, y, z), orientation: Some(normal), normal: Some(normal), hemisphere, vertex_index: Some(i), }); } Self { sources, orientation: SourceOrientation::Fixed, subject: None, } } /// Create a source space on two hemispheric shells (simple cortex model) pub fn create_cortex_shells(center: [f64; 3], radius: f64, n_per_hemisphere: usize) -> Self { let mut sources = Vec::new(); let center = Vector3::new(center[0], center[1], center[2]); let golden_ratio = f64::midpoint(1.0, 5.0_f64.sqrt()); // Left hemisphere for i in 0..n_per_hemisphere { let theta = 2.0 * PI * i as f64 / golden_ratio; let phi = (1.0 - 2.0 * (i as f64 + 0.5) / (2 * n_per_hemisphere) as f64).acos(); let x = center.x - radius * phi.sin() * theta.cos().abs(); let y = center.y + radius * phi.sin() * theta.sin(); let z = center.z + radius * phi.cos(); let normal = normalize(&Vector3::new(x - center.x, y - center.y, z - center.z)); sources.push(SourcePoint { position: Vector3::new(x, y, z), orientation: Some(normal), normal: Some(normal), hemisphere: -1, vertex_index: Some(i), }); } // Right hemisphere for i in 0..n_per_hemisphere { let theta = 2.0 * PI * i as f64 / golden_ratio; let phi = (1.0 - 2.0 * (i as f64 + 0.5) / (2 * n_per_hemisphere) as f64).acos(); let x = center.x + radius * phi.sin() * theta.cos().abs(); let y = center.y + radius * phi.sin() * theta.sin(); let z = center.z + radius * phi.cos(); let normal = normalize(&Vector3::new(x - center.x, y - center.y, z - center.z)); sources.push(SourcePoint { position: Vector3::new(x, y, z), orientation: Some(normal), normal: Some(normal), hemisphere: 1, vertex_index: Some(n_per_hemisphere + i), }); } Self { sources, orientation: SourceOrientation::Fixed, subject: None, } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_source_point() { let src = SourcePoint::new([0.01, 0.02, 0.03]); assert!((src.position().x - 0.01).abs() < 1e-10); } #[test] fn test_source_with_orientation() { let src = SourcePoint::with_orientation([0.0, 0.0, 0.05], [0.0, 0.0, 1.0]); let ori = src.orientation().unwrap(); assert!((ori.z - 1.0).abs() < 1e-10); } #[test] fn test_volume_grid() { let ss = SourceSpace::create_volume_grid( [(-0.02, 0.02), (-0.02, 0.02), (-0.02, 0.02)], 0.01, None, ); assert!(ss.len() > 0); } #[test] fn test_spherical_shell() { let ss = SourceSpace::create_spherical_shell([0.0, 0.0, 0.04], 0.06, 100); assert_eq!(ss.len(), 100); assert!(ss.is_fixed_orientation()); } #[test] fn test_cortex_shells() { let ss = SourceSpace::create_cortex_shells([0.0, 0.0, 0.04], 0.06, 50); assert_eq!(ss.len(), 100); // 50 per hemisphere } }