use serde::{Deserialize, Serialize};
pub const SELECT_MASK_SIZE: usize = 6;
pub const SUPPORTED_PLATFORMS: &[&str] = &["linux", "macos", "windows"];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ChannelFormat {
Individual,
Pairs,
DirectedPairs,
}
impl ChannelFormat {
pub fn from_str(s: &str) -> Option<Self> {
match s {
"individual" => Some(Self::Individual),
"pairs" => Some(Self::Pairs),
"directed_pairs" => Some(Self::DirectedPairs),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct OutputColumns {
pub coefficients: u8,
pub has_error: bool,
}
#[derive(Debug, Clone, Serialize)]
pub struct VariantMetadata {
pub abbreviation: &'static str,
pub name: &'static str,
pub position: u8,
pub output_suffix: &'static str,
pub stride: u8,
pub reserved: bool,
#[serde(skip)]
pub required_params: &'static [&'static str],
pub channel_format: ChannelFormat,
pub output_columns: OutputColumns,
pub documentation: &'static str,
}
impl VariantMetadata {
pub fn from_abbrev(abbrev: &str) -> Option<&'static VariantMetadata> {
VARIANT_REGISTRY.iter().find(|v| v.abbreviation == abbrev)
}
pub fn from_suffix(suffix: &str) -> Option<&'static VariantMetadata> {
VARIANT_REGISTRY.iter().find(|v| v.output_suffix == suffix)
}
pub fn from_position(pos: u8) -> Option<&'static VariantMetadata> {
VARIANT_REGISTRY.iter().find(|v| v.position == pos)
}
pub fn active_variants() -> impl Iterator<Item = &'static VariantMetadata> {
VARIANT_REGISTRY.iter().filter(|v| !v.reserved)
}
}
pub const ST: VariantMetadata = VariantMetadata {
abbreviation: "ST",
name: "Single Timeseries",
position: 0,
output_suffix: "_ST",
stride: 4,
reserved: false,
required_params: &[],
channel_format: ChannelFormat::Individual,
output_columns: OutputColumns {
coefficients: 3,
has_error: true,
},
documentation: "Analyzes individual channels independently. Most basic variant. One result row per channel.",
};
pub const CT: VariantMetadata = VariantMetadata {
abbreviation: "CT",
name: "Cross-Timeseries",
position: 1,
output_suffix: "_CT",
stride: 4,
reserved: false,
required_params: &["-WL_CT", "-WS_CT"],
channel_format: ChannelFormat::Pairs,
output_columns: OutputColumns {
coefficients: 3,
has_error: true,
},
documentation: "Analyzes relationships between channel pairs. Symmetric: pair (1,2) equals (2,1). When enabled with ST, wrapper must run CT pairs separately.",
};
pub const CD: VariantMetadata = VariantMetadata {
abbreviation: "CD",
name: "Cross-Dynamical",
position: 2,
output_suffix: "_CD_DDA_ST",
stride: 2,
reserved: false,
required_params: &["-WL_CT", "-WS_CT"],
channel_format: ChannelFormat::DirectedPairs,
output_columns: OutputColumns {
coefficients: 1,
has_error: true,
},
documentation: "Analyzes directed causal relationships. Asymmetric: (1->2) differs from (2->1). CD is independent (no longer requires ST+CT).",
};
pub const RESERVED: VariantMetadata = VariantMetadata {
abbreviation: "RESERVED",
name: "Reserved",
position: 3,
output_suffix: "_RESERVED",
stride: 1,
reserved: true,
required_params: &[],
channel_format: ChannelFormat::Individual,
output_columns: OutputColumns {
coefficients: 0,
has_error: false,
},
documentation: "Internal development function. Should always be set to 0 in production.",
};
pub const DE: VariantMetadata = VariantMetadata {
abbreviation: "DE",
name: "Delay Embedding",
position: 4,
output_suffix: "_DE",
stride: 1,
reserved: false,
required_params: &["-WL_CT", "-WS_CT"],
channel_format: ChannelFormat::Individual,
output_columns: OutputColumns {
coefficients: 0,
has_error: false,
},
documentation: "Tests for ergodic behavior in dynamical systems. Produces single aggregate measure per time window (not per-channel).",
};
pub const SY: VariantMetadata = VariantMetadata {
abbreviation: "SY",
name: "Synchronization",
position: 5,
output_suffix: "_SY",
stride: 1,
reserved: false,
required_params: &[],
channel_format: ChannelFormat::Individual,
output_columns: OutputColumns {
coefficients: 0,
has_error: false,
},
documentation: "Detects synchronized behavior between signals. Produces one value per channel/measure per time window.",
};
pub const VARIANT_REGISTRY: &[VariantMetadata] = &[ST, CT, CD, RESERVED, DE, SY];
pub const VARIANT_ORDER: &[&str] = &["ST", "CT", "CD", "RESERVED", "DE", "SY"];
pub fn generate_select_mask(variants: &[&str]) -> [u8; SELECT_MASK_SIZE] {
let mut mask = [0u8; SELECT_MASK_SIZE];
for abbrev in variants {
if let Some(variant) = VariantMetadata::from_abbrev(abbrev) {
mask[variant.position as usize] = 1;
}
}
mask
}
pub fn parse_select_mask(mask: &[u8]) -> Vec<&'static str> {
mask.iter()
.enumerate()
.filter(|(_, &bit)| bit == 1)
.filter_map(|(pos, _)| VariantMetadata::from_position(pos as u8))
.filter(|v| !v.reserved)
.map(|v| v.abbreviation)
.collect()
}
pub fn format_select_mask(mask: &[u8; SELECT_MASK_SIZE]) -> String {
mask.iter()
.map(|b| b.to_string())
.collect::<Vec<_>>()
.join(" ")
}
pub mod select_mask_positions {
pub const ST: usize = 0;
pub const CT: usize = 1;
pub const CD: usize = 2;
pub const RESERVED: usize = 3;
pub const DE: usize = 4;
pub const SY: usize = 5;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum FileType {
EDF,
ASCII,
}
impl FileType {
pub fn flag(&self) -> &'static str {
match self {
Self::EDF => "-EDF",
Self::ASCII => "-ASCII",
}
}
pub fn from_extension(ext: &str) -> Option<Self> {
match ext.to_lowercase().as_str() {
"edf" => Some(Self::EDF),
"ascii" => Some(Self::ASCII),
"txt" => Some(Self::ASCII),
"csv" => Some(Self::ASCII),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_variant_registry_size() {
assert_eq!(VARIANT_REGISTRY.len(), 6);
}
#[test]
fn test_variant_lookup_by_abbrev() {
assert!(VariantMetadata::from_abbrev("ST").is_some());
assert!(VariantMetadata::from_abbrev("CT").is_some());
assert!(VariantMetadata::from_abbrev("CD").is_some());
assert!(VariantMetadata::from_abbrev("DE").is_some());
assert!(VariantMetadata::from_abbrev("SY").is_some());
assert!(VariantMetadata::from_abbrev("INVALID").is_none());
}
#[test]
fn test_variant_lookup_by_suffix() {
assert!(VariantMetadata::from_suffix("_ST").is_some());
assert!(VariantMetadata::from_suffix("_CT").is_some());
assert!(VariantMetadata::from_suffix("_CD_DDA_ST").is_some());
assert!(VariantMetadata::from_suffix("_DE").is_some());
assert!(VariantMetadata::from_suffix("_SY").is_some());
}
#[test]
fn test_select_mask_generation() {
let mask = generate_select_mask(&["ST", "SY"]);
assert_eq!(mask[select_mask_positions::ST], 1);
assert_eq!(mask[select_mask_positions::SY], 1);
assert_eq!(mask[select_mask_positions::CT], 0);
}
#[test]
fn test_select_mask_parsing() {
let mask = [1, 1, 0, 0, 1, 0];
let variants = parse_select_mask(&mask);
assert!(variants.contains(&"ST"));
assert!(variants.contains(&"CT"));
assert!(variants.contains(&"DE"));
assert!(!variants.contains(&"CD"));
}
#[test]
fn test_file_type_flags() {
assert_eq!(FileType::EDF.flag(), "-EDF");
assert_eq!(FileType::ASCII.flag(), "-ASCII");
}
#[test]
fn test_file_type_detection() {
assert_eq!(FileType::from_extension("edf"), Some(FileType::EDF));
assert_eq!(FileType::from_extension("ascii"), Some(FileType::ASCII));
assert_eq!(FileType::from_extension("txt"), Some(FileType::ASCII));
assert_eq!(FileType::from_extension("csv"), Some(FileType::ASCII));
assert!(FileType::from_extension("unknown").is_none());
}
#[test]
fn test_stride_values() {
assert_eq!(ST.stride, 4);
assert_eq!(CT.stride, 4);
assert_eq!(CD.stride, 2);
assert_eq!(DE.stride, 1);
assert_eq!(SY.stride, 1);
}
}