//! FreeSurfer subject container //! //! Provides a unified interface for loading anatomy from a FreeSurfer subject directory. use std::collections::HashMap; use std::path::{Path, PathBuf}; use serde::{Deserialize, Serialize}; use crate::annotation::{Annotation, annotation_filename, read_annotation}; use crate::curvature::{Curvature, curvature_filename, read_curvature}; use crate::error::{AnatomyError, Result}; use crate::surface::{Hemisphere, SurfaceMesh, SurfaceType, read_surface}; /// Handle to a loaded FreeSurfer subject #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FreeSurferHandle { /// Unique identifier pub id: String, /// Path to subject directory pub path: String, /// Subject ID (directory name) pub subject_id: String, /// Available surfaces pub available_surfaces: Vec<(Hemisphere, SurfaceType)>, /// Available annotations pub available_annotations: Vec<(Hemisphere, String)>, } /// A FreeSurfer subject with lazy-loaded surfaces and annotations pub struct FreeSurferSubject { /// Path to subject directory pub path: PathBuf, /// Subject ID (usually the directory name) pub subject_id: String, /// Loaded surfaces (cached) surfaces: HashMap<(Hemisphere, SurfaceType), SurfaceMesh>, /// Loaded annotations (cached) annotations: HashMap<(Hemisphere, String), Annotation>, /// Loaded curvature data (cached) curvatures: HashMap<(Hemisphere, String), Curvature>, } impl FreeSurferSubject { /// Open a FreeSurfer subject directory /// /// # Arguments /// /// * `path` - Path to the subject directory (e.g., /subjects/fsaverage) /// /// # Returns /// /// A `FreeSurferSubject` ready for loading surfaces and annotations /// /// # Errors /// /// Returns error if the directory doesn't exist or isn't a valid FreeSurfer subject /// /// # Example /// /// ```ignore /// use rtx_neuro_anatomy::FreeSurferSubject; /// /// let subject = FreeSurferSubject::open("/path/to/subject")?; /// println!("Loaded subject: {}", subject.subject_id); /// ``` pub fn open(path: impl AsRef) -> Result { let path = path.as_ref().to_path_buf(); // Validate: surf directory must exist let surf_dir = path.join("surf"); if !surf_dir.exists() { return Err(AnatomyError::InvalidSubjectDir { path }); } // Extract subject ID from directory name let subject_id = path .file_name() .and_then(|n| n.to_str()) .map_or_else(|| "unknown".to_string(), String::from); Ok(Self { path, subject_id, surfaces: HashMap::new(), annotations: HashMap::new(), curvatures: HashMap::new(), }) } /// Get path to surf directory pub fn surf_dir(&self) -> PathBuf { self.path.join("surf") } /// Get path to label directory pub fn label_dir(&self) -> PathBuf { self.path.join("label") } /// Load a surface (cached) /// /// # Arguments /// /// * `hemisphere` - Left or Right hemisphere /// * `surface_type` - Type of surface to load /// /// # Returns /// /// Reference to the loaded surface mesh pub fn load_surface( &mut self, hemisphere: Hemisphere, surface_type: SurfaceType, ) -> Result<&SurfaceMesh> { let key = (hemisphere, surface_type); if !self.surfaces.contains_key(&key) { let filename = surface_type.filename(&hemisphere); let path = self.surf_dir().join(&filename); let mesh = read_surface(&path, surface_type, hemisphere)?; self.surfaces.insert(key, mesh); } Ok(self.surfaces.get(&key).unwrap()) } /// Load a surface and compute normals pub fn load_surface_with_normals( &mut self, hemisphere: Hemisphere, surface_type: SurfaceType, ) -> Result<&SurfaceMesh> { self.load_surface(hemisphere, surface_type)?; let key = (hemisphere, surface_type); let mesh = self.surfaces.get_mut(&key).unwrap(); if mesh.normals.is_none() { mesh.compute_normals(); } Ok(self.surfaces.get(&key).unwrap()) } /// Get a previously loaded surface (without loading) pub fn get_surface( &self, hemisphere: Hemisphere, surface_type: SurfaceType, ) -> Option<&SurfaceMesh> { self.surfaces.get(&(hemisphere, surface_type)) } /// Get a mutable reference to a loaded surface pub fn get_surface_mut( &mut self, hemisphere: Hemisphere, surface_type: SurfaceType, ) -> Option<&mut SurfaceMesh> { self.surfaces.get_mut(&(hemisphere, surface_type)) } /// Load an annotation (cached) /// /// # Arguments /// /// * `hemisphere` - Left or Right hemisphere /// * `atlas` - Atlas name (e.g., "aparc", "aparc.a2009s") /// /// # Returns /// /// Reference to the loaded annotation pub fn load_annotation(&mut self, hemisphere: Hemisphere, atlas: &str) -> Result<&Annotation> { let key = (hemisphere, atlas.to_string()); if !self.annotations.contains_key(&key) { let filename = annotation_filename(&hemisphere, atlas); let path = self.label_dir().join(&filename); let annot = read_annotation(&path, atlas, hemisphere)?; self.annotations.insert(key.clone(), annot); } Ok(self.annotations.get(&key).unwrap()) } /// Get a previously loaded annotation pub fn get_annotation(&self, hemisphere: Hemisphere, atlas: &str) -> Option<&Annotation> { self.annotations.get(&(hemisphere, atlas.to_string())) } /// Load curvature data (cached) /// /// # Arguments /// /// * `hemisphere` - Left or Right hemisphere /// * `curv_type` - Curvature type (e.g., "curv", "sulc", "thickness") /// /// # Returns /// /// Reference to the loaded curvature data pub fn load_curvature( &mut self, hemisphere: Hemisphere, curv_type: &str, ) -> Result<&Curvature> { let key = (hemisphere, curv_type.to_string()); if !self.curvatures.contains_key(&key) { let filename = curvature_filename(&hemisphere, curv_type); let path = self.surf_dir().join(&filename); let curv = read_curvature(&path, curv_type, hemisphere)?; self.curvatures.insert(key.clone(), curv); } Ok(self.curvatures.get(&key).unwrap()) } /// Get a previously loaded curvature pub fn get_curvature(&self, hemisphere: Hemisphere, curv_type: &str) -> Option<&Curvature> { self.curvatures.get(&(hemisphere, curv_type.to_string())) } /// List available surfaces in the subject directory pub fn list_surfaces(&self) -> Vec<(Hemisphere, SurfaceType)> { let mut available = Vec::new(); let surf_dir = self.surf_dir(); for hemi in [Hemisphere::Left, Hemisphere::Right] { for surf_type in [ SurfaceType::White, SurfaceType::Pial, SurfaceType::Inflated, SurfaceType::Sphere, SurfaceType::Orig, ] { let filename = surf_type.filename(&hemi); if surf_dir.join(&filename).exists() { available.push((hemi, surf_type)); } } } available } /// List available annotations in the subject directory pub fn list_annotations(&self) -> Vec<(Hemisphere, String)> { let mut available = Vec::new(); let label_dir = self.label_dir(); if !label_dir.exists() { return available; } // Common atlases to check let atlases = ["aparc", "aparc.a2009s", "aparc.DKTatlas", "BA_exvivo"]; for hemi in [Hemisphere::Left, Hemisphere::Right] { for atlas in &atlases { let filename = annotation_filename(&hemi, atlas); if label_dir.join(&filename).exists() { available.push((hemi, atlas.to_string())); } } } available } /// List available curvature files pub fn list_curvatures(&self) -> Vec<(Hemisphere, String)> { let mut available = Vec::new(); let surf_dir = self.surf_dir(); let curv_types = ["curv", "sulc", "thickness", "area", "volume"]; for hemi in [Hemisphere::Left, Hemisphere::Right] { for curv_type in &curv_types { let filename = curvature_filename(&hemi, curv_type); if surf_dir.join(&filename).exists() { available.push((hemi, curv_type.to_string())); } } } available } /// Create a handle for IPC pub fn to_handle(&self, id: &str) -> FreeSurferHandle { FreeSurferHandle { id: id.to_string(), path: self.path.display().to_string(), subject_id: self.subject_id.clone(), available_surfaces: self.list_surfaces(), available_annotations: self.list_annotations(), } } /// Unload all cached data to free memory pub fn clear_cache(&mut self) { self.surfaces.clear(); self.annotations.clear(); self.curvatures.clear(); } /// Number of loaded surfaces pub fn n_loaded_surfaces(&self) -> usize { self.surfaces.len() } /// Number of loaded annotations pub fn n_loaded_annotations(&self) -> usize { self.annotations.len() } } #[cfg(test)] mod tests { use super::*; use std::fs; use tempfile::TempDir; fn create_test_subject() -> (TempDir, PathBuf) { let dir = TempDir::new().unwrap(); let subject_path = dir.path().join("test_subject"); // Create required directories fs::create_dir_all(subject_path.join("surf")).unwrap(); fs::create_dir_all(subject_path.join("label")).unwrap(); (dir, subject_path) } #[test] fn test_open_subject() { let (_dir, subject_path) = create_test_subject(); let subject = FreeSurferSubject::open(&subject_path).unwrap(); assert_eq!(subject.subject_id, "test_subject"); assert_eq!(subject.path, subject_path); } #[test] fn test_open_invalid_directory() { let result = FreeSurferSubject::open("/nonexistent/path"); assert!(matches!( result, Err(AnatomyError::InvalidSubjectDir { .. }) )); } #[test] fn test_list_empty_subject() { let (_dir, subject_path) = create_test_subject(); let subject = FreeSurferSubject::open(&subject_path).unwrap(); // No surfaces or annotations should be found assert!(subject.list_surfaces().is_empty()); assert!(subject.list_annotations().is_empty()); } #[test] fn test_handle_creation() { let (_dir, subject_path) = create_test_subject(); let subject = FreeSurferSubject::open(&subject_path).unwrap(); let handle = subject.to_handle("test-id"); assert_eq!(handle.id, "test-id"); assert_eq!(handle.subject_id, "test_subject"); } #[test] fn test_clear_cache() { let (_dir, subject_path) = create_test_subject(); let mut subject = FreeSurferSubject::open(&subject_path).unwrap(); assert_eq!(subject.n_loaded_surfaces(), 0); subject.clear_cache(); assert_eq!(subject.n_loaded_surfaces(), 0); } }