use super::{Observation, Orient};
use crate::spoof_monitors::SqmMonitor;
#[derive(Clone, Debug, PartialEq)]
pub struct SatGridRow {
pub scenario: String,
pub source: String,
pub level: String,
pub prn: u32,
pub cn0: f64,
pub abs_e: f64,
pub abs_l: f64,
pub lock: f64,
pub prompt_i: f64,
pub prompt_q: f64,
}
impl SatGridRow {
pub fn is_genuine(&self) -> bool {
self.source.eq_ignore_ascii_case("genuine")
}
pub fn observations(&self) -> Vec<Observation> {
let monitor = SqmMonitor::new();
let sqm = monitor.el_metric(self.abs_e.abs(), self.abs_l.abs()).abs();
let denom = self.prompt_i.abs() + self.prompt_q.abs();
let qratio = if denom > 0.0 {
self.prompt_q.abs() / denom
} else {
0.0
};
vec![
Observation::new("cn0", self.cn0, Orient::Negate),
Observation::new("sqm", sqm, Orient::Raw),
Observation::new("lock", self.lock, Orient::Negate),
Observation::new("qratio", qratio, Orient::Raw),
]
}
}
pub fn parse(text: &str) -> Vec<SatGridRow> {
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 i_scn = idx("scenario"); let (
Some(i_src),
Some(i_lvl),
Some(i_prn),
Some(i_cn0),
Some(i_e),
Some(i_l),
Some(i_lock),
Some(i_pi),
Some(i_pq),
) = (
idx("source"),
idx("level"),
idx("prn"),
idx("cn0"),
idx("abs_e"),
idx("abs_l"),
idx("lock"),
idx("prompt_i"),
idx("prompt_q"),
)
else {
return Vec::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).copied();
let num = |i: usize| f.get(i).and_then(|s| s.parse::<f64>().ok());
let (Some(source), Some(level)) = (get(i_src), get(i_lvl)) else {
continue;
};
let prn = f
.get(i_prn)
.and_then(|s| s.parse::<u32>().ok())
.unwrap_or(0);
let (Some(cn0), Some(abs_e), Some(abs_l), Some(lock), Some(prompt_i), Some(prompt_q)) = (
num(i_cn0),
num(i_e),
num(i_l),
num(i_lock),
num(i_pi),
num(i_pq),
) else {
continue;
};
let scenario = i_scn
.and_then(|i| f.get(i))
.map(|s| s.to_string())
.unwrap_or_default();
out.push(SatGridRow {
scenario,
source: source.to_string(),
level: level.to_string(),
prn,
cn0,
abs_e,
abs_l,
lock,
prompt_i,
prompt_q,
});
}
out
}
#[cfg(test)]
mod tests {
use super::*;
const SAMPLE: &str = "\
source,level,prn,cn0,abs_e,abs_l,lock,prompt_i,prompt_q
genuine,na,2,45.0,1000,1000,0.95,1200,5
counterfeit,40,5,29.0,1000,500,0.30,800,800
";
#[test]
fn parses_rows_and_flags_genuine() {
let rows = parse(SAMPLE);
assert_eq!(rows.len(), 2);
assert!(rows[0].is_genuine());
assert!(!rows[1].is_genuine());
assert_eq!(rows[1].level, "40");
assert_eq!(rows[1].prn, 5);
}
#[test]
fn emits_four_oriented_detectors() {
let rows = parse(SAMPLE);
let gen = rows[0].observations();
let cf = rows[1].observations();
assert_eq!(gen.len(), 4);
let det = |obs: &[Observation], name: &str| {
obs.iter().find(|o| o.detector == name).unwrap().score
};
assert_eq!(det(&gen, "cn0"), -45.0);
assert!(det(&cf, "cn0") > det(&gen, "cn0"));
assert!((det(&gen, "sqm") - 0.0).abs() < 1e-12);
assert!((det(&cf, "sqm") - (500.0 / 1500.0)).abs() < 1e-9);
assert!(det(&cf, "lock") > det(&gen, "lock"));
assert!(det(&cf, "qratio") > det(&gen, "qratio"));
assert!((det(&cf, "qratio") - 0.5).abs() < 1e-9);
}
#[test]
fn sqm_uses_correlator_magnitude_so_sign_does_not_matter() {
let signed = "source,level,prn,cn0,abs_e,abs_l,lock,prompt_i,prompt_q\n\
counterfeit,20,5,30,-1000,500,0.4,-100,50\n";
let r = &parse(signed)[0];
let sqm = r
.observations()
.iter()
.find(|o| o.detector == "sqm")
.unwrap()
.score;
assert!((sqm - (500.0 / 1500.0)).abs() < 1e-9);
}
#[test]
fn scenario_column_is_optional_and_parsed_when_present() {
assert_eq!(parse(SAMPLE)[0].scenario, "");
let with = "scenario,source,level,prn,cn0,abs_e,abs_l,lock,prompt_i,prompt_q\n\
Arlington_Nov_8_Round_2,counterfeit,35,5,29,1000,500,0.3,800,800\n";
let r = &parse(with)[0];
assert_eq!(r.scenario, "Arlington_Nov_8_Round_2");
assert_eq!(r.level, "35");
}
#[test]
fn missing_columns_or_header_yields_nothing() {
assert!(parse("").is_empty());
assert!(parse("source,level,prn\ngenuine,na,2\n").is_empty());
}
}