use super::iono::Ionosphere;
use super::land_mobile::LandMobile;
use super::multipath::GroundReflector;
use super::scint::Scintillation;
use super::tropo::Troposphere;
use super::{geometric_path, ChannelEffect, LineOfSight};
use crate::frames::ecef_to_geodetic;
use crate::iq::scene::{SatView, SceneChannel};
use crate::iq::ChannelSnapshot;
#[derive(Clone, Debug, Default)]
pub(crate) struct SceneChannelAdapter {
carrier_hz: f64,
iono: Option<Ionosphere>,
tropo: Option<Troposphere>,
scint: Option<Scintillation>,
multipath: Vec<GroundReflector>,
land_mobile: Option<LandMobile>,
nlos: bool,
}
impl SceneChannelAdapter {
pub(crate) fn new(carrier_hz: f64) -> Self {
Self {
carrier_hz,
..Self::default()
}
}
pub(crate) fn with_iono(mut self, iono: Ionosphere) -> Self {
self.iono = Some(iono);
self
}
pub(crate) fn with_tropo(mut self, tropo: Troposphere) -> Self {
self.tropo = Some(tropo);
self
}
pub(crate) fn with_scint(mut self, scint: Scintillation) -> Self {
self.scint = Some(scint);
self
}
pub(crate) fn with_multipath(mut self, r: GroundReflector) -> Self {
self.multipath.push(r);
self
}
pub(crate) fn with_land_mobile(mut self, lm: LandMobile) -> Self {
self.land_mobile = Some(lm);
self
}
pub(crate) fn with_nlos(mut self, nlos: bool) -> Self {
self.nlos = nlos;
self
}
}
fn line_of_sight(view: &SatView) -> LineOfSight {
let g = ecef_to_geodetic(view.receiver_ecef);
LineOfSight {
el_rad: view.elevation_deg.to_radians(),
az_rad: view.azimuth_deg.to_radians(),
range_m: view.pseudorange_m,
range_rate_mps: view.pseudorange_rate_mps,
lat_rad: g.lat_rad,
lon_rad: g.lon_rad,
height_m: g.alt_m,
}
}
impl SceneChannel for SceneChannelAdapter {
fn snapshot(&mut self, sat_id: u32, t_s: f64, view: &SatView) -> ChannelSnapshot {
let geom = line_of_sight(view);
let carrier = self.carrier_hz;
let mut snap = ChannelSnapshot {
t_s,
paths: vec![geometric_path(&geom, carrier)],
};
if let Some(e) = self.iono.as_mut() {
e.apply(sat_id, t_s, &geom, carrier, &mut snap);
}
if let Some(e) = self.tropo.as_mut() {
e.apply(sat_id, t_s, &geom, carrier, &mut snap);
}
if let Some(e) = self.scint.as_mut() {
e.apply(sat_id, t_s, &geom, carrier, &mut snap);
}
for e in &mut self.multipath {
e.apply(sat_id, t_s, &geom, carrier, &mut snap);
}
if let Some(e) = self.land_mobile.as_mut() {
e.apply(sat_id, t_s, &geom, carrier, &mut snap);
}
if self.nlos {
if let Some(direct) = snap.paths.first_mut() {
direct.amplitude = 0.0;
}
}
let base = geometric_path(&geom, carrier);
for p in &mut snap.paths {
p.group_delay_s -= base.group_delay_s;
p.carrier_phase_rad -= base.carrier_phase_rad;
}
snap
}
}
#[cfg(test)]
mod tests {
use super::super::iono::IonoSource;
use super::super::multipath::{excess_path_m, Ground};
use super::*;
use crate::frames::{geodetic_to_ecef, Geodetic};
use crate::iq::C_M_PER_S;
use std::f64::consts::PI;
const L1_HZ: f64 = 1_575_420_000.0;
fn view(el_deg: f64) -> SatView {
let rx = geodetic_to_ecef(Geodetic {
lat_rad: 40f64.to_radians(),
lon_rad: (-105f64).to_radians(),
alt_m: 1600.0,
});
SatView {
pseudorange_m: 21_000_000.0,
pseudorange_rate_mps: -300.0,
elevation_deg: el_deg,
azimuth_deg: 40.0,
receiver_ecef: rx,
}
}
#[test]
fn empty_adapter_yields_the_unperturbed_direct_path() {
let mut a = SceneChannelAdapter::new(L1_HZ);
let s = a.snapshot(3, 0.5, &view(30.0));
assert_eq!(s.paths.len(), 1);
let p = s.paths[0];
assert!(p.group_delay_s.abs() < 1e-15);
assert!(p.carrier_phase_rad.abs() < 1e-6);
assert_eq!(p.amplitude, 1.0);
assert_eq!(s.t_s, 0.5);
}
#[test]
fn ionosphere_excess_matches_the_closed_form() {
let stec = 40.0;
let mut a = SceneChannelAdapter::new(L1_HZ)
.with_iono(Ionosphere::new(IonoSource::SlantTec { stec_tecu: stec }));
let s = a.snapshot(1, 0.0, &view(25.0));
let i_m = 40.3e16 * stec / (L1_HZ * L1_HZ);
let code_m = s.paths[0].group_delay_s * C_M_PER_S;
let carrier_m = -s.paths[0].carrier_phase_rad * (C_M_PER_S / L1_HZ) / (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 multipath_adds_a_reflected_path_with_the_fresnel_excess_delay() {
let h = 2.5;
let mut a =
SceneChannelAdapter::new(L1_HZ).with_multipath(GroundReflector::new(h, Ground::DRY));
let v = view(35.0);
let s = a.snapshot(7, 0.0, &v);
assert_eq!(s.paths.len(), 2);
assert!(s.paths[0].group_delay_s.abs() < 1e-15);
let want = excess_path_m(h, v.elevation_deg.to_radians()) / C_M_PER_S;
assert!((s.paths[1].group_delay_s - want).abs() < 1e-15);
}
#[test]
fn nlos_zeroes_the_direct_path_but_keeps_the_reflection() {
let mut a = SceneChannelAdapter::new(L1_HZ)
.with_multipath(GroundReflector::new(2.0, Ground::WET))
.with_nlos(true);
let s = a.snapshot(1, 0.0, &view(30.0));
assert!(s.is_nlos());
assert_eq!(s.paths[0].amplitude, 0.0);
assert!(s.paths[1].amplitude > 0.0);
}
#[test]
fn scene_scintillation_sample_envelope_has_the_target_s4() {
use super::super::scint::{ScintParams, Scintillation};
use crate::iq::scene::{GpsL1Ca, RangeProfile, Scene, SceneConfig, SceneSatellite};
use crate::iq::scene::{NavData, NullTruth};
use crate::iq::{SampleSpec, VecSink};
let s4 = 0.5;
let tau0 = 0.005;
let spec = SampleSpec {
fs_hz: 1000.0,
center_hz: L1_HZ,
if_hz: 0.0,
};
let seed = 7u64;
let mut cfg = SceneConfig::new(spec, 40.0);
cfg.noise.enabled = false;
cfg.seed = seed;
let profile = RangeProfile {
range_m: 2.1e7,
range_rate_mps: 0.0,
range_accel_mps2: 0.0,
elevation_deg: 45.0,
azimuth_deg: 0.0,
};
let mut scene = Scene::new(cfg).unwrap();
let code = GpsL1Ca::new(1).unwrap();
scene.add_satellite(SceneSatellite {
id: 1,
code: Box::new(code),
geometry: crate::iq::scene::SatGeometry::Profile(profile),
cn0_dbhz: Some(45.0),
nav: NavData::None,
});
let adapter = SceneChannelAdapter::new(L1_HZ)
.with_scint(Scintillation::new(ScintParams::new(s4, tau0), seed));
scene.set_channel(Box::new(adapter));
let mut sink = VecSink::default();
scene.generate(&mut sink, &mut NullTruth).unwrap();
let intensity: Vec<f64> = sink
.samples
.iter()
.map(|z| z.re * z.re + z.im * z.im)
.collect();
let n = intensity.len() as f64;
let m1 = intensity.iter().sum::<f64>() / n;
let m2 = intensity.iter().map(|i| i * i).sum::<f64>() / n;
let got_s4 = (m2 - m1 * m1).sqrt() / m1;
assert!((got_s4 - s4).abs() < 0.12, "measured S4 {got_s4} vs {s4}");
let mut cfg2 = SceneConfig::new(spec, 2.0);
cfg2.noise.enabled = false;
let mut scene2 = Scene::new(cfg2).unwrap();
scene2.add_satellite(
SceneSatellite::gps_l1ca_profile(1, profile, Some(45.0), NavData::None).unwrap(),
);
let mut sink2 = VecSink::default();
scene2.generate(&mut sink2, &mut NullTruth).unwrap();
let i2: Vec<f64> = sink2
.samples
.iter()
.map(|z| z.re * z.re + z.im * z.im)
.collect();
let n2 = i2.len() as f64;
let a1 = i2.iter().sum::<f64>() / n2;
let a2 = i2.iter().map(|i| i * i).sum::<f64>() / n2;
let flat_s4 = (a2 - a1 * a1).sqrt() / a1;
assert!(flat_s4 < 1e-5, "no-channel S4 {flat_s4}");
}
}