use std::collections::{BTreeMap, BTreeSet, VecDeque};
use crate::carrier_phase::phase_meters;
use crate::id::{GnssSatelliteId, GnssSystem};
use crate::precise_positioning::{detect_cycle_slips, CycleSlipConfig, DualFrequencyObservation};
use crate::rinex::observations::{ObsEpochTime, RinexObs};
use crate::rinex_common::dominant_obs_interval_s;
use super::{
detect_gaps, dual_frequency_epochs, multipath_dual_frequency_observation, ObservationQcOptions,
};
const TEQC_MP_MOVING_AVERAGE_POINTS: usize = 50;
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize)]
pub struct MultipathReport {
pub satellites: Vec<SatelliteMultipathQc>,
pub systems: Vec<SystemMultipathQc>,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
pub struct SatelliteMultipathQc {
pub satellite: GnssSatelliteId,
pub mp1: Option<MpStats>,
pub mp2: Option<MpStats>,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
pub struct SystemMultipathQc {
pub system: GnssSystem,
pub mp1: Option<MpStats>,
pub mp2: Option<MpStats>,
}
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize)]
pub struct MpStats {
pub n: usize,
pub rms_m: f64,
}
#[derive(Debug, Clone, Copy)]
struct MultipathSample {
mp1_m: f64,
mp2_m: f64,
}
#[derive(Debug, Default)]
struct SatelliteMultipathState {
mp1_arc_m: Vec<f64>,
mp2_arc_m: Vec<f64>,
mp1_residuals: MovingAverageAccum,
mp2_residuals: MovingAverageAccum,
}
impl SatelliteMultipathState {
fn push(&mut self, sample: MultipathSample) {
debug_assert!(sample.mp1_m.is_finite());
debug_assert!(sample.mp2_m.is_finite());
self.mp1_arc_m.push(sample.mp1_m);
self.mp2_arc_m.push(sample.mp2_m);
}
fn close_arc(&mut self) {
debug_assert_eq!(self.mp1_arc_m.len(), self.mp2_arc_m.len());
self.mp1_residuals.add_arc(&self.mp1_arc_m);
self.mp2_residuals.add_arc(&self.mp2_arc_m);
self.mp1_arc_m.clear();
self.mp2_arc_m.clear();
}
fn finish(mut self) -> (Option<MpStats>, Option<MpStats>) {
self.close_arc();
(self.mp1_residuals.finish(), self.mp2_residuals.finish())
}
}
#[derive(Debug, Clone, Copy, Default)]
struct MovingAverageAccum {
n: usize,
sum_sq_m2: f64,
}
impl MovingAverageAccum {
fn add_arc(&mut self, arc_m: &[f64]) {
if arc_m.is_empty() {
return;
}
let mut window =
VecDeque::<f64>::with_capacity(TEQC_MP_MOVING_AVERAGE_POINTS.min(arc_m.len()));
for value_m in arc_m {
window.push_back(*value_m);
if window.len() > TEQC_MP_MOVING_AVERAGE_POINTS {
window.pop_front();
}
let residual_m = *value_m - mean_window(&window);
self.n += 1;
self.sum_sq_m2 += residual_m * residual_m;
}
}
fn finish(self) -> Option<MpStats> {
(self.n > 0).then(|| MpStats {
n: self.n,
rms_m: (self.sum_sq_m2 / self.n as f64).sqrt(),
})
}
}
#[derive(Debug, Default)]
struct SystemMultipathAccum {
mp1: SystemMovingAverageAccum,
mp2: SystemMovingAverageAccum,
}
#[derive(Debug, Clone, Copy, Default)]
struct SystemMovingAverageAccum {
samples: usize,
satellite_rms_sum_m: f64,
}
impl SystemMovingAverageAccum {
fn add_stats(&mut self, stats: MpStats) {
self.samples += stats.n;
if stats.n > 1 {
self.satellite_rms_sum_m += stats.rms_m;
}
}
fn finish(self, epoch_count: usize) -> Option<MpStats> {
if self.samples == 0 || epoch_count == 0 {
return None;
}
let average_observations_per_epoch = self.samples as f64 / epoch_count as f64;
Some(MpStats {
n: self.samples,
rms_m: self.satellite_rms_sum_m / average_observations_per_epoch,
})
}
}
pub fn mp_combination(p_i_m: f64, l_i_m: f64, l_j_m: f64, f_i_hz: f64, f_j_hz: f64) -> f64 {
debug_assert!(p_i_m.is_finite());
debug_assert!(l_i_m.is_finite());
debug_assert!(l_j_m.is_finite());
debug_assert!(f_i_hz.is_finite() && f_i_hz > 0.0);
debug_assert!(f_j_hz.is_finite() && f_j_hz > 0.0);
let fi_sq = f_i_hz * f_i_hz;
let fj_sq = f_j_hz * f_j_hz;
let denominator = fi_sq - fj_sq;
debug_assert!(denominator != 0.0);
let factor = 2.0 * fj_sq / denominator;
p_i_m - l_i_m - factor * (l_i_m - l_j_m)
}
pub fn arc_multipath_rms(series_m: &[f64]) -> f64 {
if series_m.is_empty() {
return 0.0;
}
let mean_m = mean(series_m);
let sum_sq_m2 = series_m
.iter()
.map(|value_m| {
let residual_m = *value_m - mean_m;
residual_m * residual_m
})
.sum::<f64>();
(sum_sq_m2 / series_m.len() as f64).sqrt()
}
pub fn multipath_stats(obs: &RinexObs, cycle_slip_config: &CycleSlipConfig) -> MultipathReport {
let slip_boundaries = slip_boundaries(obs, cycle_slip_config);
let gap_starts = gap_start_epoch_indices(obs);
let mut states = BTreeMap::<GnssSatelliteId, SatelliteMultipathState>::new();
let epoch_count = obs.epochs().iter().filter(|epoch| epoch.flag <= 1).count();
for (epoch_index, epoch) in obs
.epochs()
.iter()
.filter(|epoch| epoch.flag <= 1)
.enumerate()
{
if gap_starts.contains(&epoch_index) {
close_all_arcs(&mut states);
}
let mut samples =
BTreeMap::<GnssSatelliteId, (DualFrequencyObservation, MultipathSample)>::new();
for (satellite, values) in &epoch.sats {
let Some(observation) = multipath_dual_frequency_observation(obs, *satellite, values)
else {
continue;
};
let Some(sample) = multipath_sample(&observation) else {
continue;
};
samples.insert(*satellite, (observation, sample));
}
let active_satellites = states.keys().copied().collect::<Vec<_>>();
for satellite in active_satellites {
if !samples.contains_key(&satellite) {
if let Some(state) = states.get_mut(&satellite) {
state.close_arc();
}
}
}
for (satellite, (observation, sample)) in samples {
let state = states.entry(satellite).or_default();
if slip_boundaries.contains(&(epoch_index, observation.satellite_id)) {
state.close_arc();
}
state.push(sample);
}
}
finish_report(states, epoch_count)
}
fn close_all_arcs(states: &mut BTreeMap<GnssSatelliteId, SatelliteMultipathState>) {
for state in states.values_mut() {
state.close_arc();
}
}
fn finish_report(
states: BTreeMap<GnssSatelliteId, SatelliteMultipathState>,
epoch_count: usize,
) -> MultipathReport {
let mut satellite_rows = Vec::new();
let mut system_accums = BTreeMap::<GnssSystem, SystemMultipathAccum>::new();
for (satellite, state) in states {
let (mp1, mp2) = state.finish();
if mp1.is_none() && mp2.is_none() {
continue;
}
let system_acc = system_accums.entry(satellite.system).or_default();
if let Some(stats) = mp1 {
system_acc.mp1.add_stats(stats);
}
if let Some(stats) = mp2 {
system_acc.mp2.add_stats(stats);
}
satellite_rows.push(SatelliteMultipathQc {
satellite,
mp1,
mp2,
});
}
let systems = system_accums
.into_iter()
.map(|(system, acc)| SystemMultipathQc {
system,
mp1: acc.mp1.finish(epoch_count),
mp2: acc.mp2.finish(epoch_count),
})
.collect();
MultipathReport {
satellites: satellite_rows,
systems,
}
}
fn multipath_sample(observation: &DualFrequencyObservation) -> Option<MultipathSample> {
let l1_m = phase_meters(observation.phi1_cyc, observation.f1_hz).ok()?;
let l2_m = phase_meters(observation.phi2_cyc, observation.f2_hz).ok()?;
let mp1_m = mp_combination(
observation.p1_m,
l1_m,
l2_m,
observation.f1_hz,
observation.f2_hz,
);
let mp2_m = mp_combination(
observation.p2_m,
l2_m,
l1_m,
observation.f2_hz,
observation.f1_hz,
);
(mp1_m.is_finite() && mp2_m.is_finite()).then_some(MultipathSample { mp1_m, mp2_m })
}
fn slip_boundaries(
obs: &RinexObs,
cycle_slip_config: &CycleSlipConfig,
) -> BTreeSet<(usize, String)> {
let epochs = dual_frequency_epochs(obs);
let Ok(flags) = detect_cycle_slips(&epochs, *cycle_slip_config) else {
return BTreeSet::new();
};
flags
.into_iter()
.enumerate()
.flat_map(|(epoch_index, epoch)| {
epoch
.observations
.into_iter()
.filter(|observation| observation.slip)
.map(move |observation| (epoch_index, observation.satellite_id))
})
.collect()
}
fn gap_start_epoch_indices(obs: &RinexObs) -> BTreeSet<usize> {
let epoch_times = observation_epoch_times(obs);
let interval_s = obs
.header()
.interval_s
.filter(|interval_s| interval_s.is_finite() && *interval_s > 0.0)
.or_else(|| dominant_obs_interval_s(&epoch_times));
let Ok(gaps) = detect_gaps(ObservationQcOptions::default(), &epoch_times, interval_s) else {
return BTreeSet::new();
};
gaps.into_iter()
.filter_map(|gap| epoch_times.iter().position(|epoch| *epoch == gap.end_epoch))
.collect()
}
fn observation_epoch_times(obs: &RinexObs) -> Vec<ObsEpochTime> {
obs.epochs()
.iter()
.filter(|epoch| epoch.flag <= 1)
.map(|epoch| epoch.epoch)
.collect()
}
fn mean(series_m: &[f64]) -> f64 {
series_m.iter().sum::<f64>() / series_m.len() as f64
}
fn mean_window(series_m: &VecDeque<f64>) -> f64 {
series_m.iter().sum::<f64>() / series_m.len() as f64
}