use super::{Observation, Orient};
const MIN_CN0_DBHZ: f64 = 10.0;
const MAX_CN0_DBHZ: f64 = 64.0;
#[derive(Clone, Debug, PartialEq)]
pub struct TimedObs {
pub time: String,
pub obs: Observation,
}
fn header(csv: &str) -> Option<(Vec<&str>, std::str::Lines<'_>)> {
let mut lines = csv.lines();
let head = lines.find(|l| !l.trim().is_empty())?;
Some((head.split(',').map(str::trim).collect(), lines))
}
fn col(cols: &[&str], name: &str) -> Option<usize> {
cols.iter().position(|c| *c == name)
}
pub fn mon_rf_observations(csv: &str, agc_orient: Orient) -> Vec<TimedObs> {
let Some((cols, rows)) = header(csv) else {
return Vec::new();
};
let Some(t_i) = col(&cols, "real_time") else {
return Vec::new();
};
let agc_cols: Vec<usize> = cols
.iter()
.enumerate()
.filter(|(_, c)| c.starts_with("agcCnt"))
.map(|(i, _)| i)
.collect();
let jam_cols: Vec<usize> = cols
.iter()
.enumerate()
.filter(|(_, c)| c.starts_with("jamInd"))
.map(|(i, _)| i)
.collect();
let mut out = Vec::new();
for line in rows {
let f: Vec<&str> = line.split(',').collect();
let Some(time) = f.get(t_i) else { continue };
let time = (*time).to_string();
for &i in &agc_cols {
if let Some(v) = f.get(i).and_then(|s| s.trim().parse::<f64>().ok()) {
out.push(TimedObs {
time: time.clone(),
obs: Observation::new("agc", v, agc_orient),
});
}
}
for &i in &jam_cols {
if let Some(v) = f.get(i).and_then(|s| s.trim().parse::<f64>().ok()) {
out.push(TimedObs {
time: time.clone(),
obs: Observation::new("jamind", v, Orient::Raw),
});
}
}
}
out
}
pub fn rinex_cn0_observations(csv: &str) -> Vec<TimedObs> {
let Some((cols, rows)) = header(csv) else {
return Vec::new();
};
let (Some(t_i), Some(sat_i)) = (col(&cols, "time"), col(&cols, "satellite")) else {
return Vec::new();
};
let bands = [("snr_L1", 1u8), ("snr_L2", 2u8)];
let band_cols: Vec<(usize, u8)> = bands
.iter()
.filter_map(|(name, b)| col(&cols, name).map(|i| (i, *b)))
.collect();
let mut out = Vec::new();
for line in rows {
let f: Vec<&str> = line.split(',').collect();
let (Some(time), Some(sat)) = (f.get(t_i), f.get(sat_i)) else {
continue;
};
let Some(sys) = sat.trim().chars().next() else {
continue;
};
for &(i, band) in &band_cols {
if let Some(v) = f.get(i).and_then(|s| s.trim().parse::<f64>().ok()) {
if (MIN_CN0_DBHZ..=MAX_CN0_DBHZ).contains(&v) {
out.push(TimedObs {
time: (*time).to_string(),
obs: Observation::new(format!("cn0_{sys}_L{band}"), v, Orient::Negate),
});
}
}
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mon_rf_extracts_agc_and_jamind_per_block_before_the_payload_column() {
let csv = "identity,real_time,agcCnt_01,agcCnt_02,jamInd_01,jamInd_02,payload\n\
MON-RF,2024-09-11 07:00:00,5616,4914,11,5,b'\\x00,\\x01'\n";
let obs = mon_rf_observations(csv, Orient::Negate);
let agc: Vec<_> = obs.iter().filter(|o| o.obs.detector == "agc").collect();
let jam: Vec<_> = obs.iter().filter(|o| o.obs.detector == "jamind").collect();
assert_eq!(agc.len(), 2);
assert_eq!(agc[0].obs.raw, 5616.0);
assert_eq!(agc[0].obs.score, -5616.0); assert_eq!(agc[0].time, "2024-09-11 07:00:00");
assert_eq!(jam.len(), 2);
assert_eq!(jam[0].obs.raw, 11.0);
assert_eq!(jam[0].obs.score, 11.0); }
#[test]
fn rinex_extracts_cn0_per_constellation_and_band_negated() {
let csv = "time,satellite,pseudorange_L1,carrier_phase_L1,doppler_L1,snr_L1,pseudorange_L2,carrier_phase_L2,doppler_L2,snr_L2\n\
2024-09-11 07:00:00,G17,123.0,456.0,-31.0,42.0,124.0,457.0,-30.0,46.0\n\
2024-09-11 07:00:00,E03,125.0,,,40.0,,,,\n";
let obs = rinex_cn0_observations(csv);
assert_eq!(obs.len(), 3);
let g1 = obs.iter().find(|o| o.obs.detector == "cn0_G_L1").unwrap();
assert_eq!(g1.obs.raw, 42.0);
assert_eq!(g1.obs.score, -42.0);
assert!(obs
.iter()
.any(|o| o.obs.detector == "cn0_G_L2" && o.obs.raw == 46.0));
assert!(obs
.iter()
.any(|o| o.obs.detector == "cn0_E_L1" && o.obs.raw == 40.0));
}
#[test]
fn rinex_drops_column_shift_artifacts_above_the_valid_dbhz_range() {
let csv = "time,satellite,pseudorange_L1,carrier_phase_L1,doppler_L1,snr_L1,pseudorange_L2,carrier_phase_L2,doppler_L2,snr_L2\n\
2024-09-11 07:00:01,G08,222.0,333.0,1.0,22272672.58,,,,\n";
assert!(rinex_cn0_observations(csv).is_empty());
}
#[test]
fn empty_or_headerless_input_yields_nothing() {
assert!(mon_rf_observations("", Orient::Negate).is_empty());
assert!(rinex_cn0_observations("").is_empty());
assert!(mon_rf_observations("a,b,c\n1,2,3\n", Orient::Negate).is_empty());
}
}