Skip to main content

sidereon_core/observation_qc/
multipath.rs

1//! MP1/MP2 code multipath RMS for observation QC.
2
3use std::collections::{BTreeMap, BTreeSet, VecDeque};
4
5use crate::carrier_phase::phase_meters;
6use crate::id::{GnssSatelliteId, GnssSystem};
7use crate::precise_positioning::{detect_cycle_slips, CycleSlipConfig, DualFrequencyObservation};
8use crate::rinex::observations::{ObsEpochTime, RinexObs};
9use crate::rinex_common::dominant_obs_interval_s;
10
11use super::{
12    detect_gaps, dual_frequency_epochs, multipath_dual_frequency_observation, ObservationQcOptions,
13};
14
15const TEQC_MP_MOVING_AVERAGE_POINTS: usize = 50;
16
17/// MP1/MP2 multipath RMS report.
18#[derive(Debug, Clone, PartialEq, Default, serde::Serialize)]
19pub struct MultipathReport {
20    /// Per-satellite MP1/MP2 RMS rows.
21    pub satellites: Vec<SatelliteMultipathQc>,
22    /// Per-constellation MP1/MP2 RMS rows.
23    pub systems: Vec<SystemMultipathQc>,
24}
25
26/// Per-satellite multipath RMS after per-arc teqc moving-average filtering.
27#[derive(Debug, Clone, PartialEq, serde::Serialize)]
28pub struct SatelliteMultipathQc {
29    /// Satellite id.
30    pub satellite: GnssSatelliteId,
31    /// MP1 RMS, when at least one complete sample was available.
32    pub mp1: Option<MpStats>,
33    /// MP2 RMS, when at least one complete sample was available.
34    pub mp2: Option<MpStats>,
35}
36
37/// Per-constellation multipath RMS after per-arc teqc moving-average filtering.
38#[derive(Debug, Clone, PartialEq, serde::Serialize)]
39pub struct SystemMultipathQc {
40    /// GNSS constellation.
41    pub system: GnssSystem,
42    /// MP1 RMS across satellites in this constellation.
43    pub mp1: Option<MpStats>,
44    /// MP2 RMS across satellites in this constellation.
45    pub mp2: Option<MpStats>,
46}
47
48/// RMS statistics for one multipath series.
49#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize)]
50pub struct MpStats {
51    /// Number of multipath samples.
52    pub n: usize,
53    /// Root-mean-square multipath residual, in meters.
54    pub rms_m: f64,
55}
56
57#[derive(Debug, Clone, Copy)]
58struct MultipathSample {
59    mp1_m: f64,
60    mp2_m: f64,
61}
62
63#[derive(Debug, Default)]
64struct SatelliteMultipathState {
65    mp1_arc_m: Vec<f64>,
66    mp2_arc_m: Vec<f64>,
67    mp1_residuals: MovingAverageAccum,
68    mp2_residuals: MovingAverageAccum,
69}
70
71impl SatelliteMultipathState {
72    fn push(&mut self, sample: MultipathSample) {
73        debug_assert!(sample.mp1_m.is_finite());
74        debug_assert!(sample.mp2_m.is_finite());
75        self.mp1_arc_m.push(sample.mp1_m);
76        self.mp2_arc_m.push(sample.mp2_m);
77    }
78
79    fn close_arc(&mut self) {
80        debug_assert_eq!(self.mp1_arc_m.len(), self.mp2_arc_m.len());
81        self.mp1_residuals.add_arc(&self.mp1_arc_m);
82        self.mp2_residuals.add_arc(&self.mp2_arc_m);
83        self.mp1_arc_m.clear();
84        self.mp2_arc_m.clear();
85    }
86
87    fn finish(mut self) -> (Option<MpStats>, Option<MpStats>) {
88        self.close_arc();
89        (self.mp1_residuals.finish(), self.mp2_residuals.finish())
90    }
91}
92
93#[derive(Debug, Clone, Copy, Default)]
94struct MovingAverageAccum {
95    n: usize,
96    sum_sq_m2: f64,
97}
98
99impl MovingAverageAccum {
100    fn add_arc(&mut self, arc_m: &[f64]) {
101        if arc_m.is_empty() {
102            return;
103        }
104
105        let mut window =
106            VecDeque::<f64>::with_capacity(TEQC_MP_MOVING_AVERAGE_POINTS.min(arc_m.len()));
107        for value_m in arc_m {
108            window.push_back(*value_m);
109            if window.len() > TEQC_MP_MOVING_AVERAGE_POINTS {
110                window.pop_front();
111            }
112            let residual_m = *value_m - mean_window(&window);
113            self.n += 1;
114            self.sum_sq_m2 += residual_m * residual_m;
115        }
116    }
117
118    fn finish(self) -> Option<MpStats> {
119        (self.n > 0).then(|| MpStats {
120            n: self.n,
121            rms_m: (self.sum_sq_m2 / self.n as f64).sqrt(),
122        })
123    }
124}
125
126#[derive(Debug, Default)]
127struct SystemMultipathAccum {
128    mp1: SystemMovingAverageAccum,
129    mp2: SystemMovingAverageAccum,
130}
131
132#[derive(Debug, Clone, Copy, Default)]
133struct SystemMovingAverageAccum {
134    samples: usize,
135    satellite_rms_sum_m: f64,
136}
137
138impl SystemMovingAverageAccum {
139    fn add_stats(&mut self, stats: MpStats) {
140        self.samples += stats.n;
141        if stats.n > 1 {
142            self.satellite_rms_sum_m += stats.rms_m;
143        }
144    }
145
146    fn finish(self, epoch_count: usize) -> Option<MpStats> {
147        if self.samples == 0 || epoch_count == 0 {
148            return None;
149        }
150        let average_observations_per_epoch = self.samples as f64 / epoch_count as f64;
151        Some(MpStats {
152            n: self.samples,
153            rms_m: self.satellite_rms_sum_m / average_observations_per_epoch,
154        })
155    }
156}
157
158/// Compute the meter-valued MP combination for one band.
159///
160/// Inputs are pseudorange and phase in meters. The operation order intentionally
161/// squares each carrier first, forms the signed scale factor, then combines.
162pub fn mp_combination(p_i_m: f64, l_i_m: f64, l_j_m: f64, f_i_hz: f64, f_j_hz: f64) -> f64 {
163    debug_assert!(p_i_m.is_finite());
164    debug_assert!(l_i_m.is_finite());
165    debug_assert!(l_j_m.is_finite());
166    debug_assert!(f_i_hz.is_finite() && f_i_hz > 0.0);
167    debug_assert!(f_j_hz.is_finite() && f_j_hz > 0.0);
168
169    let fi_sq = f_i_hz * f_i_hz;
170    let fj_sq = f_j_hz * f_j_hz;
171    let denominator = fi_sq - fj_sq;
172    debug_assert!(denominator != 0.0);
173    let factor = 2.0 * fj_sq / denominator;
174    p_i_m - l_i_m - factor * (l_i_m - l_j_m)
175}
176
177/// RMS of a single continuous-phase MP arc after subtracting that arc's mean.
178pub fn arc_multipath_rms(series_m: &[f64]) -> f64 {
179    if series_m.is_empty() {
180        return 0.0;
181    }
182    let mean_m = mean(series_m);
183    let sum_sq_m2 = series_m
184        .iter()
185        .map(|value_m| {
186            let residual_m = *value_m - mean_m;
187            residual_m * residual_m
188        })
189        .sum::<f64>();
190    (sum_sq_m2 / series_m.len() as f64).sqrt()
191}
192
193/// Compute MP1/MP2 RMS stats from a parsed observation file.
194pub fn multipath_stats(obs: &RinexObs, cycle_slip_config: &CycleSlipConfig) -> MultipathReport {
195    let slip_boundaries = slip_boundaries(obs, cycle_slip_config);
196    let gap_starts = gap_start_epoch_indices(obs);
197    let mut states = BTreeMap::<GnssSatelliteId, SatelliteMultipathState>::new();
198    let epoch_count = obs.epochs().iter().filter(|epoch| epoch.flag <= 1).count();
199
200    for (epoch_index, epoch) in obs
201        .epochs()
202        .iter()
203        .filter(|epoch| epoch.flag <= 1)
204        .enumerate()
205    {
206        if gap_starts.contains(&epoch_index) {
207            close_all_arcs(&mut states);
208        }
209
210        let mut samples =
211            BTreeMap::<GnssSatelliteId, (DualFrequencyObservation, MultipathSample)>::new();
212        for (satellite, values) in &epoch.sats {
213            let Some(observation) = multipath_dual_frequency_observation(obs, *satellite, values)
214            else {
215                continue;
216            };
217            let Some(sample) = multipath_sample(&observation) else {
218                continue;
219            };
220            samples.insert(*satellite, (observation, sample));
221        }
222
223        let active_satellites = states.keys().copied().collect::<Vec<_>>();
224        for satellite in active_satellites {
225            if !samples.contains_key(&satellite) {
226                if let Some(state) = states.get_mut(&satellite) {
227                    state.close_arc();
228                }
229            }
230        }
231
232        for (satellite, (observation, sample)) in samples {
233            let state = states.entry(satellite).or_default();
234            if slip_boundaries.contains(&(epoch_index, observation.satellite_id)) {
235                state.close_arc();
236            }
237            state.push(sample);
238        }
239    }
240
241    finish_report(states, epoch_count)
242}
243
244fn close_all_arcs(states: &mut BTreeMap<GnssSatelliteId, SatelliteMultipathState>) {
245    for state in states.values_mut() {
246        state.close_arc();
247    }
248}
249
250fn finish_report(
251    states: BTreeMap<GnssSatelliteId, SatelliteMultipathState>,
252    epoch_count: usize,
253) -> MultipathReport {
254    let mut satellite_rows = Vec::new();
255    let mut system_accums = BTreeMap::<GnssSystem, SystemMultipathAccum>::new();
256
257    for (satellite, state) in states {
258        let (mp1, mp2) = state.finish();
259        if mp1.is_none() && mp2.is_none() {
260            continue;
261        }
262        let system_acc = system_accums.entry(satellite.system).or_default();
263        if let Some(stats) = mp1 {
264            system_acc.mp1.add_stats(stats);
265        }
266        if let Some(stats) = mp2 {
267            system_acc.mp2.add_stats(stats);
268        }
269        satellite_rows.push(SatelliteMultipathQc {
270            satellite,
271            mp1,
272            mp2,
273        });
274    }
275
276    let systems = system_accums
277        .into_iter()
278        .map(|(system, acc)| SystemMultipathQc {
279            system,
280            mp1: acc.mp1.finish(epoch_count),
281            mp2: acc.mp2.finish(epoch_count),
282        })
283        .collect();
284
285    MultipathReport {
286        satellites: satellite_rows,
287        systems,
288    }
289}
290
291fn multipath_sample(observation: &DualFrequencyObservation) -> Option<MultipathSample> {
292    let l1_m = phase_meters(observation.phi1_cyc, observation.f1_hz).ok()?;
293    let l2_m = phase_meters(observation.phi2_cyc, observation.f2_hz).ok()?;
294    let mp1_m = mp_combination(
295        observation.p1_m,
296        l1_m,
297        l2_m,
298        observation.f1_hz,
299        observation.f2_hz,
300    );
301    let mp2_m = mp_combination(
302        observation.p2_m,
303        l2_m,
304        l1_m,
305        observation.f2_hz,
306        observation.f1_hz,
307    );
308    (mp1_m.is_finite() && mp2_m.is_finite()).then_some(MultipathSample { mp1_m, mp2_m })
309}
310
311fn slip_boundaries(
312    obs: &RinexObs,
313    cycle_slip_config: &CycleSlipConfig,
314) -> BTreeSet<(usize, String)> {
315    let epochs = dual_frequency_epochs(obs);
316    let Ok(flags) = detect_cycle_slips(&epochs, *cycle_slip_config) else {
317        return BTreeSet::new();
318    };
319
320    flags
321        .into_iter()
322        .enumerate()
323        .flat_map(|(epoch_index, epoch)| {
324            epoch
325                .observations
326                .into_iter()
327                .filter(|observation| observation.slip)
328                .map(move |observation| (epoch_index, observation.satellite_id))
329        })
330        .collect()
331}
332
333fn gap_start_epoch_indices(obs: &RinexObs) -> BTreeSet<usize> {
334    let epoch_times = observation_epoch_times(obs);
335    let interval_s = obs
336        .header()
337        .interval_s
338        .filter(|interval_s| interval_s.is_finite() && *interval_s > 0.0)
339        .or_else(|| dominant_obs_interval_s(&epoch_times));
340    let Ok(gaps) = detect_gaps(ObservationQcOptions::default(), &epoch_times, interval_s) else {
341        return BTreeSet::new();
342    };
343
344    gaps.into_iter()
345        .filter_map(|gap| epoch_times.iter().position(|epoch| *epoch == gap.end_epoch))
346        .collect()
347}
348
349fn observation_epoch_times(obs: &RinexObs) -> Vec<ObsEpochTime> {
350    obs.epochs()
351        .iter()
352        .filter(|epoch| epoch.flag <= 1)
353        .map(|epoch| epoch.epoch)
354        .collect()
355}
356
357fn mean(series_m: &[f64]) -> f64 {
358    series_m.iter().sum::<f64>() / series_m.len() as f64
359}
360
361fn mean_window(series_m: &VecDeque<f64>) -> f64 {
362    series_m.iter().sum::<f64>() / series_m.len() as f64
363}