use super::geom::{elevation, median, Site};
use super::joint_pvt::{dop, SystemClock};
use super::system::System;
use serde::Serialize;
type GroupAcc = (Vec<f64>, Vec<f64>, Vec<f64>, usize, usize, usize);
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct GroupStats {
pub mean_in_view: f64,
pub median_pdop: Option<f64>,
pub median_hdop: Option<f64>,
pub median_vdop: Option<f64>,
pub availability: f64,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct PolarRow {
pub lat_deg: f64,
pub gnss: GroupStats,
pub leo: GroupStats,
pub fused: GroupStats,
}
pub fn latitude_sweep(
systems: &[System],
lats_deg: &[f64],
lons_deg: &[f64],
times_s: &[f64],
pdop_threshold: f64,
) -> Vec<PolarRow> {
let clocks: Vec<SystemClock> = systems.iter().map(|s| s.clock).collect();
let states: Vec<Vec<([f64; 3], usize)>> = times_s
.iter()
.map(|&t| {
systems
.iter()
.enumerate()
.flat_map(|(k, s)| s.orbits.iter().map(move |o| (o.state(t).0, k)))
.collect()
})
.collect();
lats_deg
.iter()
.map(|&lat| {
let mut acc: [GroupAcc; 3] = Default::default();
for &lon in lons_deg {
let site = Site {
lat_deg: lat,
lon_deg: lon,
height_m: 0.0,
};
let user = site.ecef();
let up = site.enu().2;
for sats in &states {
let vis: Vec<([f64; 3], usize)> = sats
.iter()
.filter(|(p, k)| elevation(user, up, *p) >= systems[*k].mask_rad)
.copied()
.collect();
for (g, a) in acc.iter_mut().enumerate() {
let sel: Vec<([f64; 3], usize)> = vis
.iter()
.filter(|(_, k)| match g {
0 => systems[*k].role == "gnss",
1 => systems[*k].role == "leo",
_ => true,
})
.copied()
.collect();
a.3 += sel.len();
a.5 += 1;
if let Some(d) = dop(user, &sel, &clocks) {
a.0.push(d.pdop);
a.1.push(d.hdop);
a.2.push(d.vdop);
if d.pdop <= pdop_threshold {
a.4 += 1;
}
}
}
}
}
let stats = |a: &GroupAcc| GroupStats {
mean_in_view: a.3 as f64 / a.5.max(1) as f64,
median_pdop: median(a.0.clone()),
median_hdop: median(a.1.clone()),
median_vdop: median(a.2.clone()),
availability: a.4 as f64 / a.5.max(1) as f64,
};
PolarRow {
lat_deg: lat,
gnss: stats(&acc[0]),
leo: stats(&acc[1]),
fused: stats(&acc[2]),
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::leo_fusion::system::SystemCfg;
fn sys(src: &str) -> System {
toml::from_str::<SystemCfg>(src).unwrap().build().unwrap()
}
#[test]
fn gps_vertical_geometry_weakens_at_the_pole_and_a_polar_leo_shell_restores_it() {
let gps = sys("name = \"GPS\"\npreset = \"gps-baseline\"\n");
let leo = sys(
"name = \"Polar\"\n[[shell]]\npattern = \"star\"\ntotal = 66\nplanes = 6\nphasing = 2\n\
altitude_km = 780.0\ninclination_deg = 86.4\n",
);
let rows = latitude_sweep(
&[gps, leo],
&[0.0, 89.0],
&[0.0, 90.0, 180.0, 270.0],
&[0.0, 1800.0, 3600.0, 5400.0],
6.0,
);
let eq = &rows[0];
let pole = &rows[1];
let (v_eq, v_pole) = (eq.gnss.median_vdop.unwrap(), pole.gnss.median_vdop.unwrap());
assert!(v_pole > v_eq, "GNSS VDOP equator {v_eq} pole {v_pole}");
assert!(pole.leo.mean_in_view > eq.leo.mean_in_view);
assert!(pole.fused.median_vdop.unwrap() < v_pole);
}
}