use super::acquire::codes_from_args;
use super::frontend::through_frontend;
use super::track::{acquire_inits, frac};
use super::{raw_sidecar, Args, Fail};
use crate::iq::io::inventory::open_recording;
use crate::iq::signals::SignalCode;
use crate::iq::track::{replay, CarrierLoop, EpochOutput, LoopConfig, ReplayResult};
use crate::iq::SpreadingCode;
use std::path::Path;
fn axis(a: &Args, key: &str, default: f64) -> Result<Vec<f64>, Fail> {
let v: Vec<f64> = a.list(key).map_err(Fail::Usage)?;
Ok(if v.is_empty() { vec![default] } else { v })
}
pub(crate) fn designs(a: &Args) -> Result<Vec<LoopConfig>, Fail> {
let base = LoopConfig::default();
let pll_bws = axis(a, "--pll-bw", 15.0)?;
let dll_bws = axis(a, "--dll-bw", base.dll_bn_hz)?;
let spacings = axis(a, "--spacing", base.spacing_chips)?;
let coherents: Vec<usize> = {
let v: Vec<usize> = a.list("--coherent").map_err(Fail::Usage)?;
if v.is_empty() {
vec![base.coherent_periods]
} else {
v
}
};
let mut out = Vec::new();
for &pll in &pll_bws {
for &dll in &dll_bws {
for &sp in &spacings {
for &coh in &coherents {
out.push(LoopConfig {
label: format!("pll{pll}_dll{dll}_sp{sp}_coh{coh}"),
coherent_periods: coh.max(1),
spacing_chips: sp,
dll_bn_hz: dll,
carrier: CarrierLoop::FllAssistedPll {
pll_order: 2,
pll_bn_hz: pll,
fll_order: 1,
fll_bn_hz: 10.0,
},
..base.clone()
});
}
}
}
}
Ok(out)
}
pub(crate) struct Metrics {
design: String,
code: String,
epochs: usize,
phase_jitter_deg: f64,
code_jitter_chips: f64,
phase_lock_frac: f64,
code_lock_frac: f64,
mean_cn0_dbhz: f64,
}
fn std(xs: &[f64]) -> f64 {
let n = xs.len();
if n < 2 {
return 0.0;
}
let mean = xs.iter().sum::<f64>() / n as f64;
let var = xs.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / (n - 1) as f64;
var.sqrt()
}
fn metrics(design: &str, code: &str, epochs: &[EpochOutput]) -> Metrics {
let tail = &epochs[epochs.len() / 2..];
let pll: Vec<f64> = tail.iter().map(|e| e.disc.pll_rad).collect();
let dll: Vec<f64> = tail.iter().map(|e| e.disc.dll_chips).collect();
let cn0: Vec<f64> = tail.iter().filter_map(|e| e.cn0_nwpr_dbhz).collect();
Metrics {
design: design.to_string(),
code: code.to_string(),
epochs: epochs.len(),
phase_jitter_deg: std(&pll).to_degrees(),
code_jitter_chips: std(&dll),
phase_lock_frac: frac(epochs, |e| e.phase_lock),
code_lock_frac: frac(epochs, |e| e.code_lock),
mean_cn0_dbhz: if cn0.is_empty() {
f64::NAN
} else {
cn0.iter().sum::<f64>() / cn0.len() as f64
},
}
}
fn collect(codes: &[SignalCode], results: &[ReplayResult]) -> Vec<Metrics> {
let mut rows = Vec::new();
for r in results {
for (code, epochs) in codes.iter().zip(&r.channels) {
rows.push(metrics(&r.config.label, &code.name(), epochs));
}
}
rows
}
pub(crate) fn run(args: &[String]) -> Result<String, Fail> {
let a = Args::parse(args, super::frontend::FRONTEND_SWITCHES).map_err(Fail::Usage)?;
a.need_pos(1, "sweep")?;
let fe = super::frontend::FrontendParams::from_args(&a)?;
let path = Path::new(&a.pos[0]);
let codes = codes_from_args(&a)?;
let opened = open_recording(path, raw_sidecar(&a)?)?;
let spec = opened.source.spec();
let mut acq_src = through_frontend(&fe, opened.source)?;
let inits = acquire_inits(&a, &spec, &codes, acq_src.as_mut())?;
let designs = designs(&a)?;
let max_samples = a
.num::<f64>("--max-seconds")
.map_err(Fail::Usage)?
.map(|s| (s * spec.fs_hz).round() as u64);
let mut track_src = through_frontend(&fe, open_recording(path, raw_sidecar(&a)?)?.source)?;
let results = replay(track_src.as_mut(), &inits, &designs, max_samples)?;
let rows = collect(&codes, &results);
write_outputs(&a, &rows)?;
Ok(table(&rows))
}
fn write_outputs(a: &Args, rows: &[Metrics]) -> Result<(), Fail> {
if let Some(p) = a.get("--json") {
std::fs::write(p, to_json(rows)).map_err(|e| Fail::Run(format!("{p}: {e}")))?;
}
if let Some(p) = a.get("--csv") {
std::fs::write(p, to_csv(rows)).map_err(|e| Fail::Run(format!("{p}: {e}")))?;
}
Ok(())
}
fn to_csv(rows: &[Metrics]) -> String {
let mut s = String::from(
"design,code,epochs,phase_jitter_deg,code_jitter_chips,phase_lock_frac,code_lock_frac,mean_cn0_dbhz\n",
);
for m in rows {
s.push_str(&format!(
"{},{},{},{},{},{},{},{}\n",
m.design,
m.code,
m.epochs,
m.phase_jitter_deg,
m.code_jitter_chips,
m.phase_lock_frac,
m.code_lock_frac,
m.mean_cn0_dbhz
));
}
s
}
fn to_json(rows: &[Metrics]) -> String {
let v: Vec<serde_json::Value> = rows
.iter()
.map(|m| {
serde_json::json!({
"design": m.design,
"code": m.code,
"epochs": m.epochs,
"phase_jitter_deg": m.phase_jitter_deg,
"code_jitter_chips": m.code_jitter_chips,
"phase_lock_frac": m.phase_lock_frac,
"code_lock_frac": m.code_lock_frac,
"mean_cn0_dbhz": m.mean_cn0_dbhz,
})
})
.collect();
serde_json::to_string_pretty(&serde_json::json!({ "designs": v })).unwrap_or_default()
}
fn table(rows: &[Metrics]) -> String {
let mut out = format!(
"loop-design sweep: {} (design, PRN) result(s)\n",
rows.len()
);
out.push_str(
"design\tcode\tepochs\tphase_jitter_deg\tcode_jitter_chips\tphase_lock\tcode_lock\tmean_cn0\n",
);
for m in rows {
out.push_str(&format!(
"{}\t{}\t{}\t{:.2}\t{:.4}\t{:.3}\t{:.3}\t{:.1}\n",
m.design,
m.code,
m.epochs,
m.phase_jitter_deg,
m.code_jitter_chips,
m.phase_lock_frac,
m.code_lock_frac,
m.mean_cn0_dbhz
));
}
out.pop();
out
}