Initial commit
This commit is contained in:
@@ -0,0 +1,383 @@
|
||||
//! 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<String, i32>,
|
||||
/// Mapping from numeric code to event name
|
||||
pub id_to_name: HashMap<i32, String>,
|
||||
}
|
||||
|
||||
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<String>, 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<i32> {
|
||||
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>,
|
||||
/// 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>, 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<Item = &Event> {
|
||||
self.events.iter()
|
||||
}
|
||||
|
||||
/// Get unique event values
|
||||
#[must_use]
|
||||
pub fn unique_values(&self) -> Vec<i32> {
|
||||
let mut values: Vec<i32> = 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<i32, usize> {
|
||||
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<Event>;
|
||||
|
||||
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<String>) -> Self {
|
||||
Self {
|
||||
onset,
|
||||
duration,
|
||||
description: description.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an instantaneous annotation (duration = 0)
|
||||
#[must_use]
|
||||
pub fn instant(onset: f64, description: impl Into<String>) -> 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<Annotation>,
|
||||
}
|
||||
|
||||
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<Item = &Annotation> {
|
||||
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<usize> = 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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user