use super::{ChannelSnapshot, PathState, C_M_PER_S};
use std::f64::consts::PI;
pub mod iono;
pub mod land_mobile;
pub mod multipath;
pub mod scint;
pub mod tropo;
mod scene_bridge;
pub(crate) use scene_bridge::SceneChannelAdapter;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct LineOfSight {
pub el_rad: f64,
pub az_rad: f64,
pub range_m: f64,
pub range_rate_mps: f64,
pub lat_rad: f64,
pub lon_rad: f64,
pub height_m: f64,
}
pub trait ChannelModel {
fn snapshot(&mut self, sat: u32, t_s: f64, geom: &LineOfSight) -> ChannelSnapshot;
}
pub trait ChannelEffect {
fn apply(
&mut self,
sat: u32,
t_s: f64,
geom: &LineOfSight,
carrier_hz: f64,
snap: &mut ChannelSnapshot,
);
}
pub struct Composite {
carrier_hz: f64,
effects: Vec<Box<dyn ChannelEffect>>,
}
impl Composite {
pub fn new(carrier_hz: f64) -> Self {
Self {
carrier_hz,
effects: Vec::new(),
}
}
pub fn with<E: ChannelEffect + 'static>(mut self, effect: E) -> Self {
self.push(effect);
self
}
pub fn push<E: ChannelEffect + 'static>(&mut self, effect: E) {
self.effects.push(Box::new(effect));
}
pub fn carrier_hz(&self) -> f64 {
self.carrier_hz
}
pub fn len(&self) -> usize {
self.effects.len()
}
pub fn is_empty(&self) -> bool {
self.effects.is_empty()
}
}
pub fn geometric_path(geom: &LineOfSight, carrier_hz: f64) -> PathState {
let tau = geom.range_m / C_M_PER_S;
PathState {
group_delay_s: tau,
carrier_phase_rad: -2.0 * PI * carrier_hz * tau,
amplitude: 1.0,
extra_doppler_hz: 0.0,
}
}
impl ChannelModel for Composite {
fn snapshot(&mut self, sat: u32, t_s: f64, geom: &LineOfSight) -> ChannelSnapshot {
let mut snap = ChannelSnapshot {
t_s,
paths: vec![geometric_path(geom, self.carrier_hz)],
};
for e in &mut self.effects {
e.apply(sat, t_s, geom, self.carrier_hz, &mut snap);
}
snap
}
}
pub(crate) fn add_delay_m(snap: &mut ChannelSnapshot, delay_m: f64, carrier_hz: f64) {
let dt = delay_m / C_M_PER_S;
let dphi = -2.0 * PI * carrier_hz * dt;
for p in &mut snap.paths {
p.group_delay_s += dt;
p.carrier_phase_rad += dphi;
}
}
pub(crate) fn sat_seed(seed: u64, sat: u32, stream: u64) -> u64 {
let mut z = seed
.wrapping_add(0x9E37_79B9_7F4A_7C15u64.wrapping_mul(u64::from(sat) + 1))
.wrapping_add(stream.wrapping_mul(0xD1B5_4A32_D192_ED03));
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
#[cfg(test)]
pub(crate) fn test_geom(el_deg: f64) -> LineOfSight {
LineOfSight {
el_rad: el_deg.to_radians(),
az_rad: 0.7,
range_m: 21_000_000.0,
range_rate_mps: -300.0,
lat_rad: 40f64.to_radians(),
lon_rad: -105f64.to_radians(),
height_m: 1600.0,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_composite_yields_the_geometric_direct_path() {
let mut c = Composite::new(1_575_420_000.0);
assert!(c.is_empty());
let g = test_geom(30.0);
let s = c.snapshot(3, 1.0, &g);
assert_eq!(s.t_s, 1.0);
assert_eq!(s.paths.len(), 1);
let p = s.paths[0];
assert!((p.group_delay_s - g.range_m / C_M_PER_S).abs() < 1e-18);
let want = -2.0 * PI * 1_575_420_000.0 * g.range_m / C_M_PER_S;
assert!((p.carrier_phase_rad - want).abs() < 1e-6);
assert_eq!(p.amplitude, 1.0);
assert!(!s.is_nlos());
}
#[test]
fn sat_seeds_differ_by_satellite_and_stream() {
assert_ne!(sat_seed(1, 1, 0), sat_seed(1, 2, 0));
assert_ne!(sat_seed(1, 1, 0), sat_seed(1, 1, 1));
assert_eq!(sat_seed(7, 9, 3), sat_seed(7, 9, 3));
}
}