315 lines
7.9 KiB
Rust
315 lines
7.9 KiB
Rust
//! Recording metadata and information.
|
|
|
|
use chrono::{DateTime, Utc};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// Information about a recording session
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct RecordingInfo {
|
|
/// Subject ID
|
|
pub subject_id: String,
|
|
/// Recording date and time
|
|
pub recording_date: Option<DateTime<Utc>>,
|
|
/// Recording device/system name
|
|
pub device: String,
|
|
/// Device serial number
|
|
pub device_serial: Option<String>,
|
|
/// Experimenter name
|
|
pub experimenter: Option<String>,
|
|
/// Recording location/institution
|
|
pub institution: Option<String>,
|
|
/// Description/comments
|
|
pub description: String,
|
|
/// Line frequency (50 or 60 Hz)
|
|
pub line_freq: Option<f64>,
|
|
/// High-pass filter setting during acquisition
|
|
pub highpass: Option<f64>,
|
|
/// Low-pass filter setting during acquisition
|
|
pub lowpass: Option<f64>,
|
|
}
|
|
|
|
impl RecordingInfo {
|
|
/// Create a new recording info with minimal required fields
|
|
#[must_use]
|
|
pub fn new(subject_id: impl Into<String>) -> Self {
|
|
Self {
|
|
subject_id: subject_id.into(),
|
|
recording_date: None,
|
|
device: "Unknown".to_string(),
|
|
device_serial: None,
|
|
experimenter: None,
|
|
institution: None,
|
|
description: String::new(),
|
|
line_freq: None,
|
|
highpass: None,
|
|
lowpass: None,
|
|
}
|
|
}
|
|
|
|
/// Set recording date
|
|
#[must_use]
|
|
pub fn with_date(mut self, date: DateTime<Utc>) -> Self {
|
|
self.recording_date = Some(date);
|
|
self
|
|
}
|
|
|
|
/// Set device information
|
|
#[must_use]
|
|
pub fn with_device(mut self, device: impl Into<String>) -> Self {
|
|
self.device = device.into();
|
|
self
|
|
}
|
|
|
|
/// Set line frequency
|
|
#[must_use]
|
|
pub fn with_line_freq(mut self, freq: f64) -> Self {
|
|
self.line_freq = Some(freq);
|
|
self
|
|
}
|
|
}
|
|
|
|
impl Default for RecordingInfo {
|
|
fn default() -> Self {
|
|
Self::new("unknown")
|
|
}
|
|
}
|
|
|
|
/// Handle to a loaded recording
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Recording {
|
|
/// Unique identifier
|
|
pub id: uuid::Uuid,
|
|
/// File path
|
|
pub path: std::path::PathBuf,
|
|
/// Recording metadata
|
|
pub info: RecordingInfo,
|
|
/// Format type
|
|
pub format: RecordingFormat,
|
|
/// Whether data is currently loaded in memory
|
|
pub loaded: bool,
|
|
}
|
|
|
|
impl Recording {
|
|
/// Create a new recording handle
|
|
#[must_use]
|
|
pub fn new(
|
|
path: impl Into<std::path::PathBuf>,
|
|
info: RecordingInfo,
|
|
format: RecordingFormat,
|
|
) -> Self {
|
|
Self {
|
|
id: uuid::Uuid::new_v4(),
|
|
path: path.into(),
|
|
info,
|
|
format,
|
|
loaded: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Supported recording file formats
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
pub enum RecordingFormat {
|
|
/// European Data Format
|
|
Edf,
|
|
/// BioSemi Data Format (24-bit EDF variant)
|
|
Bdf,
|
|
/// BrainVision format (.vhdr/.vmrk/.eeg)
|
|
BrainVision,
|
|
/// EEGLAB format (.set)
|
|
EegLab,
|
|
/// Elekta/Neuromag FIF format
|
|
Fif,
|
|
/// CTF MEG format (.ds folder)
|
|
Ctf,
|
|
/// 4D Neuroimaging format
|
|
FourD,
|
|
/// Yokogawa/Ricoh MEG format
|
|
Yokogawa,
|
|
/// KIT MEG format
|
|
Kit,
|
|
/// Curry format
|
|
Curry,
|
|
/// Neuroscan format (.cnt, .eeg)
|
|
Neuroscan,
|
|
/// Nihon Kohden format
|
|
NihonKohden,
|
|
/// BIDS dataset
|
|
Bids,
|
|
/// Neurodata Without Borders
|
|
Nwb,
|
|
/// Generic binary format
|
|
Binary,
|
|
/// Unknown format
|
|
Unknown,
|
|
}
|
|
|
|
impl RecordingFormat {
|
|
/// Detect format from file extension
|
|
#[must_use]
|
|
pub fn from_extension(ext: &str) -> Self {
|
|
match ext.to_lowercase().as_str() {
|
|
"edf" => Self::Edf,
|
|
"bdf" => Self::Bdf,
|
|
"vhdr" | "vmrk" | "eeg" => Self::BrainVision,
|
|
"set" => Self::EegLab,
|
|
"fif" => Self::Fif,
|
|
"ds" => Self::Ctf,
|
|
"cnt" => Self::Neuroscan,
|
|
"nwb" => Self::Nwb,
|
|
_ => Self::Unknown,
|
|
}
|
|
}
|
|
|
|
/// Get typical file extensions for this format
|
|
#[must_use]
|
|
pub fn extensions(&self) -> &[&str] {
|
|
match self {
|
|
Self::Edf => &["edf"],
|
|
Self::Bdf => &["bdf"],
|
|
Self::BrainVision => &["vhdr", "vmrk", "eeg"],
|
|
Self::EegLab => &["set"],
|
|
Self::Fif => &["fif", "fif.gz"],
|
|
Self::Ctf => &["ds"],
|
|
Self::Neuroscan => &["cnt", "eeg", "avg"],
|
|
Self::NihonKohden => &["eeg"],
|
|
Self::Nwb => &["nwb"],
|
|
Self::Yokogawa | Self::Kit => &["sqd", "con", "raw", "ave"],
|
|
_ => &[],
|
|
}
|
|
}
|
|
|
|
/// Whether this format supports continuous raw data
|
|
#[must_use]
|
|
pub fn supports_raw(&self) -> bool {
|
|
matches!(
|
|
self,
|
|
Self::Edf
|
|
| Self::Bdf
|
|
| Self::BrainVision
|
|
| Self::Fif
|
|
| Self::Ctf
|
|
| Self::FourD
|
|
| Self::Neuroscan
|
|
)
|
|
}
|
|
|
|
/// Whether this format supports epochs
|
|
#[must_use]
|
|
pub fn supports_epochs(&self) -> bool {
|
|
matches!(self, Self::EegLab | Self::Fif | Self::Nwb)
|
|
}
|
|
|
|
/// Whether this is an MEG format
|
|
#[must_use]
|
|
pub fn is_meg(&self) -> bool {
|
|
matches!(
|
|
self,
|
|
Self::Fif | Self::Ctf | Self::FourD | Self::Yokogawa | Self::Kit
|
|
)
|
|
}
|
|
|
|
/// Whether this is an EEG format
|
|
#[must_use]
|
|
pub fn is_eeg(&self) -> bool {
|
|
matches!(
|
|
self,
|
|
Self::Edf
|
|
| Self::Bdf
|
|
| Self::BrainVision
|
|
| Self::EegLab
|
|
| Self::Neuroscan
|
|
| Self::NihonKohden
|
|
| Self::Curry
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Processing history for a recording
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ProcessingHistory {
|
|
/// List of processing steps applied
|
|
steps: Vec<ProcessingStep>,
|
|
}
|
|
|
|
impl ProcessingHistory {
|
|
/// Create a new empty history
|
|
#[must_use]
|
|
pub fn new() -> Self {
|
|
Self { steps: Vec::new() }
|
|
}
|
|
|
|
/// Add a processing step
|
|
pub fn add(&mut self, step: ProcessingStep) {
|
|
self.steps.push(step);
|
|
}
|
|
|
|
/// Get all steps
|
|
#[must_use]
|
|
pub fn steps(&self) -> &[ProcessingStep] {
|
|
&self.steps
|
|
}
|
|
}
|
|
|
|
impl Default for ProcessingHistory {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
/// A single processing step
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ProcessingStep {
|
|
/// Name of the processing function
|
|
pub function: String,
|
|
/// Parameters used
|
|
pub parameters: serde_json::Value,
|
|
/// Timestamp when applied
|
|
pub timestamp: DateTime<Utc>,
|
|
/// Description
|
|
pub description: Option<String>,
|
|
}
|
|
|
|
impl ProcessingStep {
|
|
/// Create a new processing step
|
|
#[must_use]
|
|
pub fn new(function: impl Into<String>, parameters: serde_json::Value) -> Self {
|
|
Self {
|
|
function: function.into(),
|
|
parameters,
|
|
timestamp: Utc::now(),
|
|
description: None,
|
|
}
|
|
}
|
|
|
|
/// Add description
|
|
#[must_use]
|
|
pub fn with_description(mut self, desc: impl Into<String>) -> Self {
|
|
self.description = Some(desc.into());
|
|
self
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_format_detection() {
|
|
assert_eq!(RecordingFormat::from_extension("edf"), RecordingFormat::Edf);
|
|
assert_eq!(RecordingFormat::from_extension("BDF"), RecordingFormat::Bdf);
|
|
assert_eq!(
|
|
RecordingFormat::from_extension("vhdr"),
|
|
RecordingFormat::BrainVision
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_format_capabilities() {
|
|
assert!(RecordingFormat::Edf.is_eeg());
|
|
assert!(!RecordingFormat::Edf.is_meg());
|
|
assert!(RecordingFormat::Fif.is_meg());
|
|
assert!(RecordingFormat::Edf.supports_raw());
|
|
}
|
|
}
|