1use std::collections::BTreeMap;
44
45use crate::astro::frames::orientation::EarthOrientation;
46use crate::astro::frames::transforms::FrameTransformError;
47use crate::astro::state::CartesianState;
48use crate::astro::time::model::{Instant, TimeScale};
49use crate::constants::{KM_TO_M, US_TO_S};
50use crate::id::GnssSatelliteId;
51use crate::observables::{ObservableEphemerisSource, ObservableState, ObservablesError};
52use crate::sp3::interp::{instant_to_j2000_seconds, interpolate_precise_state, PreciseSatSeries};
53use crate::sp3::{Sp3, Sp3State};
54use crate::{Error, Result};
55
56#[derive(Debug, Clone, Copy, PartialEq)]
69pub struct PreciseEphemerisSample {
70 pub sat: GnssSatelliteId,
72 pub epoch: Instant,
74 pub position_ecef_m: [f64; 3],
76 pub clock_s: Option<f64>,
78 pub clock_event: bool,
82}
83
84impl PreciseEphemerisSample {
85 pub fn new(
90 sat: GnssSatelliteId,
91 epoch: Instant,
92 position_ecef_m: [f64; 3],
93 clock_s: Option<f64>,
94 ) -> Self {
95 Self {
96 sat,
97 epoch,
98 position_ecef_m,
99 clock_s,
100 clock_event: false,
101 }
102 }
103}
104
105#[derive(Debug, Clone, Copy, PartialEq)]
111pub struct PreciseEphemerisStateSample {
112 pub sat: GnssSatelliteId,
114 pub epoch: Instant,
116 pub position_ecef_m: [f64; 3],
118 pub velocity_ecef_m_s: [f64; 3],
120 pub clock_s: Option<f64>,
122 pub clock_rate_s_s: Option<f64>,
124 pub clock_event: bool,
126}
127
128impl PreciseEphemerisStateSample {
129 pub fn new(
131 sat: GnssSatelliteId,
132 epoch: Instant,
133 position_ecef_m: [f64; 3],
134 velocity_ecef_m_s: [f64; 3],
135 clock_s: Option<f64>,
136 clock_rate_s_s: Option<f64>,
137 ) -> Self {
138 Self {
139 sat,
140 epoch,
141 position_ecef_m,
142 velocity_ecef_m_s,
143 clock_s,
144 clock_rate_s_s,
145 clock_event: false,
146 }
147 }
148}
149
150pub fn sp3_ecef_state_to_eci(
156 sample: &PreciseEphemerisStateSample,
157 orientation: &EarthOrientation,
158) -> core::result::Result<CartesianState, FrameTransformError> {
159 validate_state_sample(sample)?;
160 let epoch_j2000_s = instant_to_j2000_seconds(&sample.epoch)
161 .ok_or_else(|| frame_error("epoch", "must be a split Julian date"))?;
162 if !epoch_j2000_s.is_finite() {
163 return Err(frame_error("epoch", "J2000 seconds must be finite"));
164 }
165 let position_itrf_km = [
166 sample.position_ecef_m[0] / KM_TO_M,
167 sample.position_ecef_m[1] / KM_TO_M,
168 sample.position_ecef_m[2] / KM_TO_M,
169 ];
170 let velocity_itrf_km_s = [
171 sample.velocity_ecef_m_s[0] / KM_TO_M,
172 sample.velocity_ecef_m_s[1] / KM_TO_M,
173 sample.velocity_ecef_m_s[2] / KM_TO_M,
174 ];
175 let (position_gcrf_km, velocity_gcrf_km_s) =
176 orientation.itrf_to_gcrf_state_km(position_itrf_km, velocity_itrf_km_s)?;
177 Ok(CartesianState::new(
178 epoch_j2000_s,
179 position_gcrf_km,
180 velocity_gcrf_km_s,
181 ))
182}
183
184#[derive(Debug, Clone, PartialEq, Eq)]
186pub enum PreciseSamplesError {
187 Empty,
189 SingleSampleSatellite(GnssSatelliteId),
191 NonMonotonicEpochs(GnssSatelliteId),
193 MixedTimeScales,
195 EpochNotRepresentable(GnssSatelliteId),
197 NonFiniteSample(GnssSatelliteId),
199}
200
201impl core::fmt::Display for PreciseSamplesError {
202 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
203 match self {
204 Self::Empty => write!(f, "no precise-ephemeris samples supplied"),
205 Self::SingleSampleSatellite(sat) => {
206 write!(f, "satellite {sat} has a single sample; need at least two")
207 }
208 Self::NonMonotonicEpochs(sat) => {
209 write!(
210 f,
211 "satellite {sat} sample epochs are not strictly increasing"
212 )
213 }
214 Self::MixedTimeScales => write!(f, "samples carry more than one time scale"),
215 Self::EpochNotRepresentable(sat) => {
216 write!(
217 f,
218 "satellite {sat} sample epoch is not representable as J2000 seconds"
219 )
220 }
221 Self::NonFiniteSample(sat) => write!(f, "satellite {sat} has a non-finite sample"),
222 }
223 }
224}
225
226impl std::error::Error for PreciseSamplesError {}
227
228#[derive(Debug, Clone, PartialEq)]
234pub struct PreciseEphemerisSamples {
235 time_scale: TimeScale,
236 nodes: BTreeMap<GnssSatelliteId, PreciseSatSeries>,
237}
238
239impl PreciseEphemerisSamples {
240 pub fn from_samples(
252 samples: impl IntoIterator<Item = PreciseEphemerisSample>,
253 ) -> core::result::Result<Self, PreciseSamplesError> {
254 let mut time_scale: Option<TimeScale> = None;
255 let mut grouped: BTreeMap<GnssSatelliteId, PreciseSatSeries> = BTreeMap::new();
256
257 for sample in samples {
258 match time_scale {
259 None => time_scale = Some(sample.epoch.scale),
260 Some(scale) if scale == sample.epoch.scale => {}
261 Some(_) => return Err(PreciseSamplesError::MixedTimeScales),
262 }
263
264 if !sample.position_ecef_m.iter().all(|c| c.is_finite())
265 || sample.clock_s.is_some_and(|c| !c.is_finite())
266 {
267 return Err(PreciseSamplesError::NonFiniteSample(sample.sat));
268 }
269
270 let seconds = instant_to_j2000_seconds(&sample.epoch)
277 .ok_or(PreciseSamplesError::EpochNotRepresentable(sample.sat))?;
278 if !seconds.is_finite() {
279 return Err(PreciseSamplesError::EpochNotRepresentable(sample.sat));
280 }
281 let xi = seconds.floor();
282
283 let series = grouped
287 .entry(sample.sat)
288 .or_insert_with(PreciseSatSeries::new);
289 series.x.push(xi);
290 series.kx.push(sample.position_ecef_m[0] / KM_TO_M);
291 series.ky.push(sample.position_ecef_m[1] / KM_TO_M);
292 series.kz.push(sample.position_ecef_m[2] / KM_TO_M);
293 if let Some(clock_s) = sample.clock_s {
294 let clock_us = clock_s / US_TO_S;
299 if !clock_us.is_finite() {
300 return Err(PreciseSamplesError::NonFiniteSample(sample.sat));
301 }
302 series.clk.push((xi, clock_us, sample.clock_event));
306 }
307 }
308
309 if grouped.is_empty() {
310 return Err(PreciseSamplesError::Empty);
311 }
312 for (&sat, series) in &grouped {
313 if series.x.len() < 2 {
314 return Err(PreciseSamplesError::SingleSampleSatellite(sat));
315 }
316 if series.x.windows(2).any(|w| w[1] <= w[0]) {
317 return Err(PreciseSamplesError::NonMonotonicEpochs(sat));
318 }
319 }
320
321 Ok(Self {
322 time_scale: time_scale.expect("non-empty group has a time scale"),
323 nodes: grouped,
324 })
325 }
326
327 pub fn time_scale(&self) -> TimeScale {
329 self.time_scale
330 }
331
332 pub fn satellites(&self) -> impl Iterator<Item = GnssSatelliteId> + '_ {
334 self.nodes.keys().copied()
335 }
336
337 pub(super) fn node_series(&self) -> &BTreeMap<GnssSatelliteId, PreciseSatSeries> {
338 &self.nodes
339 }
340
341 pub fn position_at_j2000_seconds(&self, sat: GnssSatelliteId, query: f64) -> Result<Sp3State> {
348 static EMPTY_F64: [f64; 0] = [];
354 static EMPTY_CLK: [(f64, f64, bool); 0] = [];
355 match self.nodes.get(&sat) {
356 Some(series) => interpolate_precise_state(
357 sat,
358 &series.x,
359 &series.kx,
360 &series.ky,
361 &series.kz,
362 &series.clk,
363 query,
364 ),
365 None => interpolate_precise_state(
366 sat, &EMPTY_F64, &EMPTY_F64, &EMPTY_F64, &EMPTY_F64, &EMPTY_CLK, query,
367 ),
368 }
369 }
370
371 pub fn position(&self, sat: GnssSatelliteId, epoch: Instant) -> Result<Sp3State> {
375 if epoch.scale != self.time_scale {
376 return Err(Error::InvalidInput(format!(
377 "precise-sample query time scale {} does not match source time scale {}",
378 epoch.scale.abbrev(),
379 self.time_scale.abbrev()
380 )));
381 }
382 let query = instant_to_j2000_seconds(&epoch).ok_or(Error::EpochOutOfRange)?;
383 self.position_at_j2000_seconds(sat, query)
384 }
385}
386
387impl ObservableEphemerisSource for PreciseEphemerisSamples {
388 fn observable_state_at_j2000_s(
389 &self,
390 sat: GnssSatelliteId,
391 t_j2000_s: f64,
392 ) -> core::result::Result<ObservableState, ObservablesError> {
393 let state = self
394 .position_at_j2000_seconds(sat, t_j2000_s)
395 .map_err(ObservablesError::Ephemeris)?;
396 Ok(ObservableState {
397 position_ecef_m: state.position.as_array(),
398 clock_s: state.clock_s,
399 })
400 }
401}
402
403impl Sp3 {
404 pub fn precise_ephemeris_samples(&self) -> Vec<PreciseEphemerisSample> {
412 let mut out = Vec::new();
413 for (idx, &epoch) in self.epochs.iter().enumerate() {
414 if let Ok(states) = self.states_at(idx) {
415 for (&sat, state) in states {
416 out.push(PreciseEphemerisSample {
417 sat,
418 epoch,
419 position_ecef_m: state.position.as_array(),
420 clock_s: state.clock_s,
421 clock_event: state.flags.clock_event,
422 });
423 }
424 }
425 }
426 out
427 }
428
429 pub fn precise_ephemeris_state_samples(&self) -> Vec<PreciseEphemerisStateSample> {
435 let mut out = Vec::new();
436 for (idx, &epoch) in self.epochs.iter().enumerate() {
437 if let Ok(states) = self.states_at(idx) {
438 for (&sat, state) in states {
439 if let Some(velocity) = state.velocity {
440 out.push(PreciseEphemerisStateSample {
441 sat,
442 epoch,
443 position_ecef_m: state.position.as_array(),
444 velocity_ecef_m_s: velocity.as_array(),
445 clock_s: state.clock_s,
446 clock_rate_s_s: state.clock_rate_s_s,
447 clock_event: state.flags.clock_event,
448 });
449 }
450 }
451 }
452 }
453 out
454 }
455}
456
457fn validate_state_sample(
458 sample: &PreciseEphemerisStateSample,
459) -> core::result::Result<(), FrameTransformError> {
460 if !sample.position_ecef_m.iter().all(|value| value.is_finite()) {
461 return Err(frame_error("position_ecef_m", "components must be finite"));
462 }
463 if !sample
464 .velocity_ecef_m_s
465 .iter()
466 .all(|value| value.is_finite())
467 {
468 return Err(frame_error(
469 "velocity_ecef_m_s",
470 "components must be finite",
471 ));
472 }
473 if sample.clock_s.is_some_and(|value| !value.is_finite()) {
474 return Err(frame_error("clock_s", "must be finite"));
475 }
476 if sample
477 .clock_rate_s_s
478 .is_some_and(|value| !value.is_finite())
479 {
480 return Err(frame_error("clock_rate_s_s", "must be finite"));
481 }
482 Ok(())
483}
484
485fn frame_error(field: &'static str, reason: &'static str) -> FrameTransformError {
486 FrameTransformError::InvalidInput { field, reason }
487}
488
489#[cfg(test)]
490mod tests {
491 use super::*;
492 use crate::astro::time::model::{InstantRepr, JulianDateSplit};
493 use crate::constants::SECONDS_PER_DAY;
494 use crate::GnssSystem;
495
496 const J2000_JD_WHOLE: f64 = 2_451_545.0;
497
498 fn gps(prn: u8) -> GnssSatelliteId {
499 GnssSatelliteId::new(GnssSystem::Gps, prn).expect("valid satellite id")
500 }
501
502 fn sample(
503 scale: TimeScale,
504 j2000_s: f64,
505 prn: u8,
506 pos: [f64; 3],
507 clk: Option<f64>,
508 ) -> PreciseEphemerisSample {
509 let split =
510 JulianDateSplit::new(J2000_JD_WHOLE, j2000_s / SECONDS_PER_DAY).expect("valid split");
511 PreciseEphemerisSample::new(
512 gps(prn),
513 Instant {
514 scale,
515 repr: InstantRepr::JulianDate(split),
516 },
517 pos,
518 clk,
519 )
520 }
521
522 #[test]
523 fn from_samples_rejects_empty() {
524 let err = PreciseEphemerisSamples::from_samples(std::iter::empty())
525 .expect_err("empty sample set must fail");
526 assert_eq!(err, PreciseSamplesError::Empty);
527 }
528
529 #[test]
530 fn from_samples_rejects_single_sample_satellite() {
531 let samples = vec![sample(
532 TimeScale::Gpst,
533 0.0,
534 21,
535 [20_000_000.0, 14_000_000.0, 21_000_000.0],
536 Some(1.0e-6),
537 )];
538 let err =
539 PreciseEphemerisSamples::from_samples(samples).expect_err("single sample must fail");
540 assert_eq!(err, PreciseSamplesError::SingleSampleSatellite(gps(21)));
541 }
542
543 #[test]
544 fn from_samples_rejects_non_monotonic_epochs() {
545 let samples = vec![
546 sample(TimeScale::Gpst, 900.0, 21, [1.0e7, 2.0e7, 3.0e7], None),
547 sample(TimeScale::Gpst, 900.0, 21, [1.0e7, 2.0e7, 3.0e7], None),
548 ];
549 let err = PreciseEphemerisSamples::from_samples(samples)
550 .expect_err("repeated epoch must fail as non-monotonic");
551 assert_eq!(err, PreciseSamplesError::NonMonotonicEpochs(gps(21)));
552
553 let descending = vec![
554 sample(TimeScale::Gpst, 1_800.0, 7, [1.0e7, 2.0e7, 3.0e7], None),
555 sample(TimeScale::Gpst, 900.0, 7, [1.1e7, 2.1e7, 3.1e7], None),
556 ];
557 let err = PreciseEphemerisSamples::from_samples(descending)
558 .expect_err("descending epochs must fail");
559 assert_eq!(err, PreciseSamplesError::NonMonotonicEpochs(gps(7)));
560 }
561
562 #[test]
563 fn from_samples_rejects_mixed_time_scales() {
564 let samples = vec![
565 sample(TimeScale::Gpst, 0.0, 21, [1.0e7, 2.0e7, 3.0e7], None),
566 sample(TimeScale::Utc, 900.0, 21, [1.0e7, 2.0e7, 3.0e7], None),
567 ];
568 let err = PreciseEphemerisSamples::from_samples(samples)
569 .expect_err("mixed time scales must fail");
570 assert_eq!(err, PreciseSamplesError::MixedTimeScales);
571 }
572
573 #[test]
574 fn from_samples_rejects_non_finite_sample() {
575 let samples = vec![
576 sample(TimeScale::Gpst, 0.0, 21, [f64::NAN, 2.0e7, 3.0e7], None),
577 sample(TimeScale::Gpst, 900.0, 21, [1.0e7, 2.0e7, 3.0e7], None),
578 ];
579 let err = PreciseEphemerisSamples::from_samples(samples).expect_err("non-finite must fail");
580 assert_eq!(err, PreciseSamplesError::NonFiniteSample(gps(21)));
581 }
582
583 #[test]
584 fn from_samples_rejects_epoch_with_non_finite_j2000_seconds() {
585 let split = JulianDateSplit::new(f64::MAX, 0.0).expect("finite split Julian date");
586 let samples = vec![PreciseEphemerisSample::new(
587 gps(21),
588 Instant {
589 scale: TimeScale::Gpst,
590 repr: InstantRepr::JulianDate(split),
591 },
592 [1.0e7, 2.0e7, 3.0e7],
593 None,
594 )];
595 let err = PreciseEphemerisSamples::from_samples(samples)
596 .expect_err("non-finite J2000 seconds must fail");
597 assert_eq!(err, PreciseSamplesError::EpochNotRepresentable(gps(21)));
598 }
599
600 #[test]
601 fn from_samples_rejects_clock_that_overflows_native_microseconds() {
602 let samples = vec![
603 sample(
604 TimeScale::Gpst,
605 0.0,
606 21,
607 [1.0e7, 2.0e7, 3.0e7],
608 Some(f64::MAX),
609 ),
610 sample(TimeScale::Gpst, 900.0, 21, [1.1e7, 2.1e7, 3.1e7], None),
611 ];
612 let err = PreciseEphemerisSamples::from_samples(samples)
613 .expect_err("overflowed native clock must fail");
614 assert_eq!(err, PreciseSamplesError::NonFiniteSample(gps(21)));
615 }
616
617 #[test]
618 fn from_samples_out_of_range_query_errors() {
619 let samples = vec![
620 sample(
621 TimeScale::Gpst,
622 0.0,
623 21,
624 [2.0e7, 1.4e7, 2.1e7],
625 Some(1.0e-6),
626 ),
627 sample(
628 TimeScale::Gpst,
629 900.0,
630 21,
631 [2.0e7, 1.4e7, 2.1e7],
632 Some(1.0e-6),
633 ),
634 ];
635 let source = PreciseEphemerisSamples::from_samples(samples).expect("valid source");
636 let err = source
639 .position_at_j2000_seconds(gps(21), 1_000_000.0)
640 .expect_err("out-of-coverage query must fail");
641 assert_eq!(err, Error::EpochOutOfRange);
642 }
643
644 #[test]
645 fn unknown_sat_with_non_finite_query_is_invalid_input() {
646 let samples = vec![
647 sample(
648 TimeScale::Gpst,
649 0.0,
650 21,
651 [2.0e7, 1.4e7, 2.1e7],
652 Some(1.0e-6),
653 ),
654 sample(
655 TimeScale::Gpst,
656 900.0,
657 21,
658 [2.0e7, 1.4e7, 2.1e7],
659 Some(1.0e-6),
660 ),
661 ];
662 let source = PreciseEphemerisSamples::from_samples(samples).expect("valid source");
663
664 let err = source
668 .position_at_j2000_seconds(gps(7), f64::NAN)
669 .expect_err("non-finite query on unknown sat must fail");
670 assert!(
671 matches!(err, Error::InvalidInput(_)),
672 "expected InvalidInput, got {err:?}"
673 );
674
675 let err = source
677 .position_at_j2000_seconds(gps(7), 0.0)
678 .expect_err("finite query on unknown sat must fail");
679 assert_eq!(err, Error::UnknownSatellite(gps(7)));
680 }
681}
682
683#[cfg(all(test, sidereon_repo_tests))]
684mod parity_tests {
685 use super::*;
686 use crate::observables::{
687 predict, predict_ranges, PredictOptions, RangePrediction, RangePredictionRequest,
688 };
689 use crate::GnssSystem;
690
691 fn gps(prn: u8) -> GnssSatelliteId {
692 GnssSatelliteId::new(GnssSystem::Gps, prn).expect("valid satellite id")
693 }
694
695 fn round_trip_safe_km(km: f64) -> bool {
696 (km * KM_TO_M) / KM_TO_M == km
697 }
698 fn round_trip_safe_us(us: f64) -> bool {
699 (us * US_TO_S) / US_TO_S == us
700 }
701
702 fn authored_sp3() -> Sp3 {
706 let header_src = concat!(
707 env!("CARGO_MANIFEST_DIR"),
708 "/tests/fixtures/sp3/GAP_G01_20201760000_15M.sp3"
709 );
710 let gap = std::fs::read_to_string(header_src).expect("read header fixture");
711 let epoch_start = gap.find("\n* ").expect("first epoch line") + 1;
712 let header = &gap[..epoch_start];
713
714 let xs = [
717 26_000.0, 25_990.0, 25_960.0, 25_910.0, 25_840.0, 25_750.0, 25_640.0, 25_510.0,
718 25_360.0, 25_190.0, 25_000.0, 24_790.0,
719 ];
720 let ys = [
721 1_000.0, 2_000.0, 2_990.0, 3_960.0, 4_910.0, 5_840.0, 6_750.0, 7_640.0, 8_510.0,
722 9_360.0, 10_190.0, 11_000.0,
723 ];
724 let zs = [
725 -3_000.0, -3_050.0, -3_120.0, -3_210.0, -3_320.0, -3_450.0, -3_600.0, -3_770.0,
726 -3_960.0, -4_170.0, -4_400.0, -4_650.0,
727 ];
728 let cs = [
729 100.0, 142.0, -313.0, 6_159.0, 1_234.0, -884.0, 401.0, 862.0, -606.0, 10.0, -369.0,
730 3_654.0,
731 ];
732
733 let mut text = String::from(header);
734 for i in 0..xs.len() {
735 assert!(round_trip_safe_km(xs[i]), "xs[{i}] not round-trip-safe");
736 assert!(round_trip_safe_km(ys[i]), "ys[{i}] not round-trip-safe");
737 assert!(round_trip_safe_km(zs[i]), "zs[{i}] not round-trip-safe");
738 assert!(round_trip_safe_us(cs[i]), "cs[{i}] not round-trip-safe");
739 let total_min = i * 15;
740 let hour = total_min / 60;
741 let minute = total_min % 60;
742 text.push_str(&format!("* 2020 6 24 {hour:2} {minute:2} 0.00000000\n"));
743 text.push_str(&format!(
744 "PG01{:14.6}{:14.6}{:14.6}{:14.6}\n",
745 xs[i], ys[i], zs[i], cs[i]
746 ));
747 }
748 text.push_str("EOF\n");
749 Sp3::parse(text.as_bytes()).expect("parse authored SP3")
750 }
751
752 fn authored_sp3_with_clock_event(event_idx: usize) -> Sp3 {
757 let header_src = concat!(
758 env!("CARGO_MANIFEST_DIR"),
759 "/tests/fixtures/sp3/GAP_G01_20201760000_15M.sp3"
760 );
761 let gap = std::fs::read_to_string(header_src).expect("read header fixture");
762 let epoch_start = gap.find("\n* ").expect("first epoch line") + 1;
763 let header = &gap[..epoch_start];
764
765 let xs = [
766 26_000.0, 25_990.0, 25_960.0, 25_910.0, 25_840.0, 25_750.0, 25_640.0, 25_510.0,
767 25_360.0, 25_190.0, 25_000.0, 24_790.0,
768 ];
769 let ys = [
770 1_000.0, 2_000.0, 2_990.0, 3_960.0, 4_910.0, 5_840.0, 6_750.0, 7_640.0, 8_510.0,
771 9_360.0, 10_190.0, 11_000.0,
772 ];
773 let zs = [
774 -3_000.0, -3_050.0, -3_120.0, -3_210.0, -3_320.0, -3_450.0, -3_600.0, -3_770.0,
775 -3_960.0, -4_170.0, -4_400.0, -4_650.0,
776 ];
777 let cs = [
780 100.0, 142.0, 180.0, 210.0, 235.0, 260.0, -7_500.0, -7_550.0, -7_680.0, -7_790.0,
781 -7_880.0, -7_000.0,
782 ];
783
784 let mut text = String::from(header);
785 for i in 0..xs.len() {
786 assert!(round_trip_safe_km(xs[i]), "xs[{i}] not round-trip-safe");
787 assert!(round_trip_safe_km(ys[i]), "ys[{i}] not round-trip-safe");
788 assert!(round_trip_safe_km(zs[i]), "zs[{i}] not round-trip-safe");
789 assert!(round_trip_safe_us(cs[i]), "cs[{i}] not round-trip-safe");
790 let total_min = i * 15;
791 let hour = total_min / 60;
792 let minute = total_min % 60;
793 text.push_str(&format!("* 2020 6 24 {hour:2} {minute:2} 0.00000000\n"));
794 let mut record = format!(
796 "PG01{:14.6}{:14.6}{:14.6}{:14.6}",
797 xs[i], ys[i], zs[i], cs[i]
798 );
799 if i == event_idx {
800 while record.len() < 74 {
803 record.push(' ');
804 }
805 record.push('E');
806 }
807 record.push('\n');
808 text.push_str(&record);
809 }
810 text.push_str("EOF\n");
811 let sp3 = Sp3::parse(text.as_bytes()).expect("parse authored SP3");
812 let state = sp3.state(gps(1), event_idx).expect("event-epoch state");
814 assert!(
815 state.flags.clock_event,
816 "authored E flag did not parse at epoch {event_idx}"
817 );
818 sp3
819 }
820
821 #[test]
826 fn from_samples_preserves_clock_event_arc_split() {
827 let event_idx = 6usize;
828 let sp3 = authored_sp3_with_clock_event(event_idx);
829 let extracted = sp3.precise_ephemeris_samples();
830 assert!(
832 extracted.iter().any(|s| s.sat == gps(1) && s.clock_event),
833 "extracted samples dropped the clock-event flag"
834 );
835 let samples = PreciseEphemerisSamples::from_samples(extracted).expect("source");
836
837 let epochs = sp3.epochs_j2000_seconds();
838 assert!(epochs.len() > event_idx + 1);
839
840 let mut queries = Vec::new();
842 for w in epochs.windows(2) {
843 queries.push(w[0]);
844 queries.push(0.5 * (w[0] + w[1]));
845 }
846 queries.push(*epochs.last().unwrap());
847
848 let mut saw_some_clock = false;
849 for &q in &queries {
850 let a = sp3.position_at_j2000_seconds(gps(1), q).expect("sp3 state");
851 let b = samples
852 .position_at_j2000_seconds(gps(1), q)
853 .expect("samples state");
854 assert_eq!(
855 a.clock_s.map(f64::to_bits),
856 b.clock_s.map(f64::to_bits),
857 "clock bits differ at query {q} across the reset"
858 );
859 if a.clock_s.is_some() {
860 saw_some_clock = true;
861 }
862 }
863 assert!(saw_some_clock, "expected clock estimates across the grid");
864
865 let reset_epoch = epochs[event_idx];
870 let just_after = 0.5 * (epochs[event_idx] + epochs[event_idx + 1]);
871 let clk_after = sp3
872 .position_at_j2000_seconds(gps(1), just_after)
873 .expect("state after reset")
874 .clock_s
875 .expect("clock after reset");
876 assert!(
880 clk_after < -1.0e-3,
881 "post-reset clock {clk_after:e} s is not on the post-reset sub-arc; \
882 the arc split was not applied"
883 );
884 let _ = reset_epoch;
885 }
886
887 fn assert_state_bits_eq(a: &Sp3State, b: &Sp3State) {
888 assert_eq!(
889 a.position.as_array().map(f64::to_bits),
890 b.position.as_array().map(f64::to_bits),
891 "position bits differ"
892 );
893 assert_eq!(
894 a.clock_s.map(f64::to_bits),
895 b.clock_s.map(f64::to_bits),
896 "clock bits differ"
897 );
898 }
899
900 #[test]
901 fn from_samples_is_byte_identical_to_parsed_sp3() {
902 let sp3 = authored_sp3();
903 let samples =
904 PreciseEphemerisSamples::from_samples(sp3.precise_ephemeris_samples()).expect("source");
905
906 let epochs = sp3.epochs_j2000_seconds();
907 assert!(epochs.len() >= 4);
908
909 let mut queries = Vec::new();
911 for w in epochs.windows(2) {
912 queries.push(w[0]);
913 queries.push(0.5 * (w[0] + w[1]));
914 queries.push(0.75 * w[0] + 0.25 * w[1]);
915 }
916 queries.push(*epochs.last().unwrap());
917
918 for &q in &queries {
920 let a = sp3.position_at_j2000_seconds(gps(1), q).expect("sp3 state");
921 let b = samples
922 .position_at_j2000_seconds(gps(1), q)
923 .expect("samples state");
924 assert_state_bits_eq(&a, &b);
925 }
926
927 let receivers = [
929 [4_027_894.0, 307_046.0, 4_919_474.0],
930 [1_130_000.0, -4_830_000.0, 3_994_000.0],
931 [-2_700_000.0, -4_290_000.0, 3_855_000.0],
932 ];
933 let options = PredictOptions::default();
934 for &q in &queries {
935 for rx in receivers {
936 let requests = [RangePredictionRequest {
937 sat: gps(1),
938 receiver_ecef_m: rx,
939 t_rx_j2000_s: q,
940 }];
941 let mut a = [RangePrediction {
942 geometric_range_m: 0.0,
943 sat_clock_s: None,
944 transmit_time_j2000_s: 0.0,
945 sat_pos_ecef_m: [0.0; 3],
946 }; 1];
947 let mut b = a;
948 predict_ranges(&sp3, &requests, options, &mut a).expect("sp3 ranges");
949 predict_ranges(&samples, &requests, options, &mut b).expect("sample ranges");
950 assert_eq!(
951 a[0].geometric_range_m.to_bits(),
952 b[0].geometric_range_m.to_bits()
953 );
954 assert_eq!(
955 a[0].transmit_time_j2000_s.to_bits(),
956 b[0].transmit_time_j2000_s.to_bits()
957 );
958 assert_eq!(
959 a[0].sat_clock_s.map(f64::to_bits),
960 b[0].sat_clock_s.map(f64::to_bits)
961 );
962 assert_eq!(
963 a[0].sat_pos_ecef_m.map(f64::to_bits),
964 b[0].sat_pos_ecef_m.map(f64::to_bits)
965 );
966
967 let oa = predict(&sp3, gps(1), rx, q, options).expect("sp3 predict");
969 let ob = predict(&samples, gps(1), rx, q, options).expect("samples predict");
970 assert_eq!(
971 oa.geometric_range_m.to_bits(),
972 ob.geometric_range_m.to_bits()
973 );
974 assert_eq!(oa.doppler_hz.to_bits(), ob.doppler_hz.to_bits());
975 }
976 }
977 }
978
979 #[test]
980 fn from_samples_tracks_real_fixture_to_sub_micron() {
981 let path = concat!(
987 env!("CARGO_MANIFEST_DIR"),
988 "/tests/fixtures/sp3/GRG0MGXFIN_20201760000_01D_15M_ORB.SP3"
989 );
990 let bytes = std::fs::read(path).expect("read fixture");
991 let sp3 = Sp3::parse(&bytes).expect("parse fixture");
992 let samples =
993 PreciseEphemerisSamples::from_samples(sp3.precise_ephemeris_samples()).expect("source");
994
995 let epochs = sp3.epochs_j2000_seconds();
996 let sats: Vec<_> = sp3.satellites().to_vec();
997 let mut compared = 0u64;
998 let mut byte_identical = 0u64;
999 let mut max_abs_diff_m = 0.0f64;
1000
1001 for &sat in sats.iter().take(20) {
1002 for w in epochs.windows(2) {
1003 for q in [w[0], 0.5 * (w[0] + w[1])] {
1004 let (Ok(a), Ok(b)) = (
1005 sp3.position_at_j2000_seconds(sat, q),
1006 samples.position_at_j2000_seconds(sat, q),
1007 ) else {
1008 continue;
1009 };
1010 let pa = a.position.as_array();
1011 let pb = b.position.as_array();
1012 for k in 0..3 {
1013 compared += 1;
1014 if pa[k].to_bits() == pb[k].to_bits() {
1015 byte_identical += 1;
1016 }
1017 max_abs_diff_m = max_abs_diff_m.max((pa[k] - pb[k]).abs());
1018 }
1019 }
1020 }
1021 }
1022
1023 assert!(compared > 0);
1024 assert!(
1025 max_abs_diff_m < 1.0e-6,
1026 "max divergence {max_abs_diff_m:e} m exceeds sub-micron bound"
1027 );
1028 assert!(
1029 byte_identical * 100 >= compared * 90,
1030 "expected the vast majority byte-identical, got {byte_identical}/{compared}"
1031 );
1032 }
1033}