1use std::collections::BTreeMap;
33
34use crate::astro::time::{day_of_year, second_of_day};
35use crate::constants::C_M_S;
36use crate::estimation::recipe::{StrategyId, Technique};
37use crate::estimation::strategies::{
38 estimate, EstimateError, EstimateInput, EstimateOptions, EstimateOutput,
39};
40use crate::observables::ObservableEphemerisSource;
41use crate::spp::{
42 self, Corrections, EphemerisSource, KlobucharCoeffs, Observation as SppObservation, SppError,
43 SurfaceMet,
44};
45
46use super::{
47 solve_fixed_from_float, solve_float_epochs, FixedSolution, FixedSolveConfig, FixedSolveError,
48 FloatEpoch, FloatSolution, FloatSolveConfig, FloatSolveError, FloatState,
49};
50
51#[derive(Debug, Clone, Copy, PartialEq)]
53pub struct PppInitialGuess {
54 pub position_m: [f64; 3],
56 pub clock_m: f64,
58}
59
60#[derive(Debug, Clone, Copy, PartialEq)]
62pub struct PppAutoInitOptions {
63 pub initial_guess: Option<PppInitialGuess>,
66 pub spp_initial_guess: [f64; 4],
69 pub spp_troposphere: bool,
72 pub spp_met: SurfaceMet,
74}
75
76impl Default for PppAutoInitOptions {
77 fn default() -> Self {
81 Self {
82 initial_guess: None,
83 spp_initial_guess: [0.0; 4],
84 spp_troposphere: false,
85 spp_met: SurfaceMet {
86 pressure_hpa: 1013.25,
87 temperature_k: 288.15,
88 relative_humidity: 0.5,
89 },
90 }
91 }
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
96pub enum PppAutoInitStrategy {
97 #[default]
99 Reference,
100 Canonical,
102}
103
104impl PppAutoInitStrategy {
105 const fn strategy_id(self) -> StrategyId {
106 match self {
107 Self::Reference => StrategyId::ppp_reference(),
108 Self::Canonical => StrategyId::Canonical {
109 technique: Technique::Ppp,
110 },
111 }
112 }
113}
114
115#[derive(Debug, Clone)]
117pub enum PppAutoInitError {
118 EmptyEpochs,
120 CodeSeedFailed {
123 epoch_index: usize,
125 source: SppError,
127 },
128 Float(FloatSolveError),
130 Fixed(FixedSolveError),
132}
133
134impl core::fmt::Display for PppAutoInitError {
135 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
136 match self {
137 Self::EmptyEpochs => write!(f, "PPP auto-init requires at least one epoch"),
138 Self::CodeSeedFailed {
139 epoch_index,
140 source,
141 } => write!(f, "PPP code seed failed at epoch {epoch_index}: {source}"),
142 Self::Float(error) => write!(f, "{error}"),
143 Self::Fixed(error) => write!(f, "{error}"),
144 }
145 }
146}
147
148impl std::error::Error for PppAutoInitError {
149 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
150 match self {
151 Self::CodeSeedFailed { source, .. } => Some(source),
152 Self::Float(error) => Some(error),
153 Self::Fixed(error) => Some(error),
154 Self::EmptyEpochs => None,
155 }
156 }
157}
158
159pub fn solve_ppp_auto_init_float<S>(
167 source: &S,
168 epochs: &[FloatEpoch],
169 options: PppAutoInitOptions,
170 config: FloatSolveConfig,
171) -> Result<FloatSolution, PppAutoInitError>
172where
173 S: EphemerisSource + ObservableEphemerisSource,
174{
175 let initial_state = seed_state(source, epochs, options)?;
176 solve_float_epochs(source, epochs, initial_state, config).map_err(PppAutoInitError::Float)
177}
178
179pub fn solve_ppp_auto_init_float_with_strategy<S>(
182 source: &S,
183 epochs: &[FloatEpoch],
184 options: PppAutoInitOptions,
185 config: FloatSolveConfig,
186 strategy: PppAutoInitStrategy,
187) -> Result<FloatSolution, PppAutoInitError>
188where
189 S: EphemerisSource + ObservableEphemerisSource,
190{
191 let initial_state = seed_state(source, epochs, options)?;
192 solve_float_with_strategy(source, epochs, initial_state, config, strategy)
193}
194
195pub fn solve_ppp_auto_init_fixed<S>(
202 source: &S,
203 epochs: &[FloatEpoch],
204 options: PppAutoInitOptions,
205 float_config: FloatSolveConfig,
206 fixed_config: FixedSolveConfig,
207) -> Result<FixedSolution, PppAutoInitError>
208where
209 S: EphemerisSource + ObservableEphemerisSource,
210{
211 let float_solution = solve_ppp_auto_init_float(source, epochs, options, float_config)?;
212 solve_fixed_from_float(source, epochs, float_solution, fixed_config)
213 .map_err(PppAutoInitError::Fixed)
214}
215
216pub fn solve_ppp_auto_init_fixed_with_strategy<S>(
219 source: &S,
220 epochs: &[FloatEpoch],
221 options: PppAutoInitOptions,
222 float_config: FloatSolveConfig,
223 fixed_config: FixedSolveConfig,
224 strategy: PppAutoInitStrategy,
225) -> Result<FixedSolution, PppAutoInitError>
226where
227 S: EphemerisSource + ObservableEphemerisSource,
228{
229 let float_solution =
230 solve_ppp_auto_init_float_with_strategy(source, epochs, options, float_config, strategy)?;
231 solve_fixed_with_strategy(source, epochs, float_solution, fixed_config, strategy)
232}
233
234fn solve_float_with_strategy(
235 source: &dyn ObservableEphemerisSource,
236 epochs: &[FloatEpoch],
237 initial_state: FloatState,
238 config: FloatSolveConfig,
239 strategy: PppAutoInitStrategy,
240) -> Result<FloatSolution, PppAutoInitError> {
241 match estimate(
242 EstimateInput::PppFloat {
243 source,
244 epochs,
245 initial_state,
246 config,
247 },
248 EstimateOptions::new(strategy.strategy_id()),
249 ) {
250 Ok(EstimateOutput::PppFloat(solution)) => Ok(*solution),
251 Err(EstimateError::PppFloat(error)) => Err(PppAutoInitError::Float(error)),
252 Ok(_) | Err(_) => {
253 unreachable!("PPP float strategy produces a PPP float result or error")
254 }
255 }
256}
257
258fn solve_fixed_with_strategy(
259 source: &dyn ObservableEphemerisSource,
260 epochs: &[FloatEpoch],
261 float_solution: FloatSolution,
262 config: FixedSolveConfig,
263 strategy: PppAutoInitStrategy,
264) -> Result<FixedSolution, PppAutoInitError> {
265 match estimate(
266 EstimateInput::PppFixed {
267 source,
268 epochs,
269 float_solution,
270 config,
271 },
272 EstimateOptions::new(strategy.strategy_id()),
273 ) {
274 Ok(EstimateOutput::PppFixed(solution)) => Ok(*solution),
275 Err(EstimateError::PppFixed(error)) => Err(PppAutoInitError::Fixed(error)),
276 Ok(_) | Err(_) => {
277 unreachable!("PPP fixed strategy produces a PPP fixed result or error")
278 }
279 }
280}
281
282fn seed_state<S>(
285 source: &S,
286 epochs: &[FloatEpoch],
287 options: PppAutoInitOptions,
288) -> Result<FloatState, PppAutoInitError>
289where
290 S: EphemerisSource + ObservableEphemerisSource,
291{
292 if epochs.is_empty() {
293 return Err(PppAutoInitError::EmptyEpochs);
294 }
295 let (position_m, clocks_m) = match options.initial_guess {
296 Some(guess) => (guess.position_m, vec![guess.clock_m; epochs.len()]),
297 None => spp_seed(source, epochs, options)?,
298 };
299 Ok(FloatState {
300 position_m,
301 clocks_m,
302 ambiguities_m: initial_ambiguities(epochs),
303 ztd_m: 0.0,
304 })
305}
306
307fn spp_seed<S>(
311 source: &S,
312 epochs: &[FloatEpoch],
313 options: PppAutoInitOptions,
314) -> Result<([f64; 3], Vec<f64>), PppAutoInitError>
315where
316 S: EphemerisSource + ObservableEphemerisSource,
317{
318 let mut positions = Vec::with_capacity(epochs.len());
319 let mut clocks = Vec::with_capacity(epochs.len());
320 for (epoch_index, epoch) in epochs.iter().enumerate() {
321 let inputs = spp_seed_inputs(epoch, options);
322 let solution = spp::solve(source, &inputs, false).map_err(|source| {
323 PppAutoInitError::CodeSeedFailed {
324 epoch_index,
325 source,
326 }
327 })?;
328 positions.push(solution.position.as_array());
329 clocks.push(solution.rx_clock_s * C_M_S);
330 }
331 Ok((mean_position(&positions), clocks))
332}
333
334fn spp_seed_inputs(epoch: &FloatEpoch, options: PppAutoInitOptions) -> spp::SolveInputs {
337 let observations = epoch
338 .observations
339 .iter()
340 .map(|obs| SppObservation {
341 satellite_id: obs.sat,
342 pseudorange_m: obs.code_m,
343 })
344 .collect();
345 spp::SolveInputs {
346 observations,
347 t_rx_j2000_s: epoch.t_rx_j2000_s,
348 t_rx_second_of_day_s: second_of_day(
349 i32::from(epoch.epoch.hour),
350 i32::from(epoch.epoch.minute),
351 epoch.epoch.second,
352 ),
353 day_of_year: day_of_year(
354 epoch.epoch.year,
355 i32::from(epoch.epoch.month),
356 i32::from(epoch.epoch.day),
357 i32::from(epoch.epoch.hour),
358 i32::from(epoch.epoch.minute),
359 epoch.epoch.second,
360 ),
361 initial_guess: options.spp_initial_guess,
362 corrections: Corrections {
363 ionosphere: false,
364 troposphere: options.spp_troposphere,
365 },
366 klobuchar: KlobucharCoeffs {
367 alpha: [0.0; 4],
368 beta: [0.0; 4],
369 },
370 beidou_klobuchar: None,
371 galileo_nequick: None,
372 sbas_iono: None,
373 glonass_channels: BTreeMap::new(),
374 met: options.spp_met,
375 robust: None,
376 }
377}
378
379fn mean_position(positions: &[[f64; 3]]) -> [f64; 3] {
386 let mut sum = [0.0_f64; 3];
387 for position in positions.iter().rev() {
388 sum[0] += position[0];
389 sum[1] += position[1];
390 sum[2] += position[2];
391 }
392 let n = positions.len() as f64;
393 [sum[0] / n, sum[1] / n, sum[2] / n]
394}
395
396fn initial_ambiguities(epochs: &[FloatEpoch]) -> BTreeMap<String, f64> {
403 let mut ambiguities = BTreeMap::new();
404 for epoch in epochs {
405 for obs in &epoch.observations {
406 ambiguities
407 .entry(obs.ambiguity_id.clone())
408 .or_insert(obs.phase_m - obs.code_m);
409 }
410 }
411 ambiguities
412}
413
414#[cfg(test)]
415mod tests {
416 use super::*;
417 use crate::astro::math::vec3::{norm3, sub3};
418 use crate::constants::F_L1_HZ;
419 use crate::estimation::strategies::{
420 estimate as estimate_with_strategy, EstimateInput, EstimateOptions, EstimateOutput,
421 };
422 use crate::observables::{predict, ObservableState, ObservablesError, PredictOptions};
423 use crate::ppp_corrections::CivilDateTime;
424 use crate::precise_positioning::{
425 FixedAmbiguityOptions, FixedSolveConfig, FloatObservation, FloatSolution,
426 FloatSolveOptions, MeasurementWeights, RangeCorrections, TroposphereOptions,
427 };
428 use crate::{GnssSatelliteId, GnssSystem};
429 use std::collections::BTreeMap;
430
431 struct SeedSource {
433 states: BTreeMap<GnssSatelliteId, [f64; 3]>,
434 }
435
436 impl ObservableEphemerisSource for SeedSource {
437 fn observable_state_at_j2000_s(
438 &self,
439 sat: GnssSatelliteId,
440 _t_j2000_s: f64,
441 ) -> Result<ObservableState, ObservablesError> {
442 Ok(ObservableState {
443 position_ecef_m: self
444 .states
445 .get(&sat)
446 .copied()
447 .ok_or(ObservablesError::NoEphemeris)?,
448 clock_s: Some(0.0),
449 })
450 }
451 }
452
453 impl EphemerisSource for SeedSource {
454 fn position_clock_at_j2000_s(
455 &self,
456 sat: GnssSatelliteId,
457 _t_j2000_s: f64,
458 ) -> Option<([f64; 3], f64)> {
459 self.states
460 .get(&sat)
461 .copied()
462 .map(|position| (position, 0.0))
463 }
464 }
465
466 fn sat_layout() -> [(u8, [f64; 3]); 6] {
467 [
471 (1, [14_350_000.0, 3_190_000.0, 21_440_000.0]),
472 (2, [20_000_000.0, 3_000_000.0, 18_000_000.0]),
473 (3, [9_000_000.0, 9_000_000.0, 22_000_000.0]),
474 (4, [16_000_000.0, -4_000_000.0, 21_000_000.0]),
475 (5, [10_000_000.0, -2_000_000.0, 24_000_000.0]),
476 (6, [19_000_000.0, 8_000_000.0, 17_000_000.0]),
477 ]
478 }
479
480 fn source_and_ids() -> (SeedSource, Vec<GnssSatelliteId>) {
481 let layout = sat_layout();
482 let ids: Vec<GnssSatelliteId> = layout
483 .iter()
484 .map(|(prn, _)| GnssSatelliteId::new(GnssSystem::Gps, *prn).expect("valid prn"))
485 .collect();
486 let states = ids
487 .iter()
488 .zip(layout.iter())
489 .map(|(id, (_, position))| (*id, *position))
490 .collect();
491 (SeedSource { states }, ids)
492 }
493
494 fn make_epoch(
495 source: &SeedSource,
496 ids: &[GnssSatelliteId],
497 truth: [f64; 3],
498 clock_m: f64,
499 ambiguities_m: &BTreeMap<String, f64>,
500 t_rx_j2000_s: f64,
501 ) -> FloatEpoch {
502 let observations = ids
503 .iter()
504 .map(|id| {
505 let prediction = predict(
506 source,
507 *id,
508 truth,
509 t_rx_j2000_s,
510 PredictOptions {
511 carrier_hz: F_L1_HZ,
512 light_time: true,
513 sagnac: true,
514 },
515 )
516 .expect("prediction");
517 let code_m = prediction.geometric_range_m + clock_m;
518 let ambiguity_m = ambiguities_m[&id.to_string()];
519 FloatObservation {
520 sat: *id,
521 satellite_id: id.to_string(),
522 ambiguity_id: id.to_string(),
523 code_m,
524 phase_m: code_m + ambiguity_m,
525 freq1_hz: 0.0,
526 freq2_hz: 0.0,
527 glonass_channel: None,
528 }
529 })
530 .collect();
531 FloatEpoch {
532 epoch: CivilDateTime {
533 year: 2020,
534 month: 6,
535 day: 24,
536 hour: 12,
537 minute: 0,
538 second: 0.0,
539 },
540 jd_whole: 2_459_024.5,
541 jd_fraction: 0.5,
542 t_rx_j2000_s,
543 observations,
544 }
545 }
546
547 fn float_config() -> FloatSolveConfig {
548 FloatSolveConfig {
549 weights: MeasurementWeights {
550 code: 1.0,
551 phase: 100.0,
552 elevation_weighting: false,
553 },
554 tropo: TroposphereOptions::disabled(),
555 corrections: RangeCorrections::disabled(),
556 opts: FloatSolveOptions::default(),
557 residual_screen: false,
558 }
559 }
560
561 fn fixed_config(ids: &[GnssSatelliteId], wavelength_m: f64) -> FixedSolveConfig {
562 let wavelengths_m: BTreeMap<String, f64> = ids
563 .iter()
564 .map(|id| (id.to_string(), wavelength_m))
565 .collect();
566 let offsets_m: BTreeMap<String, f64> = ids.iter().map(|id| (id.to_string(), 0.0)).collect();
567 FixedSolveConfig {
568 weights: float_config().weights,
569 tropo: float_config().tropo,
570 corrections: float_config().corrections,
571 opts: float_config().opts,
572 ambiguity: FixedAmbiguityOptions {
573 wavelengths_m,
574 offsets_m,
575 ratio_threshold: super::super::defaults::RATIO_THRESHOLD,
576 },
577 }
578 }
579
580 fn manual_float_with_strategy(
581 source: &SeedSource,
582 epochs: &[FloatEpoch],
583 strategy: PppAutoInitStrategy,
584 ) -> FloatSolution {
585 let initial_state =
586 seed_state(source, epochs, PppAutoInitOptions::default()).expect("seed builds");
587 match estimate_with_strategy(
588 EstimateInput::PppFloat {
589 source,
590 epochs,
591 initial_state,
592 config: float_config(),
593 },
594 EstimateOptions::new(strategy.strategy_id()),
595 )
596 .expect("manual float strategy")
597 {
598 EstimateOutput::PppFloat(solution) => *solution,
599 _ => unreachable!("PPP float estimate returns PPP float output"),
600 }
601 }
602
603 #[test]
604 fn auto_init_float_recovers_truth() {
605 let (source, ids) = source_and_ids();
606 let truth = [3_512_900.0, 780_500.0, 5_248_700.0];
607 let ambiguities_m: BTreeMap<String, f64> = ids
608 .iter()
609 .enumerate()
610 .map(|(idx, id)| (id.to_string(), 0.25 + idx as f64 * 0.1))
611 .collect();
612 let clocks = [12.5, 13.0, 11.8];
613 let epochs: Vec<FloatEpoch> = clocks
614 .iter()
615 .enumerate()
616 .map(|(idx, &clock_m)| {
617 make_epoch(&source, &ids, truth, clock_m, &ambiguities_m, idx as f64)
618 })
619 .collect();
620
621 let solution = solve_ppp_auto_init_float(
622 &source,
623 &epochs,
624 PppAutoInitOptions::default(),
625 float_config(),
626 )
627 .expect("float arc solves");
628
629 let error_m = norm3(sub3(solution.position_m, truth));
630 assert!(error_m < 1.0e-3, "position error {error_m} m too large");
631 for (idx, id) in ids.iter().enumerate() {
632 let recovered = solution.ambiguities_m[&id.to_string()];
633 let expected = 0.25 + idx as f64 * 0.1;
634 assert!(
635 (recovered - expected).abs() < 1.0e-3,
636 "ambiguity {id} recovered {recovered} expected {expected}"
637 );
638 }
639 }
640
641 #[test]
642 fn auto_init_matches_explicit_float_solve() {
643 let (source, ids) = source_and_ids();
647 let truth = [3_512_900.0, 780_500.0, 5_248_700.0];
648 let ambiguities_m: BTreeMap<String, f64> = ids
649 .iter()
650 .enumerate()
651 .map(|(idx, id)| (id.to_string(), 0.4 + idx as f64 * 0.05))
652 .collect();
653 let clocks = [9.5, 10.25];
654 let epochs: Vec<FloatEpoch> = clocks
655 .iter()
656 .enumerate()
657 .map(|(idx, &clock_m)| {
658 make_epoch(&source, &ids, truth, clock_m, &ambiguities_m, idx as f64)
659 })
660 .collect();
661
662 let driven = solve_ppp_auto_init_float(
663 &source,
664 &epochs,
665 PppAutoInitOptions::default(),
666 float_config(),
667 )
668 .expect("driver solves");
669
670 let hand_state =
671 seed_state(&source, &epochs, PppAutoInitOptions::default()).expect("seed builds");
672 let by_hand =
673 solve_float_epochs(&source, &epochs, hand_state, float_config()).expect("hand solve");
674 assert_eq!(driven, by_hand);
675 }
676
677 #[test]
678 fn auto_init_float_with_strategy_matches_manual_strategy_composition() {
679 let (source, ids) = source_and_ids();
680 let truth = [3_512_900.0, 780_500.0, 5_248_700.0];
681 let ambiguities_m: BTreeMap<String, f64> = ids
682 .iter()
683 .enumerate()
684 .map(|(idx, id)| (id.to_string(), 0.35 + idx as f64 * 0.07))
685 .collect();
686 let epochs: Vec<FloatEpoch> = [8.5, 9.25, 8.9]
687 .iter()
688 .enumerate()
689 .map(|(idx, &clock_m)| {
690 make_epoch(&source, &ids, truth, clock_m, &ambiguities_m, idx as f64)
691 })
692 .collect();
693
694 for strategy in [
695 PppAutoInitStrategy::Reference,
696 PppAutoInitStrategy::Canonical,
697 ] {
698 let driven = solve_ppp_auto_init_float_with_strategy(
699 &source,
700 &epochs,
701 PppAutoInitOptions::default(),
702 float_config(),
703 strategy,
704 )
705 .expect("strategy driver solves");
706 let manual = manual_float_with_strategy(&source, &epochs, strategy);
707 assert_eq!(driven, manual);
708 }
709 }
710
711 #[test]
712 fn auto_init_fixed_holds_integers() {
713 let (source, ids) = source_and_ids();
714 let truth = [3_512_900.0, 780_500.0, 5_248_700.0];
715 let wavelength_m = C_M_S / F_L1_HZ;
716 let cycles = [5i64, -3, 8, 2, -6, 4];
718 let ambiguities_m: BTreeMap<String, f64> = ids
719 .iter()
720 .zip(cycles.iter())
721 .map(|(id, &n)| (id.to_string(), n as f64 * wavelength_m))
722 .collect();
723 let clocks = [12.5, 12.7, 12.6];
724 let epochs: Vec<FloatEpoch> = clocks
725 .iter()
726 .enumerate()
727 .map(|(idx, &clock_m)| {
728 make_epoch(&source, &ids, truth, clock_m, &ambiguities_m, idx as f64)
729 })
730 .collect();
731
732 let fixed_config = fixed_config(&ids, wavelength_m);
733
734 let fixed = solve_ppp_auto_init_fixed(
735 &source,
736 &epochs,
737 PppAutoInitOptions::default(),
738 float_config(),
739 fixed_config,
740 )
741 .expect("fixed arc solves");
742
743 let error_m = norm3(sub3(fixed.position_m, truth));
744 assert!(
745 error_m < 1.0e-3,
746 "fixed position error {error_m} m too large"
747 );
748 for (id, &n) in ids.iter().zip(cycles.iter()) {
749 let held = fixed.fixed_ambiguities_cycles[&id.to_string()];
750 assert_eq!(held, n, "satellite {id} integer cycle");
751 }
752 }
753
754 #[test]
755 fn auto_init_fixed_with_strategy_matches_manual_strategy_composition() {
756 let (source, ids) = source_and_ids();
757 let truth = [3_512_900.0, 780_500.0, 5_248_700.0];
758 let wavelength_m = C_M_S / F_L1_HZ;
759 let cycles = [4i64, -2, 6, 1, -5, 3];
760 let ambiguities_m: BTreeMap<String, f64> = ids
761 .iter()
762 .zip(cycles.iter())
763 .map(|(id, &n)| (id.to_string(), n as f64 * wavelength_m))
764 .collect();
765 let epochs: Vec<FloatEpoch> = [11.5, 11.7, 11.6]
766 .iter()
767 .enumerate()
768 .map(|(idx, &clock_m)| {
769 make_epoch(&source, &ids, truth, clock_m, &ambiguities_m, idx as f64)
770 })
771 .collect();
772 let strategy = PppAutoInitStrategy::Canonical;
773 let fixed_config = fixed_config(&ids, wavelength_m);
774
775 let driven = solve_ppp_auto_init_fixed_with_strategy(
776 &source,
777 &epochs,
778 PppAutoInitOptions::default(),
779 float_config(),
780 fixed_config.clone(),
781 strategy,
782 )
783 .expect("strategy fixed driver solves");
784 let manual_float = manual_float_with_strategy(&source, &epochs, strategy);
785 let manual = match estimate_with_strategy(
786 EstimateInput::PppFixed {
787 source: &source,
788 epochs: &epochs,
789 float_solution: manual_float,
790 config: fixed_config,
791 },
792 EstimateOptions::new(strategy.strategy_id()),
793 )
794 .expect("manual fixed strategy")
795 {
796 EstimateOutput::PppFixed(solution) => *solution,
797 _ => unreachable!("PPP fixed estimate returns PPP fixed output"),
798 };
799
800 assert_eq!(driven, manual);
801 }
802}