use super::{ChannelEffect, LineOfSight};
use crate::gnss_sim::{klobuchar_delay_m, KlobucharCoeffs, L1_HZ};
use crate::iq::{ChannelSnapshot, C_M_PER_S};
use std::f64::consts::PI;
#[derive(Clone, Copy, Debug)]
pub enum IonoSource {
Klobuchar {
coeffs: KlobucharCoeffs,
gps_sod_at_t0: f64,
},
VerticalTec {
vtec_tecu: f64,
shell_height_km: f64,
},
SlantTec {
stec_tecu: f64,
},
}
#[derive(Clone, Copy, Debug)]
pub struct Ionosphere {
pub source: IonoSource,
}
impl Ionosphere {
pub fn new(source: IonoSource) -> Self {
Self { source }
}
pub fn delay_m(&self, t_s: f64, geom: &LineOfSight, carrier_hz: f64) -> f64 {
match self.source {
IonoSource::Klobuchar {
coeffs,
gps_sod_at_t0,
} => {
let i_l1 = klobuchar_delay_m(
&coeffs,
geom.lat_rad,
geom.lon_rad,
geom.el_rad,
geom.az_rad,
(gps_sod_at_t0 + t_s).rem_euclid(86_400.0),
);
let r = L1_HZ / carrier_hz;
i_l1 * r * r
}
IonoSource::VerticalTec {
vtec_tecu,
shell_height_km,
} => {
let zenith_deg = 90.0 - geom.el_rad.to_degrees();
let stec = crate::ionex::slant_tec(vtec_tecu, zenith_deg, shell_height_km);
crate::timetransfer_adv::iono_delay_m(stec, carrier_hz)
}
IonoSource::SlantTec { stec_tecu } => {
crate::timetransfer_adv::iono_delay_m(stec_tecu, carrier_hz)
}
}
}
}
impl ChannelEffect for Ionosphere {
fn apply(
&mut self,
_sat: u32,
t_s: f64,
geom: &LineOfSight,
carrier_hz: f64,
snap: &mut ChannelSnapshot,
) {
let i_s = self.delay_m(t_s, geom, carrier_hz) / C_M_PER_S;
let dphi = 2.0 * PI * carrier_hz * i_s;
for p in &mut snap.paths {
p.group_delay_s += i_s;
p.carrier_phase_rad += dphi;
}
}
}
#[cfg(test)]
mod tests {
use super::super::{test_geom, ChannelModel, Composite};
use super::*;
const L5_HZ: f64 = 1_176_450_000.0;
#[test]
fn one_tecu_is_0_162_m_at_l1_misra_enge_value() {
let iono = Ionosphere::new(IonoSource::SlantTec { stec_tecu: 1.0 });
let d = iono.delay_m(0.0, &test_geom(45.0), L1_HZ);
assert!((d - 0.1624).abs() < 0.0005, "{d}");
}
#[test]
fn code_and_carrier_diverge_by_equal_and_opposite_amounts() {
let g = test_geom(20.0);
let stec = 50.0;
let mut base = Composite::new(L1_HZ);
let mut ch =
Composite::new(L1_HZ).with(Ionosphere::new(IonoSource::SlantTec { stec_tecu: stec }));
let p0 = base.snapshot(1, 0.0, &g).paths[0];
let p1 = ch.snapshot(1, 0.0, &g).paths[0];
let i_m = 40.3e16 * stec / (L1_HZ * L1_HZ);
let code_m = (p1.group_delay_s - p0.group_delay_s) * C_M_PER_S;
let lambda = C_M_PER_S / L1_HZ;
let carrier_m = -(p1.carrier_phase_rad - p0.carrier_phase_rad) * lambda / (2.0 * PI);
assert!((code_m - i_m).abs() < 1e-6, "{code_m} vs {i_m}");
assert!((carrier_m + i_m).abs() < 1e-6, "{carrier_m} vs {}", -i_m);
}
#[test]
fn klobuchar_source_reuses_the_engine_model_and_scales_inverse_f_squared() {
let g = test_geom(25.0);
let coeffs = KlobucharCoeffs::default();
let iono = Ionosphere::new(IonoSource::Klobuchar {
coeffs,
gps_sod_at_t0: 50_000.0,
});
let l1 = iono.delay_m(400.0, &g, L1_HZ);
let engine = klobuchar_delay_m(&coeffs, g.lat_rad, g.lon_rad, g.el_rad, g.az_rad, 50_400.0);
assert_eq!(l1, engine);
let l5 = iono.delay_m(400.0, &g, L5_HZ);
let want = (L1_HZ / L5_HZ).powi(2);
assert!((l5 / l1 - want).abs() < 1e-12);
}
#[test]
fn vertical_tec_maps_to_slant_with_the_thin_shell_factor() {
let iono = Ionosphere::new(IonoSource::VerticalTec {
vtec_tecu: 20.0,
shell_height_km: 350.0,
});
let zen = iono.delay_m(0.0, &test_geom(90.0), L1_HZ);
assert!((zen - 40.3e16 * 20.0 / (L1_HZ * L1_HZ)).abs() < 1e-9);
let el = 10f64.to_radians();
let re = crate::ionex::IONO_SHELL_RE_KM;
let sin_zp = re / (re + 350.0) * el.cos();
let m = 1.0 / (1.0 - sin_zp * sin_zp).sqrt();
let low = iono.delay_m(0.0, &test_geom(10.0), L1_HZ);
assert!((low / zen - m).abs() < 1e-9);
}
}