1use crate::astro::bodies::{sun_moon_ecef, SunMoon};
9use crate::astro::math::vec3::{add3, cross3, dot3, neg3, norm3, scale3, sub3, unit3};
10use crate::astro::time::{CoverageError, TimeScaleInputErrorKind, TimeScales, ValidityMode};
11use crate::validate;
12use std::collections::BTreeMap;
13use std::f64::consts::PI;
14
15use crate::antenna;
16use crate::constants::{C_M_S, F_L1_HZ, OMEGA_E_DOT_RAD_S, RAD_TO_DEG};
17use crate::ephemeris::Sp3;
18use crate::observables::{
19 predict, ObservablesError, ObservablesInputErrorKind, PredictOptions, PredictedObservables,
20};
21use crate::tides::{solid_earth_tide, TideError};
22use crate::tolerances::{FREQUENCY_DENOMINATOR_EPS_HZ, YAW_SINGULARITY_EPS_RAD};
23use crate::GnssSatelliteId;
24
25const TWO_PI: f64 = 2.0 * PI;
26
27#[derive(Debug, Clone, Copy, PartialEq)]
29pub struct CivilDateTime {
30 pub year: i32,
31 pub month: u8,
32 pub day: u8,
33 pub hour: u8,
34 pub minute: u8,
35 pub second: f64,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq)]
40pub struct PppCorrectionObservation {
41 pub sat: GnssSatelliteId,
42 pub freq1_hz: f64,
43 pub freq2_hz: f64,
44}
45
46#[derive(Debug, Clone, PartialEq)]
48pub struct PppCorrectionEpoch {
49 pub epoch: CivilDateTime,
50 pub t_rx_j2000_s: f64,
51 pub observations: Vec<PppCorrectionObservation>,
52}
53
54#[derive(Debug, Clone, PartialEq)]
56pub struct SatelliteAntennaFrequency {
57 pub label: String,
58 pub pco_m: [f64; 3],
59 pub noazi_pcv_m: Vec<(f64, f64)>,
60}
61
62#[derive(Debug, Clone, PartialEq)]
64pub struct SatelliteAntenna {
65 pub sat: GnssSatelliteId,
66 pub valid_from: Option<CivilDateTime>,
67 pub valid_until: Option<CivilDateTime>,
68 pub frequencies: Vec<SatelliteAntennaFrequency>,
69}
70
71#[derive(Debug, Clone, PartialEq)]
73pub struct SatelliteAntennaOptions {
74 pub freq1_label: String,
75 pub freq1_hz: f64,
76 pub freq2_label: String,
77 pub freq2_hz: f64,
78 pub antennas: Vec<SatelliteAntenna>,
79}
80
81#[derive(Debug, Clone, PartialEq)]
83pub struct PppCorrectionsOptions {
84 pub solid_earth_tide: bool,
85 pub phase_windup: bool,
86 pub satellite_antenna: Option<SatelliteAntennaOptions>,
87}
88
89#[derive(Debug, Clone, Copy, PartialEq)]
91pub struct EpochVectorCorrection {
92 pub epoch_index: usize,
93 pub vector_m: [f64; 3],
94}
95
96#[derive(Debug, Clone, PartialEq)]
98pub struct SatScalarCorrection {
99 pub sat: GnssSatelliteId,
100 pub epoch_index: usize,
101 pub value_m: f64,
102}
103
104#[derive(Debug, Clone, PartialEq)]
106pub struct SatVectorCorrection {
107 pub sat: GnssSatelliteId,
108 pub epoch_index: usize,
109 pub vector_m: [f64; 3],
110}
111
112#[derive(Debug, Clone, PartialEq, Default)]
114pub struct PppCorrections {
115 pub tide: Vec<EpochVectorCorrection>,
116 pub windup_m: Vec<SatScalarCorrection>,
117 pub sat_pco_ecef: Vec<SatVectorCorrection>,
118 pub sat_pcv_m: Vec<SatScalarCorrection>,
119}
120
121#[derive(Debug, Clone, PartialEq, thiserror::Error)]
122pub enum PppCorrectionsError {
123 #[error("invalid PPP correction input {field}: {reason}")]
124 InvalidInput {
125 field: &'static str,
126 reason: &'static str,
127 },
128 #[error("invalid PPP correction epoch at epoch {epoch_index}: {source}")]
129 Epoch {
130 epoch_index: usize,
131 #[source]
132 source: CoverageError,
133 },
134 #[error("solid Earth tide correction failed at epoch {epoch_index}: {source}")]
135 Tide {
136 epoch_index: usize,
137 #[source]
138 source: TideError,
139 },
140 #[error(
141 "invalid phase wind-up carrier frequencies at epoch {epoch_index} for {sat}: {field} {reason}"
142 )]
143 WindupFrequency {
144 epoch_index: usize,
145 sat: GnssSatelliteId,
146 field: &'static str,
147 reason: &'static str,
148 },
149 #[error("invalid satellite antenna carrier frequencies: {field} {reason}")]
150 SatelliteAntennaFrequency {
151 field: &'static str,
152 reason: &'static str,
153 },
154}
155
156pub fn build(
158 sp3: &Sp3,
159 epochs: &[PppCorrectionEpoch],
160 receiver_ecef_m: [f64; 3],
161 options: &PppCorrectionsOptions,
162) -> Result<PppCorrections, PppCorrectionsError> {
163 validate_receiver_state(receiver_ecef_m)?;
164
165 let mut corrections = PppCorrections::default();
166 if !options.solid_earth_tide && !options.phase_windup && options.satellite_antenna.is_none() {
167 return Ok(corrections);
168 }
169
170 let satellite_antenna_frequencies = options
171 .satellite_antenna
172 .as_ref()
173 .map(validate_satellite_antenna_options)
174 .transpose()?;
175
176 let mut previous_windup_cycles: BTreeMap<GnssSatelliteId, f64> = BTreeMap::new();
177
178 for (epoch_index, epoch_row) in epochs.iter().enumerate() {
179 let sun_moon =
180 sun_moon_at(epoch_row.epoch).map_err(|source| PppCorrectionsError::Epoch {
181 epoch_index,
182 source,
183 })?;
184
185 if options.solid_earth_tide {
186 let d = tide_at(
187 receiver_ecef_m,
188 epoch_row.epoch,
189 sun_moon.sun,
190 sun_moon.moon,
191 )
192 .map_err(|source| PppCorrectionsError::Tide {
193 epoch_index,
194 source,
195 })?;
196 corrections.tide.push(EpochVectorCorrection {
197 epoch_index,
198 vector_m: d,
199 });
200 }
201
202 for observation in &epoch_row.observations {
203 let obs = match predict(
204 sp3,
205 observation.sat,
206 receiver_ecef_m,
207 epoch_row.t_rx_j2000_s,
208 PredictOptions {
209 carrier_hz: F_L1_HZ,
210 light_time: true,
211 sagnac: true,
212 },
213 ) {
214 Ok(obs) => obs,
215 Err(ObservablesError::InvalidInput { field, kind }) => {
216 return Err(PppCorrectionsError::InvalidInput {
217 field,
218 reason: observables_input_reason(kind),
219 });
220 }
221 Err(ObservablesError::NoEphemeris | ObservablesError::Ephemeris(_)) => continue,
222 };
223
224 if options.phase_windup {
225 let prev = previous_windup_cycles.get(&observation.sat).copied();
226 if let Some(phw) = windup_cycles(&obs, receiver_ecef_m, sun_moon.sun, prev) {
227 let (f1, f2) = windup_frequency_pair(options, observation, epoch_index)?;
228 corrections.windup_m.push(SatScalarCorrection {
229 sat: observation.sat,
230 epoch_index,
231 value_m: windup_metres(phw, f1, f2),
232 });
233 previous_windup_cycles.insert(observation.sat, phw);
234 }
235 }
236
237 if let Some(sat_ant) = &options.satellite_antenna {
238 if let Some((pco_ecef, pcv_m)) = satellite_antenna_correction(
239 &obs,
240 sun_moon.sun,
241 observation.sat,
242 epoch_row.epoch,
243 sat_ant,
244 satellite_antenna_frequencies
245 .expect("satellite antenna frequencies are validated when enabled"),
246 ) {
247 corrections.sat_pco_ecef.push(SatVectorCorrection {
248 sat: observation.sat,
249 epoch_index,
250 vector_m: pco_ecef,
251 });
252 corrections.sat_pcv_m.push(SatScalarCorrection {
253 sat: observation.sat,
254 epoch_index,
255 value_m: pcv_m,
256 });
257 }
258 }
259 }
260 }
261
262 Ok(corrections)
263}
264
265fn validate_receiver_state(receiver_ecef_m: [f64; 3]) -> Result<(), PppCorrectionsError> {
266 validate::finite_vec3(receiver_ecef_m, "receiver_ecef_m").map_err(ppp_invalid_input)?;
267 validate::finite_positive(norm3(receiver_ecef_m), "receiver radius_m")
268 .map_err(ppp_invalid_input)?;
269 Ok(())
270}
271
272fn ppp_invalid_input(error: validate::FieldError) -> PppCorrectionsError {
273 PppCorrectionsError::InvalidInput {
274 field: error.field(),
275 reason: error.reason(),
276 }
277}
278
279fn observables_input_reason(kind: ObservablesInputErrorKind) -> &'static str {
280 match kind {
281 ObservablesInputErrorKind::NonFinite => "not finite",
282 ObservablesInputErrorKind::NotPositive => "not positive",
283 ObservablesInputErrorKind::Negative => "negative",
284 ObservablesInputErrorKind::OutOfRange => "out of range",
285 ObservablesInputErrorKind::Missing => "missing",
286 ObservablesInputErrorKind::FloatParse => "invalid float",
287 ObservablesInputErrorKind::IntParse => "invalid integer",
288 ObservablesInputErrorKind::InvalidCivilDate => "invalid civil date",
289 ObservablesInputErrorKind::InvalidCivilTime => "invalid civil time",
290 }
291}
292
293fn windup_frequency_pair(
294 options: &PppCorrectionsOptions,
295 observation: &PppCorrectionObservation,
296 epoch_index: usize,
297) -> Result<(f64, f64), PppCorrectionsError> {
298 let (f1_hz, f2_hz) = options
299 .satellite_antenna
300 .as_ref()
301 .map(|a| (a.freq1_hz, a.freq2_hz))
302 .unwrap_or((observation.freq1_hz, observation.freq2_hz));
303 validate_frequency_pair(
304 f1_hz,
305 f2_hz,
306 FrequencyPairFields {
307 freq1: "phase wind-up freq1_hz",
308 freq2: "phase wind-up freq2_hz",
309 pair: "phase wind-up frequency pair",
310 },
311 |field, reason| PppCorrectionsError::WindupFrequency {
312 epoch_index,
313 sat: observation.sat,
314 field,
315 reason,
316 },
317 )
318}
319
320fn validate_satellite_antenna_frequency_pair(
321 options: &SatelliteAntennaOptions,
322) -> Result<(f64, f64), PppCorrectionsError> {
323 validate_frequency_pair(
324 options.freq1_hz,
325 options.freq2_hz,
326 FrequencyPairFields {
327 freq1: "satellite antenna freq1_hz",
328 freq2: "satellite antenna freq2_hz",
329 pair: "satellite antenna frequency pair",
330 },
331 |field, reason| PppCorrectionsError::SatelliteAntennaFrequency { field, reason },
332 )
333}
334
335fn validate_satellite_antenna_options(
336 options: &SatelliteAntennaOptions,
337) -> Result<(f64, f64), PppCorrectionsError> {
338 let frequencies_hz = validate_satellite_antenna_frequency_pair(options)?;
339 validate_satellite_antenna_pcv_samples(options)?;
340 Ok(frequencies_hz)
341}
342
343fn validate_satellite_antenna_pcv_samples(
344 options: &SatelliteAntennaOptions,
345) -> Result<(), PppCorrectionsError> {
346 for antenna in &options.antennas {
347 for frequency in &antenna.frequencies {
348 for &(nadir_deg, pcv_m) in &frequency.noazi_pcv_m {
349 validate::finite(nadir_deg, "satellite antenna noazi_pcv_m")
350 .map_err(ppp_invalid_input)?;
351 validate::finite(pcv_m, "satellite antenna noazi_pcv_m")
352 .map_err(ppp_invalid_input)?;
353 }
354 }
355 }
356 Ok(())
357}
358
359#[derive(Debug, Clone, Copy)]
360struct FrequencyPairFields {
361 freq1: &'static str,
362 freq2: &'static str,
363 pair: &'static str,
364}
365
366fn validate_frequency_pair(
367 f1_hz: f64,
368 f2_hz: f64,
369 fields: FrequencyPairFields,
370 invalid: impl Fn(&'static str, &'static str) -> PppCorrectionsError,
371) -> Result<(f64, f64), PppCorrectionsError> {
372 let f1_hz = validate::finite_positive(f1_hz, fields.freq1)
373 .map_err(|e| invalid(e.field(), e.reason()))?;
374 let f2_hz = validate::finite_positive(f2_hz, fields.freq2)
375 .map_err(|e| invalid(e.field(), e.reason()))?;
376 if (f1_hz - f2_hz).abs() < FREQUENCY_DENOMINATOR_EPS_HZ {
377 Err(invalid(fields.pair, "must differ"))
378 } else {
379 Ok((f1_hz, f2_hz))
380 }
381}
382
383fn sun_moon_at(epoch: CivilDateTime) -> Result<SunMoon, CoverageError> {
384 let ts = time_scales_at(epoch)?;
385 Ok(sun_moon_ecef(&ts).expect("validated time scales produce Sun/Moon vectors"))
386}
387
388fn time_scales_at(epoch: CivilDateTime) -> Result<TimeScales, CoverageError> {
389 let civil = validate::civil_datetime_with_second_policy(
390 i64::from(epoch.year),
391 i64::from(epoch.month),
392 i64::from(epoch.day),
393 i64::from(epoch.hour),
394 i64::from(epoch.minute),
395 epoch.second,
396 validate::CivilSecondPolicy::UtcLike,
397 )
398 .map_err(|error| CoverageError::InvalidInput {
399 field: error.field(),
400 kind: TimeScaleInputErrorKind::from(&error),
401 })?;
402
403 TimeScales::from_utc_validated(
404 civil.year as i32,
405 civil.month as i32,
406 civil.day as i32,
407 civil.hour as i32,
408 civil.minute as i32,
409 civil.second,
410 ValidityMode::Strict,
411 )
412 .map(|validated| validated.value)
413}
414
415fn tide_at(
416 receiver_ecef_m: [f64; 3],
417 epoch: CivilDateTime,
418 sun_ecef_m: [f64; 3],
419 moon_ecef_m: [f64; 3],
420) -> Result<[f64; 3], TideError> {
421 let fhr = epoch.hour as f64 + epoch.minute as f64 / 60.0 + epoch.second / 3600.0;
422 solid_earth_tide(
423 &receiver_ecef_m,
424 epoch.year,
425 epoch.month as i32,
426 epoch.day as i32,
427 fhr,
428 &sun_ecef_m,
429 &moon_ecef_m,
430 )
431}
432
433fn windup_metres(phw_cycles: f64, f1_hz: f64, f2_hz: f64) -> f64 {
434 let lam1 = C_M_S / f1_hz;
435 let lam2 = C_M_S / f2_hz;
436 let gamma = ionosphere_free_gamma(f1_hz, f2_hz);
437 (gamma * lam1 - (gamma - 1.0) * lam2) * phw_cycles
438}
439
440fn windup_cycles(
441 pred: &PredictedObservables,
442 receiver_ecef_m: [f64; 3],
443 sun_ecef_m: [f64; 3],
444 prev_phw: Option<f64>,
445) -> Option<f64> {
446 let rs = pred.sat_pos_ecef_m;
447 let vs = pred.sat_velocity_m_s;
448 let (exs, eys) = sat_yaw(rs, vs, sun_ecef_m)?;
449 let ek = unit3(sub3(receiver_ecef_m, rs))?;
450
451 let (n, e, _u) = crate::estimation::substrate::frames::local_neu_basis(
452 crate::estimation::recipe::FrameRecipe::GeodeticNeuCrossProduct,
453 receiver_ecef_m,
454 );
455 let exr = n;
456 let eyr = neg3(e);
457
458 let eks = cross3(ek, eys);
459 let ekr = cross3(ek, eyr);
460 let ds = sub3(exs, add3(scale3(ek, dot3(ek, exs)), eks));
461 let dr = sub3(exr, sub3(scale3(ek, dot3(ek, exr)), ekr));
462
463 let nds = norm3(ds);
464 let ndr = norm3(dr);
465 if nds == 0.0 || ndr == 0.0 {
466 return None;
467 }
468
469 let cosp = clamp(dot3(ds, dr) / nds / ndr);
470 let mut ph = cosp.acos() / TWO_PI;
471 let drs = cross3(ds, dr);
472 if dot3(ek, drs) < 0.0 {
473 ph = -ph;
474 }
475
476 Some(match prev_phw {
477 None => ph,
478 Some(prev) => ph + (prev - ph + 0.5).floor(),
479 })
480}
481
482fn sat_yaw(rs: [f64; 3], vs: [f64; 3], sun_ecef_m: [f64; 3]) -> Option<([f64; 3], [f64; 3])> {
483 let ri_v = [
484 vs[0] - OMEGA_E_DOT_RAD_S * rs[1],
485 vs[1] + OMEGA_E_DOT_RAD_S * rs[0],
486 vs[2],
487 ];
488 let n = cross3(rs, ri_v);
489 let p = cross3(sun_ecef_m, n);
490
491 let es = unit3(rs)?;
492 let esun = unit3(sun_ecef_m)?;
493 let en = unit3(n)?;
494 let ep = unit3(p)?;
495
496 let beta = PI / 2.0 - clamp(dot3(esun, en)).acos();
497 let ee = clamp(dot3(es, ep)).acos();
498 let mut mu = PI / 2.0 + if dot3(es, esun) <= 0.0 { -ee } else { ee };
499
500 if mu < -PI / 2.0 {
501 mu += TWO_PI;
502 } else if mu >= PI / 2.0 {
503 mu -= TWO_PI;
504 }
505
506 let yaw = yaw_nominal(beta, mu);
507 let ex = cross3(en, es);
508 let cosy = yaw.cos();
509 let siny = yaw.sin();
510 let exs = add3(scale3(en, -siny), scale3(ex, cosy));
511 let eys = add3(scale3(en, -cosy), scale3(ex, -siny));
512 Some((exs, eys))
513}
514
515fn yaw_nominal(beta: f64, mu: f64) -> f64 {
516 if beta.abs() < YAW_SINGULARITY_EPS_RAD && mu.abs() < YAW_SINGULARITY_EPS_RAD {
517 PI
518 } else {
519 (-beta.tan()).atan2(mu.sin()) + PI
520 }
521}
522
523fn satellite_antenna_correction(
524 pred: &PredictedObservables,
525 sun_ecef_m: [f64; 3],
526 sat: GnssSatelliteId,
527 epoch: CivilDateTime,
528 options: &SatelliteAntennaOptions,
529 frequencies_hz: (f64, f64),
530) -> Option<([f64; 3], f64)> {
531 let rs = pred.sat_pos_ecef_m;
532 let ant = options.antenna_for(sat, epoch)?;
533
534 let ez = unit3(neg3(rs))?;
535 let es = unit3(sub3(sun_ecef_m, rs))?;
536 let ey = unit3(cross3(ez, es))?;
537 let ex = cross3(ey, ez);
538
539 let off1 = ant.pco(&options.freq1_label)?;
540 let off2 = ant.pco(&options.freq2_label)?;
541 let gamma = ionosphere_free_gamma(frequencies_hz.0, frequencies_hz.1);
542
543 let dant1 = body_to_ecef(off1, ex, ey, ez);
544 let dant2 = body_to_ecef(off2, ex, ey, ez);
545 let dant_ecef = sub3(scale3(dant1, gamma), scale3(dant2, gamma - 1.0));
546 let pcv_m = nadir_pcv_if(ant, pred, options, gamma)?;
547
548 Some((dant_ecef, pcv_m))
549}
550
551fn body_to_ecef(pco_body_m: [f64; 3], ex: [f64; 3], ey: [f64; 3], ez: [f64; 3]) -> [f64; 3] {
552 add3(
553 add3(scale3(ex, pco_body_m[0]), scale3(ey, pco_body_m[1])),
554 scale3(ez, pco_body_m[2]),
555 )
556}
557
558fn ionosphere_free_gamma(f1_hz: f64, f2_hz: f64) -> f64 {
559 let f1_sq = f1_hz * f1_hz;
560 f1_sq / (f1_sq - f2_hz * f2_hz)
561}
562
563fn nadir_pcv_if(
564 ant: &SatelliteAntenna,
565 pred: &PredictedObservables,
566 options: &SatelliteAntennaOptions,
567 gamma: f64,
568) -> Option<f64> {
569 let eu = unit3(neg3(pred.los_unit))?;
570 let ez = unit3(neg3(pred.sat_pos_ecef_m))?;
571 let nadir_deg = clamp(dot3(eu, ez)).acos() * RAD_TO_DEG;
572 let p1 = ant.pcv_noazi(&options.freq1_label, nadir_deg)?;
573 let p2 = ant.pcv_noazi(&options.freq2_label, nadir_deg)?;
574 Some(gamma * p1 - (gamma - 1.0) * p2)
575}
576
577impl SatelliteAntennaOptions {
578 fn antenna_for(&self, sat: GnssSatelliteId, epoch: CivilDateTime) -> Option<&SatelliteAntenna> {
579 self.antennas
580 .iter()
581 .find(|ant| ant.sat == sat && ant.valid_at(epoch))
582 }
583}
584
585impl SatelliteAntenna {
586 fn valid_at(&self, epoch: CivilDateTime) -> bool {
587 let after_from = self
588 .valid_from
589 .map(|from| civil_cmp(epoch, from) != std::cmp::Ordering::Less)
590 .unwrap_or(true);
591 let before_until = self
592 .valid_until
593 .map(|until| civil_cmp(epoch, until) != std::cmp::Ordering::Greater)
594 .unwrap_or(true);
595 after_from && before_until
596 }
597
598 fn frequency(&self, label: &str) -> Option<&SatelliteAntennaFrequency> {
599 self.frequencies
600 .iter()
601 .find(|f| f.label.trim() == label.trim())
602 }
603
604 fn pco(&self, label: &str) -> Option<[f64; 3]> {
605 self.frequency(label).map(|f| f.pco_m)
606 }
607
608 fn pcv_noazi(&self, label: &str, zenith_deg: f64) -> Option<f64> {
609 let frequency = self.frequency(label)?;
610 interpolate_samples(&frequency.noazi_pcv_m, zenith_deg)
611 }
612}
613
614fn civil_cmp(a: CivilDateTime, b: CivilDateTime) -> std::cmp::Ordering {
615 (
616 a.year,
617 a.month,
618 a.day,
619 a.hour,
620 a.minute,
621 ordered_seconds(a.second),
622 )
623 .cmp(&(
624 b.year,
625 b.month,
626 b.day,
627 b.hour,
628 b.minute,
629 ordered_seconds(b.second),
630 ))
631}
632
633fn ordered_seconds(second: f64) -> i64 {
634 (second * 1_000_000.0).round() as i64
635}
636
637fn interpolate_samples(samples: &[(f64, f64)], zenith_deg: f64) -> Option<f64> {
638 let mut sorted = samples.to_vec();
639 sorted.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
640 antenna::interpolate_zenith_sorted(&sorted, zenith_deg)
641}
642
643fn clamp(x: f64) -> f64 {
644 x.clamp(-1.0, 1.0)
645}
646
647#[cfg(all(test, sidereon_repo_tests))]
648mod tests {
649 use super::*;
650 use crate::constants::{F_L2_HZ, SECONDS_PER_DAY};
651 use crate::observables::j2000_seconds_from_split;
652 use crate::GnssSystem;
653
654 fn sp3_fixture() -> Sp3 {
655 let path = concat!(
656 env!("CARGO_MANIFEST_DIR"),
657 "/tests/fixtures/sp3/GRG0MGXFIN_20201760000_01D_15M_ORB.SP3"
658 );
659 let bytes = std::fs::read(path).unwrap_or_else(|e| panic!("read SP3 fixture {path}: {e}"));
660 Sp3::parse(&bytes).expect("parse SP3 fixture")
661 }
662
663 fn civil(year: i32, month: u8, day: u8, hour: u8, minute: u8, second: f64) -> CivilDateTime {
664 CivilDateTime {
665 year,
666 month,
667 day,
668 hour,
669 minute,
670 second,
671 }
672 }
673
674 fn split_jd(epoch: CivilDateTime) -> (f64, f64) {
675 let a = (14 - epoch.month as i32) / 12;
676 let y = epoch.year + 4800 - a;
677 let m = epoch.month as i32 + 12 * a - 3;
678 let jdn =
679 epoch.day as i32 + (153 * m + 2) / 5 + 365 * y + y / 4 - y / 100 + y / 400 - 32_045;
680 let jd_whole = jdn as f64 - 0.5;
681 let day_seconds =
682 epoch.hour as f64 * 3600.0 + epoch.minute as f64 * 60.0 + epoch.second / 1.0;
683 (jd_whole, day_seconds / SECONDS_PER_DAY)
684 }
685
686 fn fake_antenna_options(sat: GnssSatelliteId) -> SatelliteAntennaOptions {
687 SatelliteAntennaOptions {
688 freq1_label: "G01".to_string(),
689 freq1_hz: F_L1_HZ,
690 freq2_label: "G02".to_string(),
691 freq2_hz: F_L2_HZ,
692 antennas: vec![SatelliteAntenna {
693 sat,
694 valid_from: Some(civil(2020, 1, 1, 0, 0, 0.0)),
695 valid_until: Some(civil(2021, 1, 1, 0, 0, 0.0)),
696 frequencies: vec![
697 SatelliteAntennaFrequency {
698 label: "G01".to_string(),
699 pco_m: [0.1, -0.2, 1.0],
700 noazi_pcv_m: vec![(0.0, 0.001), (5.0, 0.002), (10.0, 0.004)],
701 },
702 SatelliteAntennaFrequency {
703 label: "G02".to_string(),
704 pco_m: [-0.1, 0.3, 0.5],
705 noazi_pcv_m: vec![(0.0, -0.001), (5.0, -0.002), (10.0, -0.003)],
706 },
707 ],
708 }],
709 }
710 }
711
712 fn windup_epoch(sat: GnssSatelliteId, freq1_hz: f64, freq2_hz: f64) -> PppCorrectionEpoch {
713 let epoch = civil(2020, 6, 24, 12, 0, 0.0);
714 let (jd_whole, jd_fraction) = split_jd(epoch);
715 PppCorrectionEpoch {
716 epoch,
717 t_rx_j2000_s: j2000_seconds_from_split(jd_whole, jd_fraction)
718 .expect("valid split Julian date"),
719 observations: vec![PppCorrectionObservation {
720 sat,
721 freq1_hz,
722 freq2_hz,
723 }],
724 }
725 }
726
727 #[test]
728 fn ppp_corrections_match_elixir_reference_fixture() {
729 let sp3 = sp3_fixture();
730 let sat = GnssSatelliteId::new(GnssSystem::Gps, 21).expect("valid satellite id");
731 let epoch = civil(2020, 6, 24, 12, 0, 0.0);
732 let (jd_whole, jd_fraction) = split_jd(epoch);
733 let receiver = [3_512_900.0, 780_500.0, 5_248_700.0];
734 let epochs = vec![PppCorrectionEpoch {
735 epoch,
736 t_rx_j2000_s: j2000_seconds_from_split(jd_whole, jd_fraction)
737 .expect("valid split Julian date"),
738 observations: vec![PppCorrectionObservation {
739 sat,
740 freq1_hz: F_L1_HZ,
741 freq2_hz: F_L2_HZ,
742 }],
743 }];
744 let options = PppCorrectionsOptions {
745 solid_earth_tide: true,
746 phase_windup: true,
747 satellite_antenna: Some(fake_antenna_options(sat)),
748 };
749
750 let got = build(&sp3, &epochs, receiver, &options).expect("valid PPP corrections");
751
752 assert_eq!(got.tide.len(), 1);
753 assert_eq!(
754 got.tide[0].vector_m.map(f64::to_bits),
755 [0x3FB8BC98E788ED00, 0x3FAA54D8C1097508, 0x3FB03498C46B3B50]
756 );
757 assert_eq!(got.windup_m.len(), 1);
758 assert_eq!(got.windup_m[0].value_m.to_bits(), 0xBF808DE79DBD2C16);
759 assert_eq!(got.sat_pco_ecef.len(), 1);
760 assert_eq!(
761 got.sat_pco_ecef[0].vector_m.map(f64::to_bits),
762 [0xBFE58ED947570048, 0x3FDEDBB280CEB1BE, 0xBFFE3BCA6A354E4A]
763 );
764 assert_eq!(got.sat_pcv_m.len(), 1);
765 assert_eq!(got.sat_pcv_m[0].value_m.to_bits(), 0x3F77617E95BD232C);
766 }
767
768 #[test]
769 fn phase_windup_rejects_invalid_observation_frequency_pairs() {
770 let sp3 = sp3_fixture();
771 let sat = GnssSatelliteId::new(GnssSystem::Gps, 21).expect("valid satellite id");
772 let receiver = [3_512_900.0, 780_500.0, 5_248_700.0];
773 let options = PppCorrectionsOptions {
774 solid_earth_tide: false,
775 phase_windup: true,
776 satellite_antenna: None,
777 };
778 let cases = [
779 (0.0, F_L2_HZ, "phase wind-up freq1_hz", "not positive"),
780 (-F_L1_HZ, F_L2_HZ, "phase wind-up freq1_hz", "not positive"),
781 (
782 F_L1_HZ,
783 F_L1_HZ,
784 "phase wind-up frequency pair",
785 "must differ",
786 ),
787 ];
788
789 for (freq1_hz, freq2_hz, field, reason) in cases {
790 let epochs = vec![windup_epoch(sat, freq1_hz, freq2_hz)];
791 let err = build(&sp3, &epochs, receiver, &options)
792 .expect_err("invalid phase wind-up frequencies must error");
793
794 assert_eq!(
795 err,
796 PppCorrectionsError::WindupFrequency {
797 epoch_index: 0,
798 sat,
799 field,
800 reason,
801 }
802 );
803 }
804 }
805
806 #[test]
807 fn phase_windup_observation_frequency_pair_computes_finite_correction() {
808 let sp3 = sp3_fixture();
809 let sat = GnssSatelliteId::new(GnssSystem::Gps, 21).expect("valid satellite id");
810 let receiver = [3_512_900.0, 780_500.0, 5_248_700.0];
811 let options = PppCorrectionsOptions {
812 solid_earth_tide: false,
813 phase_windup: true,
814 satellite_antenna: None,
815 };
816 let epochs = vec![windup_epoch(sat, F_L1_HZ, F_L2_HZ)];
817
818 let got =
819 build(&sp3, &epochs, receiver, &options).expect("valid phase wind-up frequencies");
820
821 assert_eq!(got.windup_m.len(), 1);
822 assert!(got.windup_m[0].value_m.is_finite());
823 }
824
825 #[test]
826 fn satellite_antenna_rejects_invalid_frequency_pairs_without_windup() {
827 let sp3 = sp3_fixture();
828 let sat = GnssSatelliteId::new(GnssSystem::Gps, 21).expect("valid satellite id");
829 let receiver = [3_512_900.0, 780_500.0, 5_248_700.0];
830 let cases = [
831 (0.0, F_L2_HZ, "satellite antenna freq1_hz", "not positive"),
832 (
833 F_L1_HZ,
834 f64::INFINITY,
835 "satellite antenna freq2_hz",
836 "not finite",
837 ),
838 (
839 f64::NAN,
840 F_L2_HZ,
841 "satellite antenna freq1_hz",
842 "not finite",
843 ),
844 (
845 F_L1_HZ,
846 F_L1_HZ,
847 "satellite antenna frequency pair",
848 "must differ",
849 ),
850 ];
851
852 for (freq1_hz, freq2_hz, field, reason) in cases {
853 let mut antenna = fake_antenna_options(sat);
854 antenna.freq1_hz = freq1_hz;
855 antenna.freq2_hz = freq2_hz;
856 let options = PppCorrectionsOptions {
857 solid_earth_tide: false,
858 phase_windup: false,
859 satellite_antenna: Some(antenna),
860 };
861 let epochs = vec![windup_epoch(sat, F_L1_HZ, F_L2_HZ)];
862
863 let err = build(&sp3, &epochs, receiver, &options)
864 .expect_err("invalid satellite antenna frequencies must error");
865
866 assert_eq!(
867 err,
868 PppCorrectionsError::SatelliteAntennaFrequency { field, reason }
869 );
870 }
871 }
872
873 #[test]
874 fn satellite_antenna_frequency_pair_computes_finite_corrections_without_windup() {
875 let sp3 = sp3_fixture();
876 let sat = GnssSatelliteId::new(GnssSystem::Gps, 21).expect("valid satellite id");
877 let receiver = [3_512_900.0, 780_500.0, 5_248_700.0];
878 let options = PppCorrectionsOptions {
879 solid_earth_tide: false,
880 phase_windup: false,
881 satellite_antenna: Some(fake_antenna_options(sat)),
882 };
883 let epochs = vec![windup_epoch(sat, F_L1_HZ, F_L2_HZ)];
884
885 let got =
886 build(&sp3, &epochs, receiver, &options).expect("valid satellite antenna frequencies");
887
888 assert!(got.windup_m.is_empty());
889 assert_eq!(got.sat_pco_ecef.len(), 1);
890 assert!(got.sat_pco_ecef[0]
891 .vector_m
892 .iter()
893 .all(|value| value.is_finite()));
894 assert_eq!(got.sat_pcv_m.len(), 1);
895 assert!(got.sat_pcv_m[0].value_m.is_finite());
896 }
897
898 #[test]
899 fn satellite_antenna_rejects_non_finite_pcv_samples() {
900 let sp3 = sp3_fixture();
901 let sat = GnssSatelliteId::new(GnssSystem::Gps, 21).expect("valid satellite id");
902 let receiver = [3_512_900.0, 780_500.0, 5_248_700.0];
903 let mut antenna = fake_antenna_options(sat);
904 antenna.antennas[0].frequencies[0].noazi_pcv_m[1] = (5.0, f64::NAN);
905 let options = PppCorrectionsOptions {
906 solid_earth_tide: false,
907 phase_windup: false,
908 satellite_antenna: Some(antenna),
909 };
910 let epochs = vec![windup_epoch(sat, F_L1_HZ, F_L2_HZ)];
911
912 let err = build(&sp3, &epochs, receiver, &options)
913 .expect_err("non-finite satellite PCV samples must error");
914
915 assert_eq!(
916 err,
917 PppCorrectionsError::InvalidInput {
918 field: "satellite antenna noazi_pcv_m",
919 reason: "not finite",
920 }
921 );
922 }
923
924 #[test]
925 fn satellite_antenna_empty_pcv_grid_is_not_materialized_as_zero() {
926 let sp3 = sp3_fixture();
927 let sat = GnssSatelliteId::new(GnssSystem::Gps, 21).expect("valid satellite id");
928 let epoch = civil(2020, 6, 24, 12, 0, 0.0);
929 let (jd_whole, jd_fraction) = split_jd(epoch);
930 let receiver = [3_512_900.0, 780_500.0, 5_248_700.0];
931 let epochs = vec![PppCorrectionEpoch {
932 epoch,
933 t_rx_j2000_s: j2000_seconds_from_split(jd_whole, jd_fraction)
934 .expect("valid split Julian date"),
935 observations: vec![PppCorrectionObservation {
936 sat,
937 freq1_hz: F_L1_HZ,
938 freq2_hz: F_L2_HZ,
939 }],
940 }];
941 let mut antenna = fake_antenna_options(sat);
942 antenna.antennas[0].frequencies[0].noazi_pcv_m.clear();
943 let options = PppCorrectionsOptions {
944 solid_earth_tide: false,
945 phase_windup: false,
946 satellite_antenna: Some(antenna),
947 };
948
949 let got = build(&sp3, &epochs, receiver, &options).expect("valid PPP corrections");
950
951 assert!(got.sat_pco_ecef.is_empty());
952 assert!(got.sat_pcv_m.is_empty());
953 }
954
955 #[test]
956 fn build_rejects_non_finite_receive_time_for_satellite_corrections() {
957 let sp3 = sp3_fixture();
958 let sat = GnssSatelliteId::new(GnssSystem::Gps, 21).expect("valid satellite id");
959 let options = PppCorrectionsOptions {
960 solid_earth_tide: false,
961 phase_windup: true,
962 satellite_antenna: None,
963 };
964 let epochs = vec![PppCorrectionEpoch {
965 epoch: civil(2020, 6, 24, 12, 0, 0.0),
966 t_rx_j2000_s: f64::NAN,
967 observations: vec![PppCorrectionObservation {
968 sat,
969 freq1_hz: F_L1_HZ,
970 freq2_hz: F_L2_HZ,
971 }],
972 }];
973
974 let err = build(
975 &sp3,
976 &epochs,
977 [3_512_900.0, 780_500.0, 5_248_700.0],
978 &options,
979 )
980 .expect_err("non-finite receive time must be reported");
981
982 assert_eq!(
983 err,
984 PppCorrectionsError::InvalidInput {
985 field: "t_rx_j2000_s",
986 reason: "not finite",
987 }
988 );
989 }
990
991 #[test]
992 fn noazi_pcv_interpolation_clamps_and_interpolates() {
993 let samples = vec![(10.0, 4.0), (0.0, 1.0), (5.0, 2.0)];
994
995 assert_eq!(interpolate_samples(&samples, -1.0), Some(1.0));
996 assert_eq!(interpolate_samples(&samples, 2.5), Some(1.5));
997 assert_eq!(interpolate_samples(&samples, 99.0), Some(4.0));
998 }
999
1000 #[test]
1001 fn build_rejects_invalid_receiver_state_before_disabled_short_circuit() {
1002 let sp3 = sp3_fixture();
1003 let options = PppCorrectionsOptions {
1004 solid_earth_tide: false,
1005 phase_windup: false,
1006 satellite_antenna: None,
1007 };
1008
1009 for (receiver, field, reason) in [
1010 (
1011 [f64::NAN, 780_500.0, 5_248_700.0],
1012 "receiver_ecef_m",
1013 "not finite",
1014 ),
1015 ([0.0, 0.0, 0.0], "receiver radius_m", "not positive"),
1016 ] {
1017 let err = build(&sp3, &[], receiver, &options)
1018 .expect_err("invalid receiver state must error before empty success");
1019
1020 assert_eq!(err, PppCorrectionsError::InvalidInput { field, reason });
1021 }
1022 }
1023
1024 #[test]
1025 fn build_rejects_invalid_correction_epoch_without_panicking() {
1026 let sp3 = sp3_fixture();
1027 let options = PppCorrectionsOptions {
1028 solid_earth_tide: true,
1029 phase_windup: false,
1030 satellite_antenna: None,
1031 };
1032 let epochs = vec![PppCorrectionEpoch {
1033 epoch: civil(2021, 2, 29, 12, 0, 0.0),
1034 t_rx_j2000_s: 0.0,
1035 observations: Vec::new(),
1036 }];
1037
1038 let err = build(
1039 &sp3,
1040 &epochs,
1041 [3_512_900.0, 780_500.0, 5_248_700.0],
1042 &options,
1043 )
1044 .expect_err("invalid PPP correction epoch must return an error");
1045
1046 assert_eq!(
1047 err,
1048 PppCorrectionsError::Epoch {
1049 epoch_index: 0,
1050 source: CoverageError::InvalidInput {
1051 field: "civil datetime",
1052 kind: TimeScaleInputErrorKind::InvalidCivilDate,
1053 },
1054 }
1055 );
1056 }
1057
1058 #[test]
1059 fn build_rejects_non_finite_correction_epoch_without_panicking() {
1060 let sp3 = sp3_fixture();
1061 let options = PppCorrectionsOptions {
1062 solid_earth_tide: true,
1063 phase_windup: false,
1064 satellite_antenna: None,
1065 };
1066 let epochs = vec![PppCorrectionEpoch {
1067 epoch: civil(2020, 6, 24, 12, 0, f64::NAN),
1068 t_rx_j2000_s: 0.0,
1069 observations: Vec::new(),
1070 }];
1071
1072 let err = build(
1073 &sp3,
1074 &epochs,
1075 [3_512_900.0, 780_500.0, 5_248_700.0],
1076 &options,
1077 )
1078 .expect_err("non-finite PPP correction epoch must return an error");
1079
1080 assert_eq!(
1081 err,
1082 PppCorrectionsError::Epoch {
1083 epoch_index: 0,
1084 source: CoverageError::InvalidInput {
1085 field: "civil datetime",
1086 kind: TimeScaleInputErrorKind::NonFinite,
1087 },
1088 }
1089 );
1090 }
1091
1092 #[test]
1093 fn build_rejects_correction_epoch_after_eop_coverage() {
1094 let sp3 = sp3_fixture();
1095 let options = PppCorrectionsOptions {
1096 solid_earth_tide: true,
1097 phase_windup: false,
1098 satellite_antenna: None,
1099 };
1100 let epochs = vec![PppCorrectionEpoch {
1101 epoch: civil(2100, 1, 1, 0, 0, 0.0),
1102 t_rx_j2000_s: 0.0,
1103 observations: Vec::new(),
1104 }];
1105
1106 let err = build(
1107 &sp3,
1108 &epochs,
1109 [3_512_900.0, 780_500.0, 5_248_700.0],
1110 &options,
1111 )
1112 .expect_err("post-coverage PPP correction epoch must return an error");
1113
1114 assert_eq!(
1115 err,
1116 PppCorrectionsError::Epoch {
1117 epoch_index: 0,
1118 source: CoverageError::OutsideCoverage(
1119 crate::astro::time::DegradeReason::AfterCoverage
1120 ),
1121 }
1122 );
1123 }
1124
1125 #[test]
1126 fn build_accepts_valid_correction_epoch() {
1127 let sp3 = sp3_fixture();
1128 let options = PppCorrectionsOptions {
1129 solid_earth_tide: true,
1130 phase_windup: false,
1131 satellite_antenna: None,
1132 };
1133 let epochs = vec![PppCorrectionEpoch {
1134 epoch: civil(2020, 6, 24, 12, 0, 0.0),
1135 t_rx_j2000_s: 0.0,
1136 observations: Vec::new(),
1137 }];
1138
1139 let got = build(
1140 &sp3,
1141 &epochs,
1142 [3_512_900.0, 780_500.0, 5_248_700.0],
1143 &options,
1144 )
1145 .expect("valid PPP correction epoch must build");
1146
1147 assert_eq!(got.tide.len(), 1);
1148 }
1149
1150 #[test]
1151 fn build_rejects_degenerate_receiver_state_before_tide() {
1152 let sp3 = sp3_fixture();
1153 let epoch = civil(2020, 6, 24, 12, 0, 0.0);
1154 let options = PppCorrectionsOptions {
1155 solid_earth_tide: true,
1156 phase_windup: false,
1157 satellite_antenna: None,
1158 };
1159 let epochs = vec![PppCorrectionEpoch {
1160 epoch,
1161 t_rx_j2000_s: 0.0,
1162 observations: Vec::new(),
1163 }];
1164
1165 let err = build(&sp3, &epochs, [0.0, 0.0, 0.0], &options)
1166 .expect_err("degenerate tide geometry must error");
1167
1168 assert_eq!(
1169 err,
1170 PppCorrectionsError::InvalidInput {
1171 field: "receiver radius_m",
1172 reason: "not positive",
1173 }
1174 );
1175 }
1176}