1use crate::astro::frames::transforms::itrs_to_geodetic_compute;
9use std::f64::consts::PI;
10
11use crate::astro::time::civil;
12use crate::constants::{
13 C_M_S, DEGREES_PER_CIRCLE, DEGREES_PER_SEMICIRCLE, F_L1_HZ, KM_TO_M, MICROSECONDS_PER_SECOND,
14 OBSERVABLE_TRANSMIT_TIME_ITERATIONS, OMEGA_E_DOT_RAD_S,
15};
16use crate::ephemeris::BroadcastEphemeris;
17use crate::estimation::recipe::SagnacRecipe;
18use crate::id::GnssSatelliteId;
19use crate::sp3::Sp3;
20use crate::spp::EphemerisSource;
21use crate::validate;
22use crate::Error;
23
24const FD_HALF_S: f64 = 0.5;
25
26#[derive(Debug, Clone, Copy, PartialEq)]
28pub struct ObservableState {
29 pub position_ecef_m: [f64; 3],
31 pub clock_s: Option<f64>,
33}
34
35pub trait ObservableEphemerisSource {
37 fn observable_state_at_j2000_s(
39 &self,
40 sat: GnssSatelliteId,
41 t_j2000_s: f64,
42 ) -> Result<ObservableState, ObservablesError>;
43}
44
45impl ObservableEphemerisSource for Sp3 {
46 fn observable_state_at_j2000_s(
47 &self,
48 sat: GnssSatelliteId,
49 t_j2000_s: f64,
50 ) -> Result<ObservableState, ObservablesError> {
51 let state = self
52 .position_at_j2000_seconds(sat, t_j2000_s)
53 .map_err(ObservablesError::Ephemeris)?;
54 Ok(ObservableState {
55 position_ecef_m: state.position.as_array(),
56 clock_s: state.clock_s,
57 })
58 }
59}
60
61impl ObservableEphemerisSource for BroadcastEphemeris {
62 fn observable_state_at_j2000_s(
63 &self,
64 sat: GnssSatelliteId,
65 t_j2000_s: f64,
66 ) -> Result<ObservableState, ObservablesError> {
67 let Some((position_ecef_m, clock_s)) =
68 EphemerisSource::position_clock_at_j2000_s(self, sat, t_j2000_s)
69 else {
70 return Err(ObservablesError::NoEphemeris);
71 };
72 Ok(ObservableState {
73 position_ecef_m,
74 clock_s: Some(clock_s),
75 })
76 }
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub enum ObservablesInputErrorKind {
82 NonFinite,
84 NotPositive,
86 Negative,
88 OutOfRange,
90 Missing,
92 FloatParse,
94 IntParse,
96 InvalidCivilDate,
98 InvalidCivilTime,
100}
101
102impl core::fmt::Display for ObservablesInputErrorKind {
103 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
104 let label = match self {
105 Self::NonFinite => "not finite",
106 Self::NotPositive => "not positive",
107 Self::Negative => "negative",
108 Self::OutOfRange => "out of range",
109 Self::Missing => "missing",
110 Self::FloatParse => "invalid float",
111 Self::IntParse => "invalid integer",
112 Self::InvalidCivilDate => "invalid civil date",
113 Self::InvalidCivilTime => "invalid civil time",
114 };
115 f.write_str(label)
116 }
117}
118
119impl From<&validate::FieldError> for ObservablesInputErrorKind {
120 fn from(error: &validate::FieldError) -> Self {
121 match error {
122 validate::FieldError::Missing { .. } => Self::Missing,
123 validate::FieldError::NonFinite { .. } => Self::NonFinite,
124 validate::FieldError::NotPositive { .. } => Self::NotPositive,
125 validate::FieldError::Negative { .. } => Self::Negative,
126 validate::FieldError::OutOfRange { .. } => Self::OutOfRange,
127 validate::FieldError::FloatParse { .. } => Self::FloatParse,
128 validate::FieldError::IntParse { .. } => Self::IntParse,
129 validate::FieldError::InvalidCivilDate { .. } => Self::InvalidCivilDate,
130 validate::FieldError::InvalidCivilTime { .. } => Self::InvalidCivilTime,
131 }
132 }
133}
134
135#[derive(Debug, Clone, PartialEq, Eq)]
137pub enum ObservablesError {
138 InvalidInput {
141 field: &'static str,
143 kind: ObservablesInputErrorKind,
145 },
146 NoEphemeris,
148 Ephemeris(Error),
150}
151
152impl core::fmt::Display for ObservablesError {
153 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
154 match self {
155 Self::InvalidInput { field, kind } => {
156 write!(f, "invalid observable input {field}: {kind}")
157 }
158 Self::NoEphemeris => write!(f, "no ephemeris"),
159 Self::Ephemeris(err) => write!(f, "{err}"),
160 }
161 }
162}
163
164impl std::error::Error for ObservablesError {}
165
166#[derive(Debug, Clone, Copy, PartialEq)]
168pub struct PredictOptions {
169 pub carrier_hz: f64,
171 pub light_time: bool,
173 pub sagnac: bool,
175}
176
177#[derive(Debug, Clone, Copy, PartialEq, Eq)]
179pub struct TransmitTimeOptions {
180 pub light_time: bool,
182 pub sagnac: bool,
184}
185
186impl Default for TransmitTimeOptions {
187 fn default() -> Self {
188 Self {
189 light_time: true,
190 sagnac: true,
191 }
192 }
193}
194
195impl Default for PredictOptions {
196 fn default() -> Self {
197 Self {
198 carrier_hz: F_L1_HZ,
199 light_time: true,
200 sagnac: true,
201 }
202 }
203}
204
205#[derive(Debug, Clone, Copy, PartialEq)]
213pub struct TransmitTimeSatelliteState {
214 pub signal_flight_time_s: f64,
216 pub transmit_offset_us: i64,
218 pub transmit_time_j2000_s: f64,
220 pub clock_s: Option<f64>,
222 pub transmit_position_ecef_m: [f64; 3],
224 pub position_ecef_m: [f64; 3],
226 pub velocity_m_s: [f64; 3],
228 pub geometric_range_m: f64,
230 pub los_unit: [f64; 3],
232}
233
234#[derive(Debug, Clone, Copy, PartialEq)]
236pub struct PredictedObservables {
237 pub geometric_range_m: f64,
239 pub range_rate_m_s: f64,
241 pub doppler_hz: f64,
243 pub sat_clock_s: Option<f64>,
245 pub elevation_deg: f64,
247 pub azimuth_deg: f64,
249 pub transmit_offset_us: i64,
251 pub transmit_time_j2000_s: f64,
253 pub los_unit: [f64; 3],
255 pub sat_pos_ecef_m: [f64; 3],
257 pub sat_velocity_m_s: [f64; 3],
259}
260
261pub fn j2000_seconds_from_split(jd_whole: f64, jd_fraction: f64) -> Result<f64, ObservablesError> {
263 validate::finite(jd_whole, "jd_whole").map_err(map_input_error)?;
264 validate::finite(jd_fraction, "jd_fraction").map_err(map_input_error)?;
265 validate::finite(
266 civil::j2000_seconds_from_split(jd_whole, jd_fraction),
267 "j2000_seconds",
268 )
269 .map_err(map_input_error)
270}
271
272pub fn transmit_time_satellite_state(
280 source: &dyn ObservableEphemerisSource,
281 sat: GnssSatelliteId,
282 receiver_ecef_m: [f64; 3],
283 t_rx_j2000_s: f64,
284 options: TransmitTimeOptions,
285) -> Result<TransmitTimeSatelliteState, ObservablesError> {
286 validate_transmit_time_inputs(receiver_ecef_m, t_rx_j2000_s)?;
287 let predict_options = PredictOptions {
288 carrier_hz: F_L1_HZ,
289 light_time: options.light_time,
290 sagnac: options.sagnac,
291 };
292 let solved = solve_transmit_time(source, sat, receiver_ecef_m, t_rx_j2000_s, predict_options)?;
293
294 let dx = solved.sat_rot_ecef_m[0] - receiver_ecef_m[0];
295 let dy = solved.sat_rot_ecef_m[1] - receiver_ecef_m[1];
296 let dz = solved.sat_rot_ecef_m[2] - receiver_ecef_m[2];
297 let range = geometric_range_m([dx, dy, dz])?;
298 let los = [dx / range, dy / range, dz / range];
299
300 let velocity = satellite_velocity(source, sat, solved.transmit_time_j2000_s)?;
301 let velocity_rot = sagnac_rotate(velocity, solved.tau_s, options.sagnac);
302 validate::finite_vec3(velocity_rot, "satellite velocity_m_s").map_err(map_input_error)?;
303
304 Ok(TransmitTimeSatelliteState {
305 signal_flight_time_s: solved.tau_s,
306 transmit_offset_us: solved.transmit_offset_us,
307 transmit_time_j2000_s: solved.transmit_time_j2000_s,
308 clock_s: solved.state.clock_s,
309 transmit_position_ecef_m: solved.state.position_ecef_m,
310 position_ecef_m: solved.sat_rot_ecef_m,
311 velocity_m_s: velocity_rot,
312 geometric_range_m: range,
313 los_unit: los,
314 })
315}
316
317pub fn predict(
319 source: &dyn ObservableEphemerisSource,
320 sat: GnssSatelliteId,
321 receiver_ecef_m: [f64; 3],
322 t_rx_j2000_s: f64,
323 options: PredictOptions,
324) -> Result<PredictedObservables, ObservablesError> {
325 validate_predict_inputs(receiver_ecef_m, t_rx_j2000_s, options)?;
326 let solved = solve_transmit_time(source, sat, receiver_ecef_m, t_rx_j2000_s, options)?;
327
328 let dx = solved.sat_rot_ecef_m[0] - receiver_ecef_m[0];
329 let dy = solved.sat_rot_ecef_m[1] - receiver_ecef_m[1];
330 let dz = solved.sat_rot_ecef_m[2] - receiver_ecef_m[2];
331 let range = geometric_range_m([dx, dy, dz])?;
332 let los = [dx / range, dy / range, dz / range];
333
334 let velocity = satellite_velocity(source, sat, solved.transmit_time_j2000_s)?;
335 let velocity_rot = sagnac_rotate(velocity, solved.tau_s, options.sagnac);
336 validate::finite_vec3(velocity_rot, "satellite velocity_m_s").map_err(map_input_error)?;
337 let range_rate = los[0] * velocity_rot[0] + los[1] * velocity_rot[1] + los[2] * velocity_rot[2];
338 validate::finite(range_rate, "range_rate_m_s").map_err(map_input_error)?;
339 let doppler_hz = -range_rate * options.carrier_hz / C_M_S;
340 validate::finite(doppler_hz, "doppler_hz").map_err(map_input_error)?;
341 let (elevation_deg, azimuth_deg) = topocentric(receiver_ecef_m, [dx, dy, dz], range)?;
342
343 Ok(PredictedObservables {
344 geometric_range_m: range,
345 range_rate_m_s: range_rate,
346 doppler_hz,
347 sat_clock_s: solved.state.clock_s,
348 elevation_deg,
349 azimuth_deg,
350 transmit_offset_us: solved.transmit_offset_us,
351 transmit_time_j2000_s: solved.transmit_time_j2000_s,
352 los_unit: los,
353 sat_pos_ecef_m: solved.sat_rot_ecef_m,
354 sat_velocity_m_s: velocity_rot,
355 })
356}
357
358#[derive(Debug, Clone, Copy)]
359struct SolvedTransmitTime {
360 tau_s: f64,
361 transmit_offset_us: i64,
362 transmit_time_j2000_s: f64,
363 state: ObservableState,
364 sat_rot_ecef_m: [f64; 3],
365}
366
367fn solve_transmit_time(
368 source: &dyn ObservableEphemerisSource,
369 sat: GnssSatelliteId,
370 receiver_ecef_m: [f64; 3],
371 t_rx_j2000_s: f64,
372 options: PredictOptions,
373) -> Result<SolvedTransmitTime, ObservablesError> {
374 if !options.light_time {
375 let state = validated_state_at_j2000_s(source, sat, t_rx_j2000_s)?;
376 let sat_rot = sagnac_rotate(state.position_ecef_m, 0.0, options.sagnac);
377 validate::finite_vec3(sat_rot, "satellite position_ecef_m").map_err(map_input_error)?;
378 return Ok(SolvedTransmitTime {
379 tau_s: 0.0,
380 transmit_offset_us: 0,
381 transmit_time_j2000_s: t_rx_j2000_s,
382 state,
383 sat_rot_ecef_m: sat_rot,
384 });
385 }
386
387 let mut tau = 0.0;
388 for iter in 0..OBSERVABLE_TRANSMIT_TIME_ITERATIONS {
389 let transmit_offset_us = microseconds_from_tau(tau);
390 let t_tx = t_rx_j2000_s - transmit_offset_us as f64 / MICROSECONDS_PER_SECOND;
391 let state = validated_state_at_j2000_s(source, sat, t_tx)?;
392 let sat_rot = sagnac_rotate(state.position_ecef_m, tau, options.sagnac);
393 validate::finite_vec3(sat_rot, "satellite position_ecef_m").map_err(map_input_error)?;
394 let dx = sat_rot[0] - receiver_ecef_m[0];
395 let dy = sat_rot[1] - receiver_ecef_m[1];
396 let dz = sat_rot[2] - receiver_ecef_m[2];
397 let range = geometric_range_m([dx, dy, dz])?;
398 let new_tau = range / C_M_S;
399
400 if iter + 1 == OBSERVABLE_TRANSMIT_TIME_ITERATIONS {
401 return finalize_transmit_time(source, sat, t_rx_j2000_s, new_tau, options.sagnac);
402 }
403
404 tau = new_tau;
405 }
406
407 unreachable!("fixed transmit-time loop always returns on its last iteration")
408}
409
410fn finalize_transmit_time(
411 source: &dyn ObservableEphemerisSource,
412 sat: GnssSatelliteId,
413 t_rx_j2000_s: f64,
414 tau: f64,
415 sagnac: bool,
416) -> Result<SolvedTransmitTime, ObservablesError> {
417 let transmit_offset_us = microseconds_from_tau(tau);
418 let t_tx = t_rx_j2000_s - transmit_offset_us as f64 / MICROSECONDS_PER_SECOND;
419 validate::finite(t_tx, "transmit_time_j2000_s").map_err(map_input_error)?;
420 let state = validated_state_at_j2000_s(source, sat, t_tx)?;
421 let sat_rot = sagnac_rotate(state.position_ecef_m, tau, sagnac);
422 validate::finite_vec3(sat_rot, "satellite position_ecef_m").map_err(map_input_error)?;
423 Ok(SolvedTransmitTime {
424 tau_s: tau,
425 transmit_offset_us,
426 transmit_time_j2000_s: t_tx,
427 state,
428 sat_rot_ecef_m: sat_rot,
429 })
430}
431
432fn microseconds_from_tau(tau_s: f64) -> i64 {
433 (tau_s * MICROSECONDS_PER_SECOND).round() as i64
434}
435
436fn satellite_velocity(
437 source: &dyn ObservableEphemerisSource,
438 sat: GnssSatelliteId,
439 t_tx_j2000_s: f64,
440) -> Result<[f64; 3], ObservablesError> {
441 let plus = validated_state_at_j2000_s(source, sat, t_tx_j2000_s + FD_HALF_S)?;
442 let minus = validated_state_at_j2000_s(source, sat, t_tx_j2000_s - FD_HALF_S)?;
443 let denom = 2.0 * FD_HALF_S;
444 let velocity = [
445 (plus.position_ecef_m[0] - minus.position_ecef_m[0]) / denom,
446 (plus.position_ecef_m[1] - minus.position_ecef_m[1]) / denom,
447 (plus.position_ecef_m[2] - minus.position_ecef_m[2]) / denom,
448 ];
449 validate::finite_vec3(velocity, "satellite velocity_m_s").map_err(map_input_error)
450}
451
452fn validate_predict_inputs(
453 receiver_ecef_m: [f64; 3],
454 t_rx_j2000_s: f64,
455 options: PredictOptions,
456) -> Result<(), ObservablesError> {
457 validate::finite_vec3(receiver_ecef_m, "receiver_ecef_m").map_err(map_input_error)?;
458 validate::finite(t_rx_j2000_s, "t_rx_j2000_s").map_err(map_input_error)?;
459 validate::finite_positive(options.carrier_hz, "options.carrier_hz").map_err(map_input_error)?;
460 Ok(())
461}
462
463fn validate_transmit_time_inputs(
464 receiver_ecef_m: [f64; 3],
465 t_rx_j2000_s: f64,
466) -> Result<(), ObservablesError> {
467 validate::finite_vec3(receiver_ecef_m, "receiver_ecef_m").map_err(map_input_error)?;
468 validate::finite(t_rx_j2000_s, "t_rx_j2000_s").map_err(map_input_error)?;
469 Ok(())
470}
471
472fn validated_state_at_j2000_s(
473 source: &dyn ObservableEphemerisSource,
474 sat: GnssSatelliteId,
475 t_j2000_s: f64,
476) -> Result<ObservableState, ObservablesError> {
477 let state = source.observable_state_at_j2000_s(sat, t_j2000_s)?;
478 validate_observable_state(&state)?;
479 Ok(state)
480}
481
482fn validate_observable_state(state: &ObservableState) -> Result<(), ObservablesError> {
483 validate::finite_vec3(state.position_ecef_m, "observable state position_ecef_m")
484 .map_err(map_input_error)?;
485 if let Some(clock_s) = state.clock_s {
486 validate::finite(clock_s, "observable state clock_s").map_err(map_input_error)?;
487 }
488 Ok(())
489}
490
491fn geometric_range_m(delta_ecef_m: [f64; 3]) -> Result<f64, ObservablesError> {
492 let range = (delta_ecef_m[0] * delta_ecef_m[0]
493 + delta_ecef_m[1] * delta_ecef_m[1]
494 + delta_ecef_m[2] * delta_ecef_m[2])
495 .sqrt();
496 validate::finite_positive(range, "geometric_range_m").map_err(map_input_error)
497}
498
499fn map_input_error(error: validate::FieldError) -> ObservablesError {
500 ObservablesError::InvalidInput {
501 field: error.field(),
502 kind: ObservablesInputErrorKind::from(&error),
503 }
504}
505
506fn sagnac_rotate(pos: [f64; 3], tau_s: f64, apply: bool) -> [f64; 3] {
507 let sagnac = if apply {
508 SagnacRecipe::ClosedFormZRotation
509 } else {
510 SagnacRecipe::Off
511 };
512 crate::estimation::substrate::range::rotate_transmit_satellite(
513 sagnac,
514 pos,
515 tau_s,
516 OMEGA_E_DOT_RAD_S,
517 )
518}
519
520fn topocentric(
521 receiver_ecef_m: [f64; 3],
522 delta_ecef_m: [f64; 3],
523 range_m: f64,
524) -> Result<(f64, f64), ObservablesError> {
525 let (lat_deg, lon_deg, _height_km) = itrs_to_geodetic_compute(
526 receiver_ecef_m[0] / KM_TO_M,
527 receiver_ecef_m[1] / KM_TO_M,
528 receiver_ecef_m[2] / KM_TO_M,
529 )
530 .map_err(|_| ObservablesError::InvalidInput {
531 field: "receiver_ecef_m",
532 kind: ObservablesInputErrorKind::OutOfRange,
533 })?;
534 let lat = lat_deg * PI / DEGREES_PER_SEMICIRCLE;
536 let lon = lon_deg * PI / DEGREES_PER_SEMICIRCLE;
537
538 let sl = lat.sin();
539 let cl = lat.cos();
540 let so = lon.sin();
541 let co = lon.cos();
542
543 let dx = delta_ecef_m[0];
544 let dy = delta_ecef_m[1];
545 let dz = delta_ecef_m[2];
546
547 let e = -so * dx + co * dy;
548 let n = -sl * co * dx - sl * so * dy + cl * dz;
549 let u = cl * co * dx + cl * so * dy + sl * dz;
550
551 let mut azimuth_deg = e.atan2(n) * DEGREES_PER_SEMICIRCLE / PI;
553 if azimuth_deg < 0.0 {
554 azimuth_deg += DEGREES_PER_CIRCLE;
555 }
556 let elevation_deg = (u / range_m).asin() * DEGREES_PER_SEMICIRCLE / PI;
557
558 validate::finite(elevation_deg, "elevation_deg").map_err(map_input_error)?;
559 validate::finite(azimuth_deg, "azimuth_deg").map_err(map_input_error)?;
560 Ok((elevation_deg, azimuth_deg))
561}
562
563#[cfg(test)]
564mod public_api_tests {
565 use super::*;
566 use crate::{GnssSatelliteId, GnssSystem};
567
568 #[derive(Debug, Clone, Copy)]
569 struct StaticSource {
570 state: ObservableState,
571 }
572
573 impl ObservableEphemerisSource for StaticSource {
574 fn observable_state_at_j2000_s(
575 &self,
576 _sat: GnssSatelliteId,
577 _t_j2000_s: f64,
578 ) -> Result<ObservableState, ObservablesError> {
579 Ok(self.state)
580 }
581 }
582
583 #[test]
584 fn transmit_time_state_matches_predict_substrate_with_no_light_time() {
585 let source = StaticSource {
586 state: ObservableState {
587 position_ecef_m: [20_200_000.0, 14_000_000.0, 21_700_000.0],
588 clock_s: Some(1.25e-6),
589 },
590 };
591 let sat = GnssSatelliteId::new(GnssSystem::Gps, 21).expect("valid satellite id");
592 let rx = [4_027_894.0, 307_046.0, 4_919_474.0];
593 let state = transmit_time_satellite_state(
594 &source,
595 sat,
596 rx,
597 646_272_000.0,
598 TransmitTimeOptions {
599 light_time: false,
600 sagnac: true,
601 },
602 )
603 .expect("state");
604 let prediction = predict(
605 &source,
606 sat,
607 rx,
608 646_272_000.0,
609 PredictOptions {
610 carrier_hz: F_L1_HZ,
611 light_time: false,
612 sagnac: true,
613 },
614 )
615 .expect("prediction");
616
617 assert_eq!(state.signal_flight_time_s.to_bits(), 0.0f64.to_bits());
618 assert_eq!(state.transmit_offset_us, 0);
619 assert_eq!(
620 state.transmit_time_j2000_s.to_bits(),
621 646_272_000.0f64.to_bits()
622 );
623 assert_eq!(state.clock_s.unwrap().to_bits(), 1.25e-6f64.to_bits());
624 assert_eq!(
625 state.transmit_position_ecef_m.map(f64::to_bits),
626 source.state.position_ecef_m.map(f64::to_bits)
627 );
628 assert_eq!(
629 state.position_ecef_m.map(f64::to_bits),
630 prediction.sat_pos_ecef_m.map(f64::to_bits)
631 );
632 assert_eq!(
633 state.velocity_m_s.map(f64::to_bits),
634 prediction.sat_velocity_m_s.map(f64::to_bits)
635 );
636 assert_eq!(
637 state.geometric_range_m.to_bits(),
638 prediction.geometric_range_m.to_bits()
639 );
640 assert_eq!(
641 state.los_unit.map(f64::to_bits),
642 prediction.los_unit.map(f64::to_bits)
643 );
644 }
645}
646
647#[cfg(all(test, sidereon_repo_tests))]
648mod tests {
649 use super::*;
650 use crate::{GnssSatelliteId, GnssSystem};
651
652 #[derive(Debug, Clone, Copy)]
653 struct StaticSource {
654 state: ObservableState,
655 }
656
657 impl ObservableEphemerisSource for StaticSource {
658 fn observable_state_at_j2000_s(
659 &self,
660 _sat: GnssSatelliteId,
661 _t_j2000_s: f64,
662 ) -> Result<ObservableState, ObservablesError> {
663 Ok(self.state)
664 }
665 }
666
667 fn sp3_fixture() -> Sp3 {
668 let path = concat!(
669 env!("CARGO_MANIFEST_DIR"),
670 "/tests/fixtures/sp3/GRG0MGXFIN_20201760000_01D_15M_ORB.SP3"
671 );
672 let bytes = std::fs::read(path).unwrap_or_else(|e| panic!("read SP3 fixture {path}: {e}"));
673 Sp3::parse(&bytes).expect("parse SP3 fixture")
674 }
675
676 fn static_source(position_ecef_m: [f64; 3]) -> StaticSource {
677 StaticSource {
678 state: ObservableState {
679 position_ecef_m,
680 clock_s: Some(0.0),
681 },
682 }
683 }
684
685 fn no_light_time_options() -> PredictOptions {
686 PredictOptions {
687 carrier_hz: F_L1_HZ,
688 light_time: false,
689 sagnac: true,
690 }
691 }
692
693 fn assert_invalid_observables_input(
694 err: ObservablesError,
695 field: &'static str,
696 kind: ObservablesInputErrorKind,
697 ) {
698 match err {
699 ObservablesError::InvalidInput {
700 field: got_field,
701 kind: got_kind,
702 } => {
703 assert_eq!(got_field, field);
704 assert_eq!(got_kind, kind);
705 }
706 other => panic!("expected InvalidInput({field}, {kind:?}), got {other:?}"),
707 }
708 }
709
710 #[test]
711 fn split_julian_to_j2000_seconds_matches_orbis_time() {
712 let t = j2000_seconds_from_split(2_459_024.5, 0.5).expect("valid split Julian date");
713 assert_eq!(t, 646_272_000.0);
714 }
715
716 #[test]
717 fn split_julian_to_j2000_seconds_rejects_non_finite_parts() {
718 for (jd_whole, jd_fraction, field) in [
719 (f64::NAN, 0.5, "jd_whole"),
720 (f64::INFINITY, 0.5, "jd_whole"),
721 (2_459_024.5, f64::NAN, "jd_fraction"),
722 (2_459_024.5, f64::NEG_INFINITY, "jd_fraction"),
723 ] {
724 let err = j2000_seconds_from_split(jd_whole, jd_fraction)
725 .expect_err("non-finite split Julian date part must fail");
726 assert_invalid_observables_input(err, field, ObservablesInputErrorKind::NonFinite);
727 }
728 }
729
730 #[test]
731 fn sp3_predict_reference_case() {
732 let sp3 = sp3_fixture();
733 let sat = GnssSatelliteId::new(GnssSystem::Gps, 21).expect("valid satellite id");
734 let rx = [3_512_900.0, 780_500.0, 5_248_700.0];
735 let obs = predict(&sp3, sat, rx, 646_272_000.0, PredictOptions::default())
736 .expect("predict observables");
737
738 assert_eq!(obs.geometric_range_m.to_bits(), 0x4173cf438ba57358);
739 assert_eq!(obs.range_rate_m_s.to_bits(), 0x402d7dd36f6b8980);
740 assert_eq!(obs.doppler_hz.to_bits(), 0xc0535f534ba7c77d);
741 assert_eq!(obs.sat_clock_s.unwrap().to_bits(), 0x3ef04d2d8279460c);
742 assert_eq!(obs.elevation_deg.to_bits(), 0x4054590eed870f52);
743 assert_eq!(obs.azimuth_deg.to_bits(), 0x40645ff5a090a131);
744 assert_eq!(obs.transmit_offset_us, 69_288);
745 assert_eq!(obs.transmit_time_j2000_s.to_bits(), 0x41c342a9fff72192);
746 assert_eq!(
747 obs.los_unit.map(f64::to_bits),
748 [0x3fe4c70da9fa70dd, 0x3fc834429adb2bae, 0x3fe792a4f57fdcb1,]
749 );
750 assert_eq!(
751 obs.sat_pos_ecef_m.map(f64::to_bits),
752 [0x41703667d8c0eb8f, 0x4151f601b1d775f3, 0x4173992c0ec03dcd,]
753 );
754 assert_eq!(
755 obs.sat_velocity_m_s.map(f64::to_bits),
756 [0xc09c17d81e540ab6, 0x409a192982abbeb7, 0x40926013f2ae8000,]
757 );
758 }
759
760 #[test]
761 fn predict_rejects_invalid_entry_inputs() {
762 let source = static_source([20_200_000.0, 14_000_000.0, 21_700_000.0]);
763 let sat = GnssSatelliteId::new(GnssSystem::Gps, 21).expect("valid satellite id");
764
765 let err = predict(
766 &source,
767 sat,
768 [f64::NAN, 0.0, 0.0],
769 646_272_000.0,
770 no_light_time_options(),
771 )
772 .expect_err("non-finite receiver position must fail");
773 assert_invalid_observables_input(
774 err,
775 "receiver_ecef_m",
776 ObservablesInputErrorKind::NonFinite,
777 );
778
779 let err = predict(
780 &source,
781 sat,
782 [0.0, 0.0, 0.0],
783 f64::INFINITY,
784 no_light_time_options(),
785 )
786 .expect_err("non-finite receive time must fail");
787 assert_invalid_observables_input(err, "t_rx_j2000_s", ObservablesInputErrorKind::NonFinite);
788
789 let mut options = no_light_time_options();
790 options.carrier_hz = 0.0;
791 let err = predict(&source, sat, [0.0, 0.0, 0.0], 646_272_000.0, options)
792 .expect_err("non-positive carrier must fail");
793 assert_invalid_observables_input(
794 err,
795 "options.carrier_hz",
796 ObservablesInputErrorKind::NotPositive,
797 );
798 }
799
800 #[test]
801 fn predict_rejects_invalid_source_state_and_zero_range() {
802 let sat = GnssSatelliteId::new(GnssSystem::Gps, 21).expect("valid satellite id");
803
804 let source = static_source([f64::NAN, 14_000_000.0, 21_700_000.0]);
805 let err = predict(
806 &source,
807 sat,
808 [0.0, 0.0, 0.0],
809 646_272_000.0,
810 no_light_time_options(),
811 )
812 .expect_err("non-finite ephemeris position must fail");
813 assert_invalid_observables_input(
814 err,
815 "observable state position_ecef_m",
816 ObservablesInputErrorKind::NonFinite,
817 );
818
819 let source = static_source([1_000.0, 2_000.0, 3_000.0]);
820 let err = predict(
821 &source,
822 sat,
823 [1_000.0, 2_000.0, 3_000.0],
824 646_272_000.0,
825 no_light_time_options(),
826 )
827 .expect_err("zero geometric range must fail");
828 assert_invalid_observables_input(
829 err,
830 "geometric_range_m",
831 ObservablesInputErrorKind::NotPositive,
832 );
833 }
834
835 #[test]
836 fn topocentric_rejects_invalid_receiver_geodetic_conversion() {
837 let err = topocentric([f64::MAX, 0.0, 0.0], [1.0, 0.0, 0.0], 1.0)
838 .expect_err("invalid receiver geodetic conversion must fail");
839
840 assert_invalid_observables_input(
841 err,
842 "receiver_ecef_m",
843 ObservablesInputErrorKind::OutOfRange,
844 );
845 }
846}