use super::elements::{sat_state, sub, LeoNavMessage, SysTime, C_LIGHT};
use super::truth::{TruthClock, TruthOrbit};
use crate::portable_math::PortableFloat;
use serde::{Deserialize, Serialize};
pub const EARTH_RADIUS_M: f64 = 6_378_137.0;
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct SisreWeights {
pub w_r: f64,
pub w_ac2: f64,
pub max_nadir_deg: f64,
}
pub fn sisre_weights(r_m: f64, mask_deg: f64) -> SisreWeights {
let re = EARTH_RADIUS_M;
let el = mask_deg.to_radians();
let lam_max = (re / r_m * el.pcos()).pacos() - el;
let n = 20_000usize; let h = lam_max / n as f64;
let (mut s_w, mut s_c2, mut s_s2) = (0.0, 0.0, 0.0);
for k in 0..=n {
let lam = k as f64 * h;
let coef = if k == 0 || k == n {
1.0
} else if k % 2 == 1 {
4.0
} else {
2.0
};
let rho = (re * re + r_m * r_m - 2.0 * re * r_m * lam.pcos()).sqrt();
let sin_eta = if rho > 0.0 {
re * lam.psin() / rho
} else {
0.0
};
let wgt = lam.psin() * coef;
s_w += wgt;
s_c2 += wgt * (1.0 - sin_eta * sin_eta);
s_s2 += wgt * sin_eta * sin_eta;
}
let max_nadir = (re * el.pcos() / r_m).pasin();
SisreWeights {
w_r: (s_c2 / s_w).sqrt(),
w_ac2: 0.5 * s_s2 / s_w,
max_nadir_deg: max_nadir.to_degrees(),
}
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct ErrorStats {
pub n: usize,
pub sisre_orb_rms_m: f64,
pub sisre_orb_max_m: f64,
pub sisre_rms_m: f64,
pub sisre_max_m: f64,
pub radial_rms_m: f64,
pub along_rms_m: f64,
pub cross_rms_m: f64,
pub clock_rms_m: f64,
pub pos3d_max_m: f64,
}
#[derive(Clone, Debug, Default)]
pub struct StatsAcc {
n: usize,
so2: f64,
somax: f64,
s2: f64,
smax: f64,
r2: f64,
a2: f64,
c2: f64,
k2: f64,
p3max: f64,
}
impl StatsAcc {
pub fn push(&mut self, w: &SisreWeights, rac: [f64; 3], clock_m: f64) {
let (a, c, r) = (rac[0], rac[1], rac[2]);
let orb = (w.w_r * w.w_r * r * r + w.w_ac2 * (a * a + c * c)).sqrt();
let tot = ((w.w_r * r - clock_m).ppowi(2) + w.w_ac2 * (a * a + c * c)).sqrt();
self.n += 1;
self.so2 += orb * orb;
self.somax = self.somax.max(orb);
self.s2 += tot * tot;
self.smax = self.smax.max(tot);
self.r2 += r * r;
self.a2 += a * a;
self.c2 += c * c;
self.k2 += clock_m * clock_m;
self.p3max = self.p3max.max((a * a + c * c + r * r).sqrt());
}
pub fn merge(&mut self, o: &StatsAcc) {
self.n += o.n;
self.so2 += o.so2;
self.somax = self.somax.max(o.somax);
self.s2 += o.s2;
self.smax = self.smax.max(o.smax);
self.r2 += o.r2;
self.a2 += o.a2;
self.c2 += o.c2;
self.k2 += o.k2;
self.p3max = self.p3max.max(o.p3max);
}
pub fn finish(&self) -> ErrorStats {
let n = self.n.max(1) as f64;
ErrorStats {
n: self.n,
sisre_orb_rms_m: (self.so2 / n).sqrt(),
sisre_orb_max_m: self.somax,
sisre_rms_m: (self.s2 / n).sqrt(),
sisre_max_m: self.smax,
radial_rms_m: (self.r2 / n).sqrt(),
along_rms_m: (self.a2 / n).sqrt(),
cross_rms_m: (self.c2 / n).sqrt(),
clock_rms_m: (self.k2 / n).sqrt(),
pos3d_max_m: self.p3max,
}
}
}
pub fn message_error(
truth: &TruthOrbit,
clock: Option<&TruthClock>,
msg: &LeoNavMessage,
t: &SysTime,
) -> ([f64; 3], f64) {
let dt = truth.dt_of(t);
let (pos, _, frame) = truth.state_ecef(dt);
let s = sat_state(msg, t);
let rac = frame.project(sub(s.pos, pos));
let clk = match clock {
Some(c) => (s.clock_s - c.apparent_s(truth, dt)) * C_LIGHT,
None => 0.0,
};
(rac, clk)
}
#[allow(clippy::too_many_arguments)]
pub fn accumulate(
acc: &mut StatsAcc,
w: &SisreWeights,
truth: &TruthOrbit,
clock: Option<&TruthClock>,
msg: &LeoNavMessage,
from: &SysTime,
len: f64,
step_s: f64,
) {
let n = (len / step_s).round().max(1.0) as usize;
for k in 0..n {
let t = from.plus(len * k as f64 / n as f64);
let (rac, clk) = message_error(truth, clock, msg, &t);
acc.push(w, rac, clk);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn weights_reproduce_the_published_meo_and_geo_table() {
let cases = [
("GPS", 26_560e3, 0.98, 49.0),
("GLONASS", 25_510e3, 0.98, 45.0),
("Galileo", 29_600e3, 0.98, 61.0),
("BeiDou MEO", 27_906e3, 0.98, 54.0),
("BeiDou GEO", 42_164e3, 0.99, 126.0),
];
for (name, r, wr, inv) in cases {
let w = sisre_weights(r, 0.0);
assert!(
(w.w_r - wr).abs() < 0.005,
"{name}: w_R {:.4} vs published {wr}",
w.w_r
);
let inv_calc = 1.0 / w.w_ac2;
assert!(
(inv_calc - inv).abs() < 0.5,
"{name}: 1/w_AC^2 {inv_calc:.2} vs published {inv}"
);
}
}
#[test]
fn leo_weights_shift_toward_the_transverse_axes() {
let w500 = sisre_weights(EARTH_RADIUS_M + 510e3, 0.0);
let w1300 = sisre_weights(EARTH_RADIUS_M + 1336e3, 0.0);
assert!(w500.w_r < 0.5 && w500.w_r > 0.4, "{w500:?}");
assert!(w1300.w_r > w500.w_r);
assert!(w500.w_ac2 > w1300.w_ac2);
let w10 = sisre_weights(EARTH_RADIUS_M + 510e3, 10.0);
assert!(w10.w_r > w500.w_r);
assert!((w500.max_nadir_deg - 67.8).abs() < 0.1);
}
}