use super::eval_cubic_spline_for_test as eval_spline;
const NODES: [f64; 9] = [
646_228_800.0,
646_229_700.0,
646_230_600.0,
646_231_500.0,
646_232_400.0,
646_233_300.0,
646_234_200.0,
646_235_100.0,
646_236_000.0,
];
const POS_KM: [[f64; 3]; 9] = [
[26560.0, 0.0, 12000.0],
[26527.106916090303, 1327.2467358292165, 12004.9],
[26427.910629784365, 2651.1755461397556, 12009.6],
[26262.659829981283, 3968.476798498796, 12014.1],
[26031.76830746338, 5275.857425916825, 12018.4],
[25735.813920634722, 6570.049157800129, 12022.5],
[25375.537151176093, 7847.81668892518, 12026.4],
[24951.839253226382, 9105.965766016789, 12030.1],
[24465.78000071663, 10341.351171717757, 12033.6],
];
const CLK_US: [f64; 9] = [
123.456789, 123.966789, 124.496789, 125.046789, 125.616789, 126.206789, 126.816789, 127.446789,
128.096789,
];
const KM_TO_M: f64 = 1_000.0;
const US_TO_S: f64 = 1.0e-6;
struct Case {
query: f64,
pos_m_bits: [u64; 3],
clk_s_bits: u64,
}
const CASES: [Case; 4] = [
Case {
query: 646_229_237.0,
pos_m_bits: [0x41795280309f6e2c, 0x4123ac631b0b7677, 0x4166e48c86718662],
clk_s_bits: 0x3f2036bf708e3a06,
},
Case {
query: 646_231_501.0,
pos_m_bits: [0x41790bba79e30898, 0x414e49c7c743418c, 0x4166ea431c70c434],
clk_s_bits: 0x3f2063e51559f4a5,
},
Case {
query: 646_235_222.5,
pos_m_bits: [0x4177bc7eb396b50d, 0x4161b110722753ff, 0x4166f24f84b74f04],
clk_s_bits: 0x3f20b755739c9dfc,
},
Case {
query: 646_236_300.0,
pos_m_bits: [0x41772a32b1eb52a0, 0x41647fd33e587e12, 0x4166f454471c71c6],
clk_s_bits: 0x3f20d1a25e72abaa,
},
];
fn col(axis: usize) -> [f64; 9] {
let mut out = [0.0; 9];
for (i, row) in POS_KM.iter().enumerate() {
out[i] = row[axis];
}
out
}
#[test]
fn small_node_count_special_cases() {
let v2 = eval_spline(&[0.0, 900.0], &[10.0, 17.5], 437.0);
assert_eq!(
v2.to_bits(),
0x402b488888888888,
"n==2 not 0-ULP: {:#018x}",
v2.to_bits()
);
let x3 = [0.0, 900.0, 1800.0];
let y3 = [10.0, 17.5, 9.0];
let v3a = eval_spline(&x3, &y3, 437.0);
let v3b = eval_spline(&x3, &y3, 1500.0);
let want_a = f64::from_bits(0x402f47adc1a12a3a);
let want_b = f64::from_bits(0x402b38e38e38e38d);
assert!(
(v3a - want_a).abs() <= 4.0 * f64::EPSILON * want_a.abs(),
"n==3 @437 off: got {} want {}",
v3a,
want_a
);
assert!(
(v3b - want_b).abs() <= 4.0 * f64::EPSILON * want_b.abs(),
"n==3 @1500 off: got {} want {}",
v3b,
want_b
);
}
fn fixture_sp3_file() -> String {
use std::fmt::Write;
let mut s = String::new();
s.push_str("#cP2020 6 24 0 0 0.00000000 9 ORBIT IGS14 FIT TST\n");
s.push_str("## 2111 432000.00000000 900.00000000 59024 0.0000000000000\n");
s.push_str("+ 1 G01 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n");
s.push_str("++ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n");
s.push_str("%c G cc GPS ccc cccc cccc cccc cccc ccccc ccccc ccccc ccccc\n");
s.push_str("%c cc cc ccc ccc cccc cccc cccc cccc ccccc ccccc ccccc ccccc\n");
s.push_str("%f 1.2500000 1.025000000 0.00000000000 0.000000000000000\n");
s.push_str("%f 0.0000000 0.000000000 0.00000000000 0.000000000000000\n");
s.push_str("%i 0 0 0 0 0 0 0 0 0\n");
s.push_str("%i 0 0 0 0 0 0 0 0 0\n");
let minutes = [0, 15, 30, 45, 60, 75, 90, 105, 120];
for (i, &min) in minutes.iter().enumerate() {
let hh = min / 60;
let mm = min % 60;
writeln!(s, "* 2020 6 24 {:>2} {:>2} 0.00000000", hh, mm).unwrap();
writeln!(
s,
"PG01{:>14.6}{:>14.6}{:>14.6}{:>14.6}",
POS_KM[i][0], POS_KM[i][1], POS_KM[i][2], CLK_US[i]
)
.unwrap();
}
s.push_str("EOF\n");
s
}
#[test]
fn public_position_api_end_to_end_matches_scipy() {
use crate::id::{GnssSatelliteId, GnssSystem};
use crate::sp3::Sp3;
use astrodynamics::time::model::{Instant, InstantRepr, JulianDateSplit, TimeScale};
let file = fixture_sp3_file();
let sp3 = Sp3::parse(file.as_bytes()).expect("parse");
let g01 = GnssSatelliteId::new(GnssSystem::Gps, 1);
let ep0 = sp3.epochs[0];
let secs0 = match ep0.repr {
InstantRepr::JulianDate(s) => {
((s.jd_whole - 2_451_545.0) * 86_400.0 + s.fraction * 86_400.0).floor()
}
_ => panic!("expected JD epoch"),
};
assert_eq!(secs0, NODES[0], "J2000-second node axis mismatch");
let query_secs = NODES[0] + 437.0;
let jd_whole = 2_451_545.0 + (query_secs / 86_400.0).floor();
let frac = (query_secs - (query_secs / 86_400.0).floor() * 86_400.0) / 86_400.0;
let q = Instant::from_julian_date(TimeScale::Gpst, JulianDateSplit::new(jd_whole, frac));
let state = sp3.position(g01, q).expect("position");
let parsed_km: Vec<[f64; 3]> = (0..9)
.map(|i| {
let st = sp3.state(g01, i).unwrap();
[
st.position.x_m / 1000.0,
st.position.y_m / 1000.0,
st.position.z_m / 1000.0,
]
})
.collect();
let x = NODES.to_vec();
let cx: Vec<f64> = parsed_km.iter().map(|r| r[0]).collect();
let cy: Vec<f64> = parsed_km.iter().map(|r| r[1]).collect();
let cz: Vec<f64> = parsed_km.iter().map(|r| r[2]).collect();
let want_x = eval_spline(&x, &cx, query_secs) * 1000.0;
let want_y = eval_spline(&x, &cy, query_secs) * 1000.0;
let want_z = eval_spline(&x, &cz, query_secs) * 1000.0;
assert_eq!(state.position.x_m.to_bits(), want_x.to_bits());
assert_eq!(state.position.y_m.to_bits(), want_y.to_bits());
assert_eq!(state.position.z_m.to_bits(), want_z.to_bits());
assert!(state.clock_s.is_some(), "clock should interpolate");
}
#[test]
fn real_igs_sp3_interpolation_is_0_ulp_vs_gnssanalysis_scipy() {
use super::sp3_golden_data::GOLDEN_SATS;
let bits_to_f64 = f64::from_bits;
let mut checked = 0usize;
for sat in GOLDEN_SATS {
let n = sat.nodes_seconds_j2000.len();
assert_eq!(
sat.positions_km_bits.len(),
n,
"{} node/pos length",
sat.prn
);
assert_eq!(sat.clocks_us_bits.len(), n, "{} node/clk length", sat.prn);
let x: Vec<f64> = sat
.nodes_seconds_j2000
.iter()
.map(|&b| bits_to_f64(b))
.collect();
let cx: Vec<f64> = sat
.positions_km_bits
.iter()
.map(|r| bits_to_f64(r[0]))
.collect();
let cy: Vec<f64> = sat
.positions_km_bits
.iter()
.map(|r| bits_to_f64(r[1]))
.collect();
let cz: Vec<f64> = sat
.positions_km_bits
.iter()
.map(|r| bits_to_f64(r[2]))
.collect();
let clk: Vec<f64> = sat.clocks_us_bits.iter().map(|&b| bits_to_f64(b)).collect();
for case in sat.cases {
let q = bits_to_f64(case.query_seconds_j2000);
let x_m = eval_spline(&x, &cx, q) * KM_TO_M;
let y_m = eval_spline(&x, &cy, q) * KM_TO_M;
let z_m = eval_spline(&x, &cz, q) * KM_TO_M;
let c_s = eval_spline(&x, &clk, q) * US_TO_S;
assert_eq!(
x_m.to_bits(),
case.position_m_bits[0],
"{} X mismatch at q={}: got {:#018x} ({}), want {:#018x}",
sat.prn,
q,
x_m.to_bits(),
x_m,
case.position_m_bits[0]
);
assert_eq!(
y_m.to_bits(),
case.position_m_bits[1],
"{} Y mismatch at q={}: got {:#018x} ({}), want {:#018x}",
sat.prn,
q,
y_m.to_bits(),
y_m,
case.position_m_bits[1]
);
assert_eq!(
z_m.to_bits(),
case.position_m_bits[2],
"{} Z mismatch at q={}: got {:#018x} ({}), want {:#018x}",
sat.prn,
q,
z_m.to_bits(),
z_m,
case.position_m_bits[2]
);
assert_eq!(
c_s.to_bits(),
case.clock_s_bits,
"{} clock mismatch at q={}: got {:#018x} ({}), want {:#018x}",
sat.prn,
q,
c_s.to_bits(),
c_s,
case.clock_s_bits
);
checked += 1;
}
}
assert!(
checked >= 15,
"expected >= 15 real-file cases, checked {}",
checked
);
}
#[test]
fn real_igs_file_through_parse_is_0_ulp_end_to_end() {
use super::super::Sp3;
use super::instant_to_j2000_seconds_for_test as inst_secs;
use super::sp3_golden_data::GOLDEN_SATS;
use crate::id::{GnssSatelliteId, GnssSystem};
use astrodynamics::time::model::{Instant, JulianDateSplit, TimeScale};
let path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/sp3/GRG0MGXFIN_20201760000_01D_15M_ORB.SP3"
);
let bytes =
std::fs::read(path).unwrap_or_else(|e| panic!("read real IGS SP3 fixture {path}: {e}"));
let sp3 = Sp3::parse(&bytes).expect("parse real IGS SP3");
let bits = f64::from_bits;
let mut checked = 0usize;
for sat in GOLDEN_SATS {
let prn: u8 = sat.prn[1..].parse().expect("golden prn");
let id = GnssSatelliteId::new(GnssSystem::Gps, prn);
let n = sat.nodes_seconds_j2000.len();
let mut secs = Vec::with_capacity(n);
let mut kx = Vec::with_capacity(n);
let mut ky = Vec::with_capacity(n);
let mut kz = Vec::with_capacity(n);
for (idx, ep) in sp3.epochs.iter().enumerate() {
let (km, _clk_us) = match sp3.interp_node_for_test(id, idx) {
Some(node) => node,
None => continue,
};
secs.push(inst_secs(ep).expect("epoch -> J2000 sec"));
kx.push(km[0]);
ky.push(km[1]);
kz.push(km[2]);
}
assert_eq!(
secs.len(),
n,
"{}: parser yielded {} position nodes, golden has {}",
sat.prn,
secs.len(),
n
);
for i in 0..n {
assert_eq!(
secs[i].to_bits(),
sat.nodes_seconds_j2000[i],
"{} J2000-second node[{}]: parser {} vs golden {}",
sat.prn,
i,
secs[i],
bits(sat.nodes_seconds_j2000[i])
);
assert_eq!(
kx[i].to_bits(),
sat.positions_km_bits[i][0],
"{} km-X node[{}] after parse round-trip: got {:#018x} ({}), want {:#018x}",
sat.prn,
i,
kx[i].to_bits(),
kx[i],
sat.positions_km_bits[i][0]
);
assert_eq!(
ky[i].to_bits(),
sat.positions_km_bits[i][1],
"{} km-Y node[{}] after parse round-trip",
sat.prn,
i
);
assert_eq!(
kz[i].to_bits(),
sat.positions_km_bits[i][2],
"{} km-Z node[{}] after parse round-trip",
sat.prn,
i
);
}
for case in sat.cases {
let q = bits(case.query_seconds_j2000); let day = (q / 86_400.0).floor();
let rem = q - day * 86_400.0;
let epoch = Instant::from_julian_date(
TimeScale::Gpst,
JulianDateSplit::new(2_451_545.0 + day, rem / 86_400.0),
);
assert_eq!(
inst_secs(&epoch).unwrap(),
q,
"{}: constructed query Instant does not reproduce golden second {} exactly",
sat.prn,
q
);
let state = sp3.position(id, epoch).expect("position via public API");
assert_eq!(
state.position.x_m.to_bits(),
case.position_m_bits[0],
"{} end-to-end X at q={}: got {:#018x} ({}), want {:#018x}",
sat.prn,
q,
state.position.x_m.to_bits(),
state.position.x_m,
case.position_m_bits[0]
);
assert_eq!(
state.position.y_m.to_bits(),
case.position_m_bits[1],
"{} end-to-end Y at q={}",
sat.prn,
q
);
assert_eq!(
state.position.z_m.to_bits(),
case.position_m_bits[2],
"{} end-to-end Z at q={}",
sat.prn,
q
);
assert_eq!(
state.clock_s.map(|c| c.to_bits()),
Some(case.clock_s_bits),
"{} end-to-end clock at q={}: got {:?}, want {:#018x}",
sat.prn,
q,
state.clock_s.map(|c| c.to_bits()),
case.clock_s_bits
);
checked += 1;
}
}
assert!(
checked >= 15,
"expected >= 15 end-to-end cases, checked {}",
checked
);
}
#[test]
fn position_clock_interpolation_is_0_ulp_vs_scipy() {
let x = NODES.to_vec();
let cx = col(0).to_vec();
let cy = col(1).to_vec();
let cz = col(2).to_vec();
let clk = CLK_US.to_vec();
for case in &CASES {
let x_m = eval_spline(&x, &cx, case.query) * KM_TO_M;
let y_m = eval_spline(&x, &cy, case.query) * KM_TO_M;
let z_m = eval_spline(&x, &cz, case.query) * KM_TO_M;
assert_eq!(
x_m.to_bits(),
case.pos_m_bits[0],
"X mismatch at q={}: got {:#018x} ({}), want {:#018x}",
case.query,
x_m.to_bits(),
x_m,
case.pos_m_bits[0]
);
assert_eq!(
y_m.to_bits(),
case.pos_m_bits[1],
"Y mismatch at q={}: got {:#018x} ({}), want {:#018x}",
case.query,
y_m.to_bits(),
y_m,
case.pos_m_bits[1]
);
assert_eq!(
z_m.to_bits(),
case.pos_m_bits[2],
"Z mismatch at q={}: got {:#018x} ({}), want {:#018x}",
case.query,
z_m.to_bits(),
z_m,
case.pos_m_bits[2]
);
let c_s = eval_spline(&x, &clk, case.query) * US_TO_S;
assert_eq!(
c_s.to_bits(),
case.clk_s_bits,
"clock mismatch at q={}: got {:#018x} ({}), want {:#018x}",
case.query,
c_s.to_bits(),
c_s,
case.clk_s_bits
);
}
}
#[test]
fn instant_to_j2000_seconds_query_exactness_bound() {
const D: f64 = 86_400.0;
const J2000_JD: f64 = 2_451_545.0;
fn jdn(y: i64, m: i64, d: i64) -> i64 {
let a = (14 - m) / 12;
let yy = y + 4800 - a;
let mm = m + 12 * a - 3;
d + (153 * mm + 2) / 5 + 365 * yy + yy / 4 - yy / 100 + yy / 400 - 32_045
}
let lo = jdn(1994, 1, 1);
let hi = jdn(2100, 1, 1);
assert!(
(hi as f64) < 2f64.powi(53),
"jdn must be exactly representable"
);
for jd in lo..=hi {
let jd_whole = jd as f64 - 0.5;
let days_whole = jd_whole - J2000_JD;
let midnight = days_whole * D;
assert_eq!(
midnight.fract(),
0.0,
"midnight term not integer for jdn {jd}"
);
let exact = (jd - 2_451_545) * 86_400 - 43_200;
assert_eq!(
midnight, exact as f64,
"midnight term != exact i64 for jdn {jd}"
);
}
const BOUND_S: f64 = 7.3e-12; let mut worst = 0.0f64;
for s in 0..86_400u32 {
let ds = s as f64;
let e = ((ds / D) * D - ds).abs();
if e > worst {
worst = e;
}
}
for grid in [2u32, 10, 100, 1000] {
for i in 0..(86_400 * grid) {
let ds = i as f64 / grid as f64;
let e = ((ds / D) * D - ds).abs();
if e > worst {
worst = e;
}
}
}
assert!(
worst <= BOUND_S,
"sub-second round-trip error {worst:e} s exceeds proven bound {BOUND_S:e} s"
);
}