384 lines
9.6 KiB
Rust
384 lines
9.6 KiB
Rust
//! 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));
|
|
}
|
|
}
|