use super::{Observation, Orient};
use crate::spoof_monitors::SqmMonitor;
pub fn observations(text: &str) -> Vec<Observation> {
let mut lines = text
.lines()
.map(str::trim)
.filter(|l| !l.is_empty() && !l.starts_with('#'));
let Some(header) = lines.next() else {
return Vec::new();
};
let cols: Vec<&str> = header.split(',').map(str::trim).collect();
let idx = |name: &str| cols.iter().position(|c| *c == name);
let (Some(ei), Some(eq), Some(li), Some(lq)) =
(idx("early_i"), idx("early_q"), idx("late_i"), idx("late_q"))
else {
return Vec::new();
};
let monitor = SqmMonitor::new();
let mut out = Vec::new();
for line in lines {
let f: Vec<&str> = line.split(',').map(str::trim).collect();
let get = |i: usize| f.get(i).and_then(|s| s.parse::<f64>().ok());
let (Some(e_i), Some(e_q), Some(l_i), Some(l_q)) = (get(ei), get(eq), get(li), get(lq))
else {
continue;
};
let e_mag = e_i.hypot(e_q);
let l_mag = l_i.hypot(l_q);
let imbalance = monitor.el_metric(e_mag, l_mag).abs();
out.push(Observation::new("sqm", imbalance, Orient::Raw));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn symmetric_correlator_scores_zero_distorted_scores_higher() {
let csv = "\
epoch_s,prn,early_i,early_q,late_i,late_q
0.0,5,3.0,4.0,3.0,4.0
1.0,5,3.0,4.0,3.0,0.0
";
let obs = observations(csv);
assert_eq!(obs.len(), 2);
assert!(obs.iter().all(|o| o.detector == "sqm"));
assert!((obs[0].score - 0.0).abs() < 1e-12, "symmetric peak -> 0");
assert!((obs[1].score - 0.25).abs() < 1e-12, "expected |ELP| = 0.25");
assert!(obs[1].score > obs[0].score);
}
#[test]
fn score_is_the_unsigned_imbalance_for_either_skew_direction() {
let csv = "\
epoch_s,prn,early_i,early_q,late_i,late_q
0.0,5,3.0,0.0,5.0,0.0
";
let obs = observations(csv);
assert!((obs[0].score - 0.25).abs() < 1e-12);
}
#[test]
fn column_order_is_resolved_by_name() {
let csv = "\
late_q,early_i,prn,late_i,early_q,epoch_s
0.0,3.0,5,5.0,0.0,0.0
";
let obs = observations(csv);
assert_eq!(obs.len(), 1);
assert!((obs[0].score - 0.25).abs() < 1e-12);
}
#[test]
fn missing_required_columns_yields_nothing() {
let csv = "epoch_s,prn,early_i\n0.0,5,3.0\n";
assert!(observations(csv).is_empty());
}
#[test]
fn empty_input_yields_nothing() {
assert!(observations("").is_empty());
assert!(observations("# only a comment\n").is_empty());
}
}