sidereon_core/observation_qc/
multipath.rs1use 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#[derive(Debug, Clone, PartialEq, Default, serde::Serialize)]
19pub struct MultipathReport {
20 pub satellites: Vec<SatelliteMultipathQc>,
22 pub systems: Vec<SystemMultipathQc>,
24}
25
26#[derive(Debug, Clone, PartialEq, serde::Serialize)]
28pub struct SatelliteMultipathQc {
29 pub satellite: GnssSatelliteId,
31 pub mp1: Option<MpStats>,
33 pub mp2: Option<MpStats>,
35}
36
37#[derive(Debug, Clone, PartialEq, serde::Serialize)]
39pub struct SystemMultipathQc {
40 pub system: GnssSystem,
42 pub mp1: Option<MpStats>,
44 pub mp2: Option<MpStats>,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize)]
50pub struct MpStats {
51 pub n: usize,
53 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
158pub 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
177pub 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
193pub 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}