//! Events and annotations for neuroimaging data. use serde::{Deserialize, Serialize}; use std::collections::HashMap; /// Event identifier - maps event codes to names #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EventId { /// Mapping from event name to numeric code pub name_to_id: HashMap, /// Mapping from numeric code to event name pub id_to_name: HashMap, } impl EventId { /// Create a new empty event ID mapping #[must_use] pub fn new() -> Self { Self { name_to_id: HashMap::new(), id_to_name: HashMap::new(), } } /// Add an event type pub fn add(&mut self, name: impl Into, id: i32) { let name = name.into(); self.name_to_id.insert(name.clone(), id); self.id_to_name.insert(id, name); } /// Get the numeric ID for an event name #[must_use] pub fn get_id(&self, name: &str) -> Option { self.name_to_id.get(name).copied() } /// Get the name for a numeric ID #[must_use] pub fn get_name(&self, id: i32) -> Option<&str> { self.id_to_name.get(&id).map(String::as_str) } /// Get all event names #[must_use] pub fn names(&self) -> Vec<&str> { self.name_to_id.keys().map(String::as_str).collect() } } impl Default for EventId { fn default() -> Self { Self::new() } } /// A discrete event in the recording (stimulus, response, marker) #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub struct Event { /// Sample number when the event occurred (0-indexed) pub sample: usize, /// Duration in samples (0 for instantaneous events) pub duration: usize, /// Event code/value pub value: i32, } impl Event { /// Create a new instantaneous event #[must_use] pub fn new(sample: usize, value: i32) -> Self { Self { sample, duration: 0, value, } } /// Create a new event with duration #[must_use] pub fn with_duration(sample: usize, duration: usize, value: i32) -> Self { Self { sample, duration, value, } } /// Convert sample to time given sampling frequency #[must_use] pub fn time(&self, sfreq: f64) -> f64 { self.sample as f64 / sfreq } /// Convert duration to time given sampling frequency #[must_use] pub fn duration_sec(&self, sfreq: f64) -> f64 { self.duration as f64 / sfreq } } /// Collection of events #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct Events { /// List of events sorted by sample number events: Vec, /// Event ID mapping pub event_id: EventId, } impl Events { /// Create a new empty events collection #[must_use] pub fn new() -> Self { Self { events: Vec::new(), event_id: EventId::new(), } } /// Create from a vector of events #[must_use] pub fn from_events(mut events: Vec, event_id: EventId) -> Self { events.sort_by_key(|e| e.sample); Self { events, event_id } } /// Number of events #[must_use] pub fn len(&self) -> usize { self.events.len() } /// Returns true if there are no events #[must_use] pub fn is_empty(&self) -> bool { self.events.is_empty() } /// Add an event pub fn add(&mut self, event: Event) { let pos = self .events .binary_search_by_key(&event.sample, |e| e.sample) .unwrap_or_else(|e| e); self.events.insert(pos, event); } /// Get events matching a specific value #[must_use] pub fn find_by_value(&self, value: i32) -> Vec<&Event> { self.events.iter().filter(|e| e.value == value).collect() } /// Get events matching a specific name #[must_use] pub fn find_by_name(&self, name: &str) -> Vec<&Event> { if let Some(id) = self.event_id.get_id(name) { self.find_by_value(id) } else { Vec::new() } } /// Get events in a time range (inclusive) #[must_use] pub fn in_range(&self, start_sample: usize, end_sample: usize) -> Vec<&Event> { self.events .iter() .filter(|e| e.sample >= start_sample && e.sample <= end_sample) .collect() } /// Get all events as a slice #[must_use] pub fn as_slice(&self) -> &[Event] { &self.events } /// Iterate over events pub fn iter(&self) -> impl Iterator { self.events.iter() } /// Get unique event values #[must_use] pub fn unique_values(&self) -> Vec { let mut values: Vec = self.events.iter().map(|e| e.value).collect(); values.sort_unstable(); values.dedup(); values } /// Count events by value #[must_use] pub fn counts(&self) -> HashMap { let mut counts = HashMap::new(); for event in &self.events { *counts.entry(event.value).or_insert(0) += 1; } counts } } impl IntoIterator for Events { type Item = Event; type IntoIter = std::vec::IntoIter; fn into_iter(self) -> Self::IntoIter { self.events.into_iter() } } impl<'a> IntoIterator for &'a Events { type Item = &'a Event; type IntoIter = std::slice::Iter<'a, Event>; fn into_iter(self) -> Self::IntoIter { self.events.iter() } } /// An annotation - text description with time range #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Annotation { /// Start time in seconds pub onset: f64, /// Duration in seconds pub duration: f64, /// Description/label pub description: String, } impl Annotation { /// Create a new annotation #[must_use] pub fn new(onset: f64, duration: f64, description: impl Into) -> Self { Self { onset, duration, description: description.into(), } } /// Create an instantaneous annotation (duration = 0) #[must_use] pub fn instant(onset: f64, description: impl Into) -> Self { Self::new(onset, 0.0, description) } /// End time of the annotation #[must_use] pub fn end(&self) -> f64 { self.onset + self.duration } /// Check if a time point falls within this annotation #[must_use] pub fn contains(&self, time: f64) -> bool { time >= self.onset && time <= self.end() } } /// Collection of annotations #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct Annotations { annotations: Vec, } impl Annotations { /// Create a new empty annotations collection #[must_use] pub fn new() -> Self { Self { annotations: Vec::new(), } } /// Number of annotations #[must_use] pub fn len(&self) -> usize { self.annotations.len() } /// Returns true if there are no annotations #[must_use] pub fn is_empty(&self) -> bool { self.annotations.is_empty() } /// Add an annotation pub fn add(&mut self, annotation: Annotation) { self.annotations.push(annotation); self.annotations.sort_by(|a, b| { a.onset .partial_cmp(&b.onset) .unwrap_or(std::cmp::Ordering::Equal) }); } /// Get annotations containing a time point #[must_use] pub fn at_time(&self, time: f64) -> Vec<&Annotation> { self.annotations .iter() .filter(|a| a.contains(time)) .collect() } /// Get annotations with a specific description #[must_use] pub fn find_by_description(&self, desc: &str) -> Vec<&Annotation> { self.annotations .iter() .filter(|a| a.description == desc) .collect() } /// Get unique descriptions #[must_use] pub fn unique_descriptions(&self) -> Vec<&str> { let mut descs: Vec<&str> = self .annotations .iter() .map(|a| a.description.as_str()) .collect(); descs.sort_unstable(); descs.dedup(); descs } /// Iterate over annotations pub fn iter(&self) -> impl Iterator { self.annotations.iter() } } #[cfg(test)] mod tests { use super::*; #[test] fn test_event_creation() { let event = Event::new(1000, 1); assert_eq!(event.sample, 1000); assert_eq!(event.value, 1); assert_eq!(event.duration, 0); // Test time conversion at 1000 Hz assert!((event.time(1000.0) - 1.0).abs() < 1e-10); } #[test] fn test_events_sorting() { let mut events = Events::new(); events.add(Event::new(500, 2)); events.add(Event::new(100, 1)); events.add(Event::new(300, 3)); let samples: Vec = events.iter().map(|e| e.sample).collect(); assert_eq!(samples, vec![100, 300, 500]); } #[test] fn test_event_id_mapping() { let mut event_id = EventId::new(); event_id.add("stimulus", 1); event_id.add("response", 2); assert_eq!(event_id.get_id("stimulus"), Some(1)); assert_eq!(event_id.get_name(2), Some("response")); assert_eq!(event_id.get_id("unknown"), None); } #[test] fn test_annotation() { let ann = Annotation::new(1.5, 0.5, "artifact"); assert!(ann.contains(1.5)); assert!(ann.contains(1.7)); assert!(ann.contains(2.0)); assert!(!ann.contains(2.1)); } }