//! Protocol and study organization system. use crate::{NeuroResult, Recording}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::path::{Path, PathBuf}; /// A protocol (study/experiment) containing multiple subjects #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Protocol { /// Protocol name pub name: String, /// Root directory for this protocol pub path: PathBuf, /// Protocol description pub description: String, /// Creation date pub created: DateTime, /// Last modified date pub modified: DateTime, /// Subjects in this protocol subjects: HashMap, /// Group-level analyses group_analyses: Vec, /// Protocol settings pub settings: ProtocolSettings, } impl Protocol { /// Create a new protocol #[must_use] pub fn new(name: impl Into, path: impl Into) -> Self { let now = Utc::now(); Self { name: name.into(), path: path.into(), description: String::new(), created: now, modified: now, subjects: HashMap::new(), group_analyses: Vec::new(), settings: ProtocolSettings::default(), } } /// Add a subject to the protocol pub fn add_subject(&mut self, subject: Subject) { self.subjects.insert(subject.id.clone(), subject); self.modified = Utc::now(); } /// Get a subject by ID #[must_use] pub fn get_subject(&self, id: &str) -> Option<&Subject> { self.subjects.get(id) } /// Get mutable reference to a subject pub fn get_subject_mut(&mut self, id: &str) -> Option<&mut Subject> { self.modified = Utc::now(); self.subjects.get_mut(id) } /// Remove a subject pub fn remove_subject(&mut self, id: &str) -> Option { self.modified = Utc::now(); self.subjects.remove(id) } /// List all subject IDs #[must_use] pub fn subject_ids(&self) -> Vec<&str> { self.subjects.keys().map(String::as_str).collect() } /// Number of subjects #[must_use] pub fn n_subjects(&self) -> usize { self.subjects.len() } /// Iterate over subjects pub fn subjects(&self) -> impl Iterator { self.subjects.values() } /// Add a group analysis pub fn add_group_analysis(&mut self, analysis: GroupAnalysis) { self.group_analyses.push(analysis); self.modified = Utc::now(); } /// Get group analyses #[must_use] pub fn group_analyses(&self) -> &[GroupAnalysis] { &self.group_analyses } /// Save protocol to disk pub fn save(&self) -> NeuroResult<()> { let protocol_file = self.path.join("protocol.json"); let json = serde_json::to_string_pretty(self).map_err(|e| { crate::NeuroError::Io(std::io::Error::new( std::io::ErrorKind::InvalidData, e.to_string(), )) })?; std::fs::write(&protocol_file, json)?; Ok(()) } /// Load protocol from disk pub fn load(path: impl AsRef) -> NeuroResult { let path = path.as_ref(); let protocol_file = path.join("protocol.json"); let json = std::fs::read_to_string(&protocol_file)?; let protocol: Self = serde_json::from_str(&json) .map_err(|e| crate::NeuroError::InvalidFormat(e.to_string()))?; Ok(protocol) } } /// Protocol-wide settings #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProtocolSettings { /// Default sampling frequency for new recordings pub default_sfreq: f64, /// Default line frequency (50 or 60 Hz) pub line_freq: f64, /// Use GPU acceleration if available pub use_gpu: bool, /// Number of parallel workers pub n_jobs: usize, } impl Default for ProtocolSettings { fn default() -> Self { Self { default_sfreq: 1000.0, line_freq: 60.0, use_gpu: true, n_jobs: 4, } } } /// A subject (participant) in a study #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Subject { /// Subject ID pub id: String, /// Subject name (optional, for display) pub name: Option, /// Subject comments/notes pub comments: String, /// Anatomy data pub anatomy: Option, /// Raw recordings recordings: Vec, /// Conditions/sessions conditions: HashMap, } impl Subject { /// Create a new subject #[must_use] pub fn new(id: impl Into) -> Self { Self { id: id.into(), name: None, comments: String::new(), anatomy: None, recordings: Vec::new(), conditions: HashMap::new(), } } /// Add a recording pub fn add_recording(&mut self, recording: Recording) { self.recordings.push(recording); } /// Get recordings #[must_use] pub fn recordings(&self) -> &[Recording] { &self.recordings } /// Add a condition pub fn add_condition(&mut self, condition: Condition) { self.conditions.insert(condition.name.clone(), condition); } /// Get a condition by name #[must_use] pub fn get_condition(&self, name: &str) -> Option<&Condition> { self.conditions.get(name) } /// List condition names #[must_use] pub fn condition_names(&self) -> Vec<&str> { self.conditions.keys().map(String::as_str).collect() } /// Set anatomy pub fn set_anatomy(&mut self, anatomy: Anatomy) { self.anatomy = Some(anatomy); } } /// Anatomical data for a subject #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Anatomy { /// Path to MRI volume pub mri_path: Option, /// Path to cortical surface (left hemisphere) pub surface_lh: Option, /// Path to cortical surface (right hemisphere) pub surface_rh: Option, /// Path to head surface (for BEM/visualization) pub surface_head: Option, /// Fiducial points (NAS, LPA, RPA) in MRI coordinates pub fiducials: HashMap, /// Coordinate transform from MRI to head coordinates pub mri_to_head: Option<[[f64; 4]; 4]>, /// Whether anatomy is from template (vs individual MRI) pub is_template: bool, /// Template name if using template pub template_name: Option, } impl Anatomy { /// Create a new anatomy #[must_use] pub fn new() -> Self { Self { mri_path: None, surface_lh: None, surface_rh: None, surface_head: None, fiducials: HashMap::new(), mri_to_head: None, is_template: false, template_name: None, } } /// Create anatomy from template #[must_use] pub fn from_template(name: impl Into) -> Self { Self { is_template: true, template_name: Some(name.into()), ..Self::new() } } /// Set fiducial point pub fn set_fiducial(&mut self, name: impl Into, pos: [f64; 3]) { self.fiducials.insert(name.into(), pos); } } impl Default for Anatomy { fn default() -> Self { Self::new() } } /// A condition/task within a subject's data #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Condition { /// Condition name pub name: String, /// Description pub description: String, /// Associated data files pub data_files: Vec, /// Epochs file (if computed) pub epochs_file: Option, /// Evoked/average file (if computed) pub evoked_file: Option, /// Source estimate files pub source_files: Vec, } impl Condition { /// Create a new condition #[must_use] pub fn new(name: impl Into) -> Self { Self { name: name.into(), description: String::new(), data_files: Vec::new(), epochs_file: None, evoked_file: None, source_files: Vec::new(), } } /// Add a data file pub fn add_data_file(&mut self, path: impl Into) { self.data_files.push(path.into()); } } /// Group-level analysis #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GroupAnalysis { /// Analysis name pub name: String, /// Description pub description: String, /// Subjects included pub subjects: Vec, /// Conditions compared pub conditions: Vec, /// Output files pub outputs: Vec, /// Creation date pub created: DateTime, } impl GroupAnalysis { /// Create a new group analysis #[must_use] pub fn new(name: impl Into) -> Self { Self { name: name.into(), description: String::new(), subjects: Vec::new(), conditions: Vec::new(), outputs: Vec::new(), created: Utc::now(), } } /// Add subjects pub fn add_subjects>(&mut self, subjects: impl IntoIterator) { self.subjects.extend(subjects.into_iter().map(Into::into)); } /// Add conditions pub fn add_conditions>(&mut self, conditions: impl IntoIterator) { self.conditions .extend(conditions.into_iter().map(Into::into)); } } #[cfg(test)] mod tests { use super::*; use tempfile::TempDir; #[test] fn test_protocol_creation() { let protocol = Protocol::new("test_study", "/tmp/test"); assert_eq!(protocol.name, "test_study"); assert_eq!(protocol.n_subjects(), 0); } #[test] fn test_add_subject() { let mut protocol = Protocol::new("test", "/tmp/test"); protocol.add_subject(Subject::new("sub-01")); protocol.add_subject(Subject::new("sub-02")); assert_eq!(protocol.n_subjects(), 2); assert!(protocol.get_subject("sub-01").is_some()); assert!(protocol.get_subject("sub-03").is_none()); } #[test] fn test_protocol_save_load() { let temp_dir = TempDir::new().unwrap(); let path = temp_dir.path(); let mut protocol = Protocol::new("test", path); protocol.add_subject(Subject::new("sub-01")); protocol.save().unwrap(); let loaded = Protocol::load(path).unwrap(); assert_eq!(loaded.name, "test"); assert_eq!(loaded.n_subjects(), 1); } }