use crate::id::GnssSatelliteId;
use crate::observables::{ObservableEphemerisSource, ObservablesError};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ReferenceState {
pub query_j2000_s: f64,
pub position_ecef_m: [f64; 3],
pub clock_s: Option<f64>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct InterpolationDivergence {
pub query_j2000_s: f64,
pub interpolated_position_m: [f64; 3],
pub reference_position_m: [f64; 3],
pub position_diff_m: [f64; 3],
pub position_norm_m: f64,
pub interpolated_clock_s: Option<f64>,
pub reference_clock_s: Option<f64>,
pub clock_diff_s: Option<f64>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct InterpolationComparison {
pub per_epoch: Vec<InterpolationDivergence>,
pub max_position_norm_m: f64,
pub rms_position_norm_m: f64,
pub max_abs_clock_diff_s: Option<f64>,
}
pub fn compare_position_series(
source: &dyn ObservableEphemerisSource,
sat: GnssSatelliteId,
reference: &[ReferenceState],
) -> Result<InterpolationComparison, ObservablesError> {
let mut per_epoch = Vec::with_capacity(reference.len());
let mut max_position_norm_m = 0.0f64;
let mut sum_sq_norm = 0.0f64;
let mut max_abs_clock_diff_s: Option<f64> = None;
for reference_state in reference {
let state = source.observable_state_at_j2000_s(sat, reference_state.query_j2000_s)?;
let interpolated_position_m = state.position_ecef_m;
let reference_position_m = reference_state.position_ecef_m;
let position_diff_m = [
interpolated_position_m[0] - reference_position_m[0],
interpolated_position_m[1] - reference_position_m[1],
interpolated_position_m[2] - reference_position_m[2],
];
let position_norm_m = (position_diff_m[0] * position_diff_m[0]
+ position_diff_m[1] * position_diff_m[1]
+ position_diff_m[2] * position_diff_m[2])
.sqrt();
let clock_diff_s = match (state.clock_s, reference_state.clock_s) {
(Some(a), Some(b)) => {
let diff = a - b;
let abs = diff.abs();
max_abs_clock_diff_s = Some(max_abs_clock_diff_s.map_or(abs, |m| m.max(abs)));
Some(diff)
}
_ => None,
};
max_position_norm_m = max_position_norm_m.max(position_norm_m);
sum_sq_norm += position_norm_m * position_norm_m;
per_epoch.push(InterpolationDivergence {
query_j2000_s: reference_state.query_j2000_s,
interpolated_position_m,
reference_position_m,
position_diff_m,
position_norm_m,
interpolated_clock_s: state.clock_s,
reference_clock_s: reference_state.clock_s,
clock_diff_s,
});
}
let rms_position_norm_m = if per_epoch.is_empty() {
0.0
} else {
(sum_sq_norm / per_epoch.len() as f64).sqrt()
};
Ok(InterpolationComparison {
per_epoch,
max_position_norm_m,
rms_position_norm_m,
max_abs_clock_diff_s,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::astro::time::model::{Instant, InstantRepr, JulianDateSplit, TimeScale};
use crate::sp3::{PreciseEphemerisSample, PreciseEphemerisSamples};
use crate::GnssSystem;
const J2000_JD_WHOLE: f64 = 2_451_545.0;
const SECONDS_PER_DAY: f64 = 86_400.0;
fn gps(prn: u8) -> GnssSatelliteId {
GnssSatelliteId::new(GnssSystem::Gps, prn).expect("valid satellite id")
}
fn sample(j2000_s: f64, pos: [f64; 3], clk: Option<f64>) -> PreciseEphemerisSample {
let split =
JulianDateSplit::new(J2000_JD_WHOLE, j2000_s / SECONDS_PER_DAY).expect("valid split");
PreciseEphemerisSample::new(
gps(21),
Instant {
scale: TimeScale::Gpst,
repr: InstantRepr::JulianDate(split),
},
pos,
clk,
)
}
fn source() -> PreciseEphemerisSamples {
let samples: Vec<_> = (0..12)
.map(|i| {
let t = i as f64 * 900.0;
sample(
t,
[
26_000_000.0 - 5.0 * t,
1_000_000.0 + 7.0 * t,
-3_000_000.0 - 2.0 * t,
],
Some(1.0e-6 + 1.0e-10 * t),
)
})
.collect();
PreciseEphemerisSamples::from_samples(samples).expect("valid source")
}
#[test]
fn self_comparison_is_zero_divergence() {
let source = source();
let epochs = [0.0, 450.0, 900.0, 1_350.0, 4_500.0, 9_900.0];
let reference: Vec<ReferenceState> = epochs
.iter()
.map(|&q| {
let state = source
.observable_state_at_j2000_s(gps(21), q)
.expect("covered query");
ReferenceState {
query_j2000_s: q,
position_ecef_m: state.position_ecef_m,
clock_s: state.clock_s,
}
})
.collect();
let report = compare_position_series(&source, gps(21), &reference).expect("comparison");
assert_eq!(report.per_epoch.len(), epochs.len());
assert_eq!(report.max_position_norm_m.to_bits(), 0.0f64.to_bits());
assert_eq!(report.rms_position_norm_m.to_bits(), 0.0f64.to_bits());
assert_eq!(report.max_abs_clock_diff_s, Some(0.0));
for d in &report.per_epoch {
assert_eq!(d.position_norm_m.to_bits(), 0.0f64.to_bits());
assert_eq!(d.clock_diff_s, Some(0.0));
}
}
#[test]
fn perturbed_reference_reports_the_offset() {
let source = source();
let q = 450.0;
let state = source
.observable_state_at_j2000_s(gps(21), q)
.expect("covered query");
let offset = [3.0, -4.0, 12.0]; let reference = [ReferenceState {
query_j2000_s: q,
position_ecef_m: [
state.position_ecef_m[0] - offset[0],
state.position_ecef_m[1] - offset[1],
state.position_ecef_m[2] - offset[2],
],
clock_s: state.clock_s.map(|c| c - 5.0e-9),
}];
let report = compare_position_series(&source, gps(21), &reference).expect("comparison");
let d = report.per_epoch[0];
assert_eq!(d.position_diff_m, offset);
assert!((d.position_norm_m - 13.0).abs() < 1e-9);
assert_eq!(report.max_position_norm_m, d.position_norm_m);
assert!((report.max_abs_clock_diff_s.unwrap() - 5.0e-9).abs() < 1e-18);
}
#[test]
fn empty_reference_is_zeroed_report() {
let source = source();
let report = compare_position_series(&source, gps(21), &[]).expect("comparison");
assert!(report.per_epoch.is_empty());
assert_eq!(report.max_position_norm_m, 0.0);
assert_eq!(report.rms_position_norm_m, 0.0);
assert_eq!(report.max_abs_clock_diff_s, None);
}
#[test]
fn out_of_coverage_reference_epoch_errors() {
let source = source();
let reference = [ReferenceState {
query_j2000_s: 1_000_000.0,
position_ecef_m: [0.0; 3],
clock_s: None,
}];
assert!(compare_position_series(&source, gps(21), &reference).is_err());
}
}