use super::{sat_seed, ChannelEffect, LineOfSight};
use crate::iq::simrng::SimRng;
use crate::iq::{ChannelSnapshot, PathState};
use rand::Rng;
use rand_distr::StandardNormal;
use std::collections::BTreeMap;
use std::f64::consts::PI;
const STREAM: u64 = 0x1A4D_0000;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum LmsState {
LineOfSight = 0,
Shadowed = 1,
Blocked = 2,
}
impl LmsState {
fn from_index(i: usize) -> Self {
match i {
0 => LmsState::LineOfSight,
1 => LmsState::Shadowed,
_ => LmsState::Blocked,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct LooParams {
pub mean_db: f64,
pub std_db: f64,
pub multipath_db: f64,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct LandMobileParams {
pub transition: [[f64; 3]; 3],
pub step_s: f64,
pub los: LooParams,
pub shadowed: LooParams,
pub blocked_multipath_db: f64,
pub diffuse_excess_delay_s: f64,
}
impl Default for LandMobileParams {
fn default() -> Self {
Self {
transition: [[0.98, 0.015, 0.005], [0.03, 0.95, 0.02], [0.02, 0.03, 0.95]],
step_s: 0.1,
los: LooParams {
mean_db: -0.5,
std_db: 0.5,
multipath_db: -20.0,
},
shadowed: LooParams {
mean_db: -6.0,
std_db: 3.0,
multipath_db: -15.0,
},
blocked_multipath_db: -18.0,
diffuse_excess_delay_s: 50e-9,
}
}
}
impl LandMobileParams {
pub fn stationary(&self) -> [f64; 3] {
let p = self.transition;
let mut a = [[0.0f64; 4]; 3];
for (i, row) in a.iter_mut().take(2).enumerate() {
for (j, v) in row.iter_mut().take(3).enumerate() {
*v = p[j][i] - if i == j { 1.0 } else { 0.0 };
}
}
a[2] = [1.0, 1.0, 1.0, 1.0];
for col in 0..3 {
let piv = (col..3)
.max_by(|&x, &y| a[x][col].abs().total_cmp(&a[y][col].abs()))
.unwrap_or(col);
a.swap(col, piv);
for r in 0..3 {
if r != col && a[col][col] != 0.0 {
let f = a[r][col] / a[col][col];
let pivot_row = a[col];
for (x, pv) in a[r].iter_mut().zip(pivot_row).skip(col) {
*x -= f * pv;
}
}
}
}
[a[0][3] / a[0][0], a[1][3] / a[1][1], a[2][3] / a[2][2]]
}
}
#[derive(Clone, Debug)]
struct SatState {
rng: SimRng,
state: LmsState,
step: i64,
direct_amp: f64,
diffuse_amp: f64,
diffuse_phase: f64,
}
#[derive(Clone, Debug)]
pub struct LandMobile {
pub params: LandMobileParams,
seed: u64,
sats: BTreeMap<u32, SatState>,
}
fn draw_amplitudes(p: &LandMobileParams, st: &mut SatState) {
let (direct, mp_db) = match st.state {
LmsState::LineOfSight | LmsState::Shadowed => {
let loo = if st.state == LmsState::LineOfSight {
p.los
} else {
p.shadowed
};
let g: f64 = st.rng.sample(StandardNormal);
(
10f64.powf((loo.mean_db + loo.std_db * g) / 20.0),
loo.multipath_db,
)
}
LmsState::Blocked => (0.0, p.blocked_multipath_db),
};
let s = (10f64.powf(mp_db / 10.0) / 2.0).sqrt();
let x: f64 = st.rng.sample(StandardNormal);
let y: f64 = st.rng.sample(StandardNormal);
st.direct_amp = direct;
st.diffuse_amp = s * x.hypot(y);
st.diffuse_phase = st.rng.gen_range(-PI..PI);
}
impl LandMobile {
pub fn new(params: LandMobileParams, seed: u64) -> Self {
Self {
params,
seed,
sats: BTreeMap::new(),
}
}
fn advance(&mut self, sat: u32, t_s: f64) -> &SatState {
let p = self.params;
let seed = self.seed;
let step = (t_s / p.step_s).floor() as i64;
let st = self.sats.entry(sat).or_insert_with(|| {
let mut rng = SimRng::seed(sat_seed(seed, sat, STREAM));
let pi = p.stationary();
let u: f64 = rng.gen();
let state = if u < pi[0] {
LmsState::LineOfSight
} else if u < pi[0] + pi[1] {
LmsState::Shadowed
} else {
LmsState::Blocked
};
let mut st = SatState {
rng,
state,
step,
direct_amp: 1.0,
diffuse_amp: 0.0,
diffuse_phase: 0.0,
};
draw_amplitudes(&p, &mut st);
st
});
while st.step < step {
let row = p.transition[st.state as usize];
let u: f64 = st.rng.gen();
let next = if u < row[0] {
0
} else if u < row[0] + row[1] {
1
} else {
2
};
st.state = LmsState::from_index(next);
st.step += 1;
draw_amplitudes(&p, st);
}
st
}
pub fn state_at(&mut self, sat: u32, t_s: f64) -> LmsState {
self.advance(sat, t_s).state
}
}
impl ChannelEffect for LandMobile {
fn apply(
&mut self,
sat: u32,
t_s: f64,
_geom: &LineOfSight,
_carrier_hz: f64,
snap: &mut ChannelSnapshot,
) {
let excess = self.params.diffuse_excess_delay_s;
let st = self.advance(sat, t_s).clone();
let Some(direct) = snap.paths.first_mut() else {
return;
};
let base = *direct;
direct.amplitude *= st.direct_amp;
snap.paths.push(PathState {
group_delay_s: base.group_delay_s + excess,
carrier_phase_rad: base.carrier_phase_rad + st.diffuse_phase,
amplitude: base.amplitude * st.diffuse_amp,
extra_doppler_hz: base.extra_doppler_hz,
});
}
}
#[cfg(test)]
mod tests {
use super::super::{test_geom, ChannelModel, Composite};
use super::*;
use crate::gnss_sim::L1_HZ;
#[test]
fn stationary_distribution_solves_pi_p_equals_pi() {
let p = LandMobileParams::default();
let pi = p.stationary();
assert!((pi.iter().sum::<f64>() - 1.0).abs() < 1e-12);
let mut v = [1.0, 0.0, 0.0];
for _ in 0..5000 {
let mut w = [0.0; 3];
for (i, vi) in v.iter().enumerate() {
for (wj, pij) in w.iter_mut().zip(p.transition[i]) {
*wj += vi * pij;
}
}
v = w;
}
for k in 0..3 {
assert!((v[k] - pi[k]).abs() < 1e-12, "{v:?} vs {pi:?}");
}
}
#[test]
fn state_occupancy_converges_to_the_stationary_distribution() {
let p = LandMobileParams::default();
let pi = p.stationary();
let mut lm = LandMobile::new(p, 77);
let n = 1_000_000usize;
let mut count = [0usize; 3];
for k in 0..n {
count[lm.state_at(3, k as f64 * p.step_s) as usize] += 1;
}
for s in 0..3 {
let occ = count[s] as f64 / n as f64;
assert!((occ - pi[s]).abs() < 0.015, "state {s}: {occ} vs {}", pi[s]);
}
}
#[test]
fn blocked_state_yields_nlos_snapshots() {
let p = LandMobileParams::default();
let mut probe = LandMobile::new(p, 5);
let mut ch = Composite::new(L1_HZ).with(LandMobile::new(p, 5));
let g = test_geom(40.0);
let (mut blocked, mut seen) = (0, 0);
for k in 0..20_000 {
let t = k as f64 * p.step_s;
let state = probe.state_at(2, t);
let s = ch.snapshot(2, t, &g);
assert_eq!(s.paths.len(), 2);
assert_eq!(s.is_nlos(), state == LmsState::Blocked, "step {k}");
if state == LmsState::Blocked {
blocked += 1;
assert_eq!(s.paths[0].amplitude, 0.0);
} else {
seen += 1;
}
}
assert!(blocked > 100 && seen > 100);
}
#[test]
fn loo_direct_amplitude_and_diffuse_power_match_their_parameters() {
let p = LandMobileParams {
transition: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
..LandMobileParams::default()
};
let mut lm = LandMobile::new(p, 9);
let sat = (0..100u32)
.find(|&s| lm.state_at(s, 0.0) == LmsState::LineOfSight)
.unwrap();
let n = 100_000;
let (mut s1, mut s2, mut mp) = (0.0, 0.0, 0.0);
for k in 0..n {
let st = lm.advance(sat, k as f64 * p.step_s).clone();
let db = 20.0 * st.direct_amp.log10();
s1 += db;
s2 += db * db;
mp += st.diffuse_amp * st.diffuse_amp;
}
let nf = n as f64;
let mean = s1 / nf;
let sd = (s2 / nf - mean * mean).sqrt();
assert!((mean - p.los.mean_db).abs() < 0.01, "{mean}");
assert!((sd - p.los.std_db).abs() < 0.01, "{sd}");
let mp_db = 10.0 * (mp / nf).log10();
assert!((mp_db - p.los.multipath_db).abs() < 0.1, "{mp_db}");
}
#[test]
fn same_seed_is_bit_identical() {
let p = LandMobileParams::default();
let g = test_geom(30.0);
let mut a = Composite::new(L1_HZ).with(LandMobile::new(p, 1));
let mut b = Composite::new(L1_HZ).with(LandMobile::new(p, 1));
for k in 0..2000 {
let t = k as f64 * 0.05;
assert_eq!(a.snapshot(6, t, &g), b.snapshot(6, t, &g));
}
}
}