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]>
348 lines
10 KiB
Rust
348 lines
10 KiB
Rust
//! 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<Orientation>,
|
|
/// Surface normal (for cortical sources)
|
|
normal: Option<Orientation>,
|
|
/// Hemisphere (left=-1, right=1)
|
|
hemisphere: i8,
|
|
/// Vertex index (for surface sources)
|
|
vertex_index: Option<usize>,
|
|
}
|
|
|
|
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<Orientation> {
|
|
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<usize> {
|
|
self.vertex_index
|
|
}
|
|
}
|
|
|
|
/// Source space containing all source locations
|
|
#[derive(Debug, Clone)]
|
|
pub struct SourceSpace {
|
|
/// Source points
|
|
sources: Vec<SourcePoint>,
|
|
/// Orientation constraint
|
|
orientation: SourceOrientation,
|
|
/// Subject name
|
|
subject: Option<String>,
|
|
}
|
|
|
|
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<SourcePoint>, 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<Item = &SourcePoint> {
|
|
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<f64>,
|
|
) -> 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
|
|
}
|
|
}
|