use std::f64::consts::PI;
use crate::metrics;
pub const EEG_BANDS: [(&str, f64, f64); 5] = [
("delta", 0.5, 4.0),
("theta", 4.0, 8.0),
("alpha", 8.0, 13.0),
("beta", 13.0, 30.0),
("gamma", 30.0, 50.0),
];
pub const CLINICAL_BANDS: [(&str, f64, f64); 6] = [
("sub-delta", 0.0, 1.0),
("delta", 1.0, 4.0),
("theta", 4.0, 8.0),
("alpha", 8.0, 12.0),
("beta", 13.0, 30.0),
("gamma", 30.0, 100.0),
];
pub fn band_names() -> Vec<&'static str> {
EEG_BANDS.iter().map(|(n, _, _)| *n).collect()
}
pub fn clinical_band_names() -> Vec<&'static str> {
CLINICAL_BANDS.iter().map(|(n, _, _)| *n).collect()
}
pub fn per_band_fidelity(orig: &[f64], recon: &[f64], fs: f64) -> Vec<(String, f64, f64, f64)> {
let n = orig.len().min(recon.len());
if n == 0 || !fs.is_finite() || fs <= 0.0 {
return CLINICAL_BANDS
.iter()
.map(|(name, _, _)| {
let empty: [f64; 0] = [];
(
name.to_string(),
metrics::pearson_r(&empty, &empty),
metrics::prd(&empty, &empty),
metrics::snr_db(&empty, &empty),
)
})
.collect();
}
let o = &orig[..n];
let r = &recon[..n];
let (o_re, o_im) = rfft(o);
let (r_re, r_im) = rfft(r);
CLINICAL_BANDS
.iter()
.map(|&(name, lo, hi)| {
let mask = band_mask(n, fs, lo, hi);
let ob = irfft_masked(&o_re, &o_im, &mask, n);
let rb = irfft_masked(&r_re, &r_im, &mask, n);
(
name.to_string(),
metrics::pearson_r(&ob, &rb),
metrics::prd(&ob, &rb),
metrics::snr_db(&ob, &rb),
)
})
.collect()
}
fn band_mask(n: usize, fs: f64, lo: f64, hi: f64) -> Vec<bool> {
let half = n / 2 + 1;
let bin_hz = fs / n as f64;
(0..half)
.map(|k| {
let f = k as f64 * bin_hz;
f >= lo && f < hi
})
.collect()
}
fn rfft(x: &[f64]) -> (Vec<f64>, Vec<f64>) {
let n = x.len();
let half = n / 2 + 1;
let mut re = vec![0.0f64; half];
let mut im = vec![0.0f64; half];
for (k, (rk, ik)) in re.iter_mut().zip(im.iter_mut()).enumerate() {
let w = -2.0 * PI * k as f64 / n as f64;
let mut sr = 0.0f64;
let mut si = 0.0f64;
for (j, &xj) in x.iter().enumerate() {
let ang = w * j as f64;
sr += xj * ang.cos();
si += xj * ang.sin();
}
*rk = sr;
*ik = si;
}
(re, im)
}
fn irfft_masked(re: &[f64], im: &[f64], mask: &[bool], n: usize) -> Vec<f64> {
let half = re.len();
let even = n % 2 == 0;
let mut out = vec![0.0f64; n];
for (j, oj) in out.iter_mut().enumerate() {
let mut acc = 0.0f64;
for k in 0..half {
if !mask[k] {
continue;
}
let ang = 2.0 * PI * k as f64 * j as f64 / n as f64;
let unique = k == 0 || (even && k == half - 1);
let scale = if unique { 1.0 } else { 2.0 };
acc += scale * (re[k] * ang.cos() - im[k] * ang.sin());
}
*oj = acc / n as f64;
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn make_signal(n: usize, fs: f64) -> Vec<f64> {
(0..n)
.map(|i| {
let t = i as f64 / fs;
0.5 + 1.0 * (2.0 * PI * 2.0 * t).sin() + 0.8 * (2.0 * PI * 10.0 * t).sin() + 0.4 * (2.0 * PI * 40.0 * t).sin() })
.collect()
}
#[test]
fn identical_signals_perfect_per_band() {
let fs = 256.0;
let x = make_signal(256, fs);
let out = per_band_fidelity(&x, &x, fs);
assert_eq!(out.len(), CLINICAL_BANDS.len());
for (name, r, prd, snr) in out {
assert!((r - 1.0).abs() < 1e-9, "{name}: r={r} expected 1");
assert!(prd.abs() < 1e-9, "{name}: prd={prd} expected 0");
assert!((snr - 120.0).abs() < 1e-9, "{name}: snr={snr} expected 120");
}
}
#[test]
fn band_order_and_names_match_spec() {
let fs = 128.0;
let x = make_signal(128, fs);
let out = per_band_fidelity(&x, &x, fs);
let got: Vec<String> = out.into_iter().map(|(n, ..)| n).collect();
let want: Vec<String> = CLINICAL_BANDS
.iter()
.map(|(n, ..)| n.to_string())
.collect();
assert_eq!(got, want);
}
#[test]
fn perturbation_degrades_some_band() {
let fs = 256.0;
let n = 256;
let x = make_signal(n, fs);
let mut y = x.clone();
for (i, yi) in y.iter_mut().enumerate() {
let t = i as f64 / fs;
*yi += 0.2 * (2.0 * PI * 10.0 * t).sin();
}
let out = per_band_fidelity(&x, &y, fs);
let alpha = out.iter().find(|(n, ..)| n == "alpha").unwrap();
assert!(alpha.2 > 1e-6, "alpha PRD should be nonzero, got {}", alpha.2);
}
#[test]
fn masked_bands_sum_to_original() {
let fs = 200.0;
let n = 200;
let x = make_signal(n, fs);
let (re, im) = rfft(&x);
let mut sum = vec![0.0f64; n];
for &(_, lo, hi) in CLINICAL_BANDS.iter() {
let mask = band_mask(n, fs, lo, hi);
let band = irfft_masked(&re, &im, &mask, n);
for (s, b) in sum.iter_mut().zip(band.iter()) {
*s += b;
}
}
for (a, b) in x.iter().zip(sum.iter()) {
assert!((a - b).abs() < 1e-6, "reconstruction mismatch: {a} vs {b}");
}
}
#[test]
fn rfft_irfft_roundtrip_full_mask() {
let fs = 64.0;
let n = 64;
let x = make_signal(n, fs);
let (re, im) = rfft(&x);
let mask = vec![true; re.len()];
let back = irfft_masked(&re, &im, &mask, n);
for (a, b) in x.iter().zip(back.iter()) {
assert!((a - b).abs() < 1e-6, "roundtrip mismatch: {a} vs {b}");
}
}
#[test]
fn degenerate_inputs_yield_neutral_table() {
let empty: Vec<f64> = vec![];
let out = per_band_fidelity(&empty, &empty, 256.0);
assert_eq!(out.len(), CLINICAL_BANDS.len());
for (name, r, prd, snr) in &out {
assert!(r.is_finite() && prd.is_finite() && snr.is_finite(), "{name} NaN");
}
let x = make_signal(16, 256.0);
let out2 = per_band_fidelity(&x, &x, 0.0);
assert_eq!(out2.len(), CLINICAL_BANDS.len());
for (name, r, prd, snr) in &out2 {
assert!(r.is_finite() && prd.is_finite() && snr.is_finite(), "{name} NaN (fs=0)");
}
}
}