//! Artifact type definitions and labels. //! //! Defines the types of artifacts that can be detected in MEG/EEG recordings. use serde::{Deserialize, Serialize}; /// Types of artifacts that can be detected #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum ArtifactType { /// Eye blink artifact (EOG) /// Characterized by large frontal deflections lasting 100-400ms EyeBlink, /// Eye movement artifact (saccades, smooth pursuit) /// Horizontal or vertical eye movements EyeMovement, /// Muscle artifact (EMG) /// High-frequency (>20Hz) contamination from facial/neck muscles Muscle, /// Heartbeat artifact (ECG/BCG) /// Cardiac-related artifact, especially prominent in MEG Heartbeat, /// Power line noise (50/60 Hz) /// Narrow-band interference from electrical mains LineNoise, /// Movement artifact /// Head or body movement causing signal distortion Movement, /// Electrode pop/jump /// Sudden amplitude changes from electrode issues ElectrodePop, /// Channel noise/bad channel /// Persistently noisy or disconnected channel ChannelNoise, /// Environmental interference /// External electromagnetic interference Environmental, /// Unknown artifact /// Detected anomaly of unknown origin Unknown, } impl ArtifactType { /// Get the index for this artifact type (for model output mapping) pub fn index(&self) -> usize { match self { Self::EyeBlink => 0, Self::EyeMovement => 1, Self::Muscle => 2, Self::Heartbeat => 3, Self::LineNoise => 4, Self::Movement => 5, Self::ElectrodePop => 6, Self::ChannelNoise => 7, Self::Environmental => 8, Self::Unknown => 9, } } /// Create from index pub fn from_index(index: usize) -> Option { match index { 0 => Some(Self::EyeBlink), 1 => Some(Self::EyeMovement), 2 => Some(Self::Muscle), 3 => Some(Self::Heartbeat), 4 => Some(Self::LineNoise), 5 => Some(Self::Movement), 6 => Some(Self::ElectrodePop), 7 => Some(Self::ChannelNoise), 8 => Some(Self::Environmental), 9 => Some(Self::Unknown), _ => None, } } /// Number of artifact types pub const fn count() -> usize { 10 } /// Get all artifact types pub fn all() -> Vec { vec![ Self::EyeBlink, Self::EyeMovement, Self::Muscle, Self::Heartbeat, Self::LineNoise, Self::Movement, Self::ElectrodePop, Self::ChannelNoise, Self::Environmental, Self::Unknown, ] } /// Get human-readable name pub fn name(&self) -> &'static str { match self { Self::EyeBlink => "Eye Blink", Self::EyeMovement => "Eye Movement", Self::Muscle => "Muscle", Self::Heartbeat => "Heartbeat", Self::LineNoise => "Line Noise", Self::Movement => "Movement", Self::ElectrodePop => "Electrode Pop", Self::ChannelNoise => "Channel Noise", Self::Environmental => "Environmental", Self::Unknown => "Unknown", } } /// Get short code pub fn code(&self) -> &'static str { match self { Self::EyeBlink => "EOG_B", Self::EyeMovement => "EOG_M", Self::Muscle => "EMG", Self::Heartbeat => "ECG", Self::LineNoise => "LINE", Self::Movement => "MOV", Self::ElectrodePop => "POP", Self::ChannelNoise => "BAD", Self::Environmental => "ENV", Self::Unknown => "UNK", } } /// Get typical frequency range for this artifact type pub fn frequency_range(&self) -> (f64, f64) { match self { Self::EyeBlink => (0.5, 4.0), Self::EyeMovement => (0.5, 10.0), Self::Muscle => (20.0, 200.0), Self::Heartbeat => (0.5, 40.0), Self::LineNoise => (49.0, 61.0), // Covers both 50 and 60 Hz Self::Movement => (0.1, 5.0), Self::ElectrodePop => (0.1, 100.0), // Broadband Self::ChannelNoise => (0.1, 200.0), // Broadband Self::Environmental => (0.1, 200.0), // Variable Self::Unknown => (0.1, 200.0), } } /// Get typical duration range in milliseconds pub fn duration_range_ms(&self) -> (f64, f64) { match self { Self::EyeBlink => (100.0, 400.0), Self::EyeMovement => (50.0, 500.0), Self::Muscle => (10.0, 1000.0), // Can be sustained Self::Heartbeat => (200.0, 400.0), // QRS complex Self::LineNoise => (0.0, f64::INFINITY), // Continuous Self::Movement => (100.0, 5000.0), Self::ElectrodePop => (1.0, 50.0), // Very brief Self::ChannelNoise => (0.0, f64::INFINITY), // Continuous Self::Environmental => (0.0, f64::INFINITY), // Variable Self::Unknown => (0.0, f64::INFINITY), } } /// Get color for visualization (RGB) pub fn color(&self) -> (u8, u8, u8) { match self { Self::EyeBlink => (255, 165, 0), // Orange Self::EyeMovement => (255, 200, 0), // Gold Self::Muscle => (255, 0, 0), // Red Self::Heartbeat => (255, 0, 128), // Pink Self::LineNoise => (128, 128, 128), // Gray Self::Movement => (0, 128, 255), // Blue Self::ElectrodePop => (255, 255, 0), // Yellow Self::ChannelNoise => (128, 0, 128), // Purple Self::Environmental => (0, 128, 0), // Green Self::Unknown => (64, 64, 64), // Dark gray } } } impl std::fmt::Display for ArtifactType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.name()) } } /// A detected artifact label with probability #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ArtifactLabel { /// Type of artifact pub artifact_type: ArtifactType, /// Detection probability (0.0 - 1.0) pub probability: f64, /// Confidence score (may differ from probability for some models) pub confidence: f64, } impl ArtifactLabel { /// Create a new artifact label pub fn new(artifact_type: ArtifactType, probability: f64) -> Self { Self { artifact_type, probability, confidence: probability, } } /// Create with separate confidence score pub fn with_confidence(artifact_type: ArtifactType, probability: f64, confidence: f64) -> Self { Self { artifact_type, probability, confidence, } } /// Check if this artifact is considered detected (probability > threshold) pub fn is_detected(&self, threshold: f64) -> bool { self.probability > threshold } } /// A region in the signal where an artifact was detected #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ArtifactRegion { /// Artifact type pub artifact_type: ArtifactType, /// Start time in seconds pub start_time: f64, /// End time in seconds pub end_time: f64, /// Affected channel indices pub channels: Vec, /// Detection probability pub probability: f64, /// Peak location (time of maximum artifact influence) pub peak_time: Option, /// Severity score (0.0 - 1.0) pub severity: f64, } impl ArtifactRegion { /// Create a new artifact region pub fn new( artifact_type: ArtifactType, start_time: f64, end_time: f64, channels: Vec, probability: f64, ) -> Self { Self { artifact_type, start_time, end_time, channels, probability, peak_time: None, severity: probability, } } /// Duration in seconds pub fn duration(&self) -> f64 { self.end_time - self.start_time } /// Duration in milliseconds pub fn duration_ms(&self) -> f64 { self.duration() * 1000.0 } /// Check if this region overlaps with a time range pub fn overlaps(&self, start: f64, end: f64) -> bool { self.start_time < end && self.end_time > start } /// Check if this region affects a specific channel pub fn affects_channel(&self, channel: usize) -> bool { self.channels.contains(&channel) } /// Merge with another overlapping region of the same type pub fn merge(&self, other: &Self) -> Option { if self.artifact_type != other.artifact_type { return None; } if !self.overlaps(other.start_time, other.end_time) { return None; } let mut channels: Vec = self.channels.clone(); for ch in &other.channels { if !channels.contains(ch) { channels.push(*ch); } } channels.sort_unstable(); Some(Self { artifact_type: self.artifact_type, start_time: self.start_time.min(other.start_time), end_time: self.end_time.max(other.end_time), channels, probability: self.probability.max(other.probability), peak_time: match (self.peak_time, other.peak_time) { (Some(a), Some(b)) => Some(if self.probability > other.probability { a } else { b }), (Some(a), None) => Some(a), (None, Some(b)) => Some(b), (None, None) => None, }, severity: self.severity.max(other.severity), }) } } /// Summary statistics for artifact detection #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct ArtifactSummary { /// Total number of artifacts detected pub total_artifacts: usize, /// Count per artifact type pub counts: Vec<(ArtifactType, usize)>, /// Total contaminated duration (seconds) pub total_duration: f64, /// Percentage of signal contaminated pub contamination_percent: f64, /// Most common artifact type pub most_common: Option, /// Channels most affected pub most_affected_channels: Vec, } impl ArtifactSummary { /// Create from a list of artifact regions pub fn from_regions(regions: &[ArtifactRegion], total_signal_duration: f64) -> Self { let mut counts: std::collections::HashMap = std::collections::HashMap::new(); let mut total_duration = 0.0; let mut channel_counts: std::collections::HashMap = std::collections::HashMap::new(); for region in regions { *counts.entry(region.artifact_type).or_insert(0) += 1; total_duration += region.duration(); for &ch in ®ion.channels { *channel_counts.entry(ch).or_insert(0) += 1; } } let counts_vec: Vec<(ArtifactType, usize)> = counts.into_iter().collect(); let most_common = counts_vec.iter().max_by_key(|(_, c)| *c).map(|(t, _)| *t); let mut channel_vec: Vec<(usize, usize)> = channel_counts.into_iter().collect(); channel_vec.sort_by(|a, b| b.1.cmp(&a.1)); let most_affected_channels: Vec = channel_vec.into_iter().take(5).map(|(ch, _)| ch).collect(); let contamination_percent = if total_signal_duration > 0.0 { (total_duration / total_signal_duration) * 100.0 } else { 0.0 }; Self { total_artifacts: regions.len(), counts: counts_vec, total_duration, contamination_percent, most_common, most_affected_channels, } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_artifact_type_index() { assert_eq!(ArtifactType::EyeBlink.index(), 0); assert_eq!(ArtifactType::Muscle.index(), 2); assert_eq!(ArtifactType::from_index(2), Some(ArtifactType::Muscle)); } #[test] fn test_artifact_type_all() { let all = ArtifactType::all(); assert_eq!(all.len(), ArtifactType::count()); } #[test] fn test_artifact_label() { let label = ArtifactLabel::new(ArtifactType::EyeBlink, 0.85); assert!(label.is_detected(0.5)); assert!(!label.is_detected(0.9)); } #[test] fn test_artifact_region() { let region = ArtifactRegion::new(ArtifactType::Muscle, 1.0, 1.5, vec![10, 11, 12], 0.9); assert_eq!(region.duration(), 0.5); assert_eq!(region.duration_ms(), 500.0); assert!(region.affects_channel(10)); assert!(!region.affects_channel(5)); } #[test] fn test_region_overlap() { let region = ArtifactRegion::new(ArtifactType::Muscle, 1.0, 2.0, vec![0], 0.9); assert!(region.overlaps(0.5, 1.5)); assert!(region.overlaps(1.5, 2.5)); assert!(!region.overlaps(2.5, 3.0)); assert!(!region.overlaps(0.0, 0.5)); } #[test] fn test_region_merge() { let r1 = ArtifactRegion::new(ArtifactType::Muscle, 1.0, 2.0, vec![0, 1], 0.8); let r2 = ArtifactRegion::new(ArtifactType::Muscle, 1.5, 2.5, vec![1, 2], 0.9); let merged = r1.merge(&r2).unwrap(); assert_eq!(merged.start_time, 1.0); assert_eq!(merged.end_time, 2.5); assert_eq!(merged.channels, vec![0, 1, 2]); assert_eq!(merged.probability, 0.9); } #[test] fn test_artifact_summary() { let regions = vec![ ArtifactRegion::new(ArtifactType::EyeBlink, 0.0, 0.3, vec![0, 1], 0.9), ArtifactRegion::new(ArtifactType::EyeBlink, 1.0, 1.3, vec![0, 1], 0.85), ArtifactRegion::new(ArtifactType::Muscle, 2.0, 2.5, vec![10], 0.7), ]; let summary = ArtifactSummary::from_regions(®ions, 10.0); assert_eq!(summary.total_artifacts, 3); assert!((summary.total_duration - 1.1).abs() < 0.01); assert_eq!(summary.most_common, Some(ArtifactType::EyeBlink)); } }