Initial commit
This commit is contained in:
@@ -0,0 +1,314 @@
|
||||
//! 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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user