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 glonass_channels: BTreeMap::new(),
373 met: options.spp_met,
374 robust: None,
375 }
376}
377
378fn mean_position(positions: &[[f64; 3]]) -> [f64; 3] {
385 let mut sum = [0.0_f64; 3];
386 for position in positions.iter().rev() {
387 sum[0] += position[0];
388 sum[1] += position[1];
389 sum[2] += position[2];
390 }
391 let n = positions.len() as f64;
392 [sum[0] / n, sum[1] / n, sum[2] / n]
393}
394
395fn initial_ambiguities(epochs: &[FloatEpoch]) -> BTreeMap<String, f64> {
402 let mut ambiguities = BTreeMap::new();
403 for epoch in epochs {
404 for obs in &epoch.observations {
405 ambiguities
406 .entry(obs.ambiguity_id.clone())
407 .or_insert(obs.phase_m - obs.code_m);
408 }
409 }
410 ambiguities
411}
412
413#[cfg(test)]
414mod tests {
415 use super::*;
416 use crate::astro::math::vec3::{norm3, sub3};
417 use crate::constants::F_L1_HZ;
418 use crate::estimation::strategies::{
419 estimate as estimate_with_strategy, EstimateInput, EstimateOptions, EstimateOutput,
420 };
421 use crate::observables::{predict, ObservableState, ObservablesError, PredictOptions};
422 use crate::ppp_corrections::CivilDateTime;
423 use crate::precise_positioning::{
424 FixedAmbiguityOptions, FixedSolveConfig, FloatObservation, FloatSolution,
425 FloatSolveOptions, MeasurementWeights, RangeCorrections, TroposphereOptions,
426 };
427 use crate::{GnssSatelliteId, GnssSystem};
428 use std::collections::BTreeMap;
429
430 struct SeedSource {
432 states: BTreeMap<GnssSatelliteId, [f64; 3]>,
433 }
434
435 impl ObservableEphemerisSource for SeedSource {
436 fn observable_state_at_j2000_s(
437 &self,
438 sat: GnssSatelliteId,
439 _t_j2000_s: f64,
440 ) -> Result<ObservableState, ObservablesError> {
441 Ok(ObservableState {
442 position_ecef_m: self
443 .states
444 .get(&sat)
445 .copied()
446 .ok_or(ObservablesError::NoEphemeris)?,
447 clock_s: Some(0.0),
448 })
449 }
450 }
451
452 impl EphemerisSource for SeedSource {
453 fn position_clock_at_j2000_s(
454 &self,
455 sat: GnssSatelliteId,
456 _t_j2000_s: f64,
457 ) -> Option<([f64; 3], f64)> {
458 self.states
459 .get(&sat)
460 .copied()
461 .map(|position| (position, 0.0))
462 }
463 }
464
465 fn sat_layout() -> [(u8, [f64; 3]); 6] {
466 [
470 (1, [14_350_000.0, 3_190_000.0, 21_440_000.0]),
471 (2, [20_000_000.0, 3_000_000.0, 18_000_000.0]),
472 (3, [9_000_000.0, 9_000_000.0, 22_000_000.0]),
473 (4, [16_000_000.0, -4_000_000.0, 21_000_000.0]),
474 (5, [10_000_000.0, -2_000_000.0, 24_000_000.0]),
475 (6, [19_000_000.0, 8_000_000.0, 17_000_000.0]),
476 ]
477 }
478
479 fn source_and_ids() -> (SeedSource, Vec<GnssSatelliteId>) {
480 let layout = sat_layout();
481 let ids: Vec<GnssSatelliteId> = layout
482 .iter()
483 .map(|(prn, _)| GnssSatelliteId::new(GnssSystem::Gps, *prn).expect("valid prn"))
484 .collect();
485 let states = ids
486 .iter()
487 .zip(layout.iter())
488 .map(|(id, (_, position))| (*id, *position))
489 .collect();
490 (SeedSource { states }, ids)
491 }
492
493 fn make_epoch(
494 source: &SeedSource,
495 ids: &[GnssSatelliteId],
496 truth: [f64; 3],
497 clock_m: f64,
498 ambiguities_m: &BTreeMap<String, f64>,
499 t_rx_j2000_s: f64,
500 ) -> FloatEpoch {
501 let observations = ids
502 .iter()
503 .map(|id| {
504 let prediction = predict(
505 source,
506 *id,
507 truth,
508 t_rx_j2000_s,
509 PredictOptions {
510 carrier_hz: F_L1_HZ,
511 light_time: true,
512 sagnac: true,
513 },
514 )
515 .expect("prediction");
516 let code_m = prediction.geometric_range_m + clock_m;
517 let ambiguity_m = ambiguities_m[&id.to_string()];
518 FloatObservation {
519 sat: *id,
520 satellite_id: id.to_string(),
521 ambiguity_id: id.to_string(),
522 code_m,
523 phase_m: code_m + ambiguity_m,
524 freq1_hz: 0.0,
525 freq2_hz: 0.0,
526 }
527 })
528 .collect();
529 FloatEpoch {
530 epoch: CivilDateTime {
531 year: 2020,
532 month: 6,
533 day: 24,
534 hour: 12,
535 minute: 0,
536 second: 0.0,
537 },
538 jd_whole: 2_459_024.5,
539 jd_fraction: 0.5,
540 t_rx_j2000_s,
541 observations,
542 }
543 }
544
545 fn float_config() -> FloatSolveConfig {
546 FloatSolveConfig {
547 weights: MeasurementWeights {
548 code: 1.0,
549 phase: 100.0,
550 elevation_weighting: false,
551 },
552 tropo: TroposphereOptions::disabled(),
553 corrections: RangeCorrections::disabled(),
554 opts: FloatSolveOptions::default(),
555 residual_screen: false,
556 }
557 }
558
559 fn fixed_config(ids: &[GnssSatelliteId], wavelength_m: f64) -> FixedSolveConfig {
560 let wavelengths_m: BTreeMap<String, f64> = ids
561 .iter()
562 .map(|id| (id.to_string(), wavelength_m))
563 .collect();
564 let offsets_m: BTreeMap<String, f64> = ids.iter().map(|id| (id.to_string(), 0.0)).collect();
565 FixedSolveConfig {
566 weights: float_config().weights,
567 tropo: float_config().tropo,
568 corrections: float_config().corrections,
569 opts: float_config().opts,
570 ambiguity: FixedAmbiguityOptions {
571 wavelengths_m,
572 offsets_m,
573 ratio_threshold: super::super::defaults::RATIO_THRESHOLD,
574 },
575 }
576 }
577
578 fn manual_float_with_strategy(
579 source: &SeedSource,
580 epochs: &[FloatEpoch],
581 strategy: PppAutoInitStrategy,
582 ) -> FloatSolution {
583 let initial_state =
584 seed_state(source, epochs, PppAutoInitOptions::default()).expect("seed builds");
585 match estimate_with_strategy(
586 EstimateInput::PppFloat {
587 source,
588 epochs,
589 initial_state,
590 config: float_config(),
591 },
592 EstimateOptions::new(strategy.strategy_id()),
593 )
594 .expect("manual float strategy")
595 {
596 EstimateOutput::PppFloat(solution) => *solution,
597 _ => unreachable!("PPP float estimate returns PPP float output"),
598 }
599 }
600
601 #[test]
602 fn auto_init_float_recovers_truth() {
603 let (source, ids) = source_and_ids();
604 let truth = [3_512_900.0, 780_500.0, 5_248_700.0];
605 let ambiguities_m: BTreeMap<String, f64> = ids
606 .iter()
607 .enumerate()
608 .map(|(idx, id)| (id.to_string(), 0.25 + idx as f64 * 0.1))
609 .collect();
610 let clocks = [12.5, 13.0, 11.8];
611 let epochs: Vec<FloatEpoch> = clocks
612 .iter()
613 .enumerate()
614 .map(|(idx, &clock_m)| {
615 make_epoch(&source, &ids, truth, clock_m, &ambiguities_m, idx as f64)
616 })
617 .collect();
618
619 let solution = solve_ppp_auto_init_float(
620 &source,
621 &epochs,
622 PppAutoInitOptions::default(),
623 float_config(),
624 )
625 .expect("float arc solves");
626
627 let error_m = norm3(sub3(solution.position_m, truth));
628 assert!(error_m < 1.0e-3, "position error {error_m} m too large");
629 for (idx, id) in ids.iter().enumerate() {
630 let recovered = solution.ambiguities_m[&id.to_string()];
631 let expected = 0.25 + idx as f64 * 0.1;
632 assert!(
633 (recovered - expected).abs() < 1.0e-3,
634 "ambiguity {id} recovered {recovered} expected {expected}"
635 );
636 }
637 }
638
639 #[test]
640 fn auto_init_matches_explicit_float_solve() {
641 let (source, ids) = source_and_ids();
645 let truth = [3_512_900.0, 780_500.0, 5_248_700.0];
646 let ambiguities_m: BTreeMap<String, f64> = ids
647 .iter()
648 .enumerate()
649 .map(|(idx, id)| (id.to_string(), 0.4 + idx as f64 * 0.05))
650 .collect();
651 let clocks = [9.5, 10.25];
652 let epochs: Vec<FloatEpoch> = clocks
653 .iter()
654 .enumerate()
655 .map(|(idx, &clock_m)| {
656 make_epoch(&source, &ids, truth, clock_m, &ambiguities_m, idx as f64)
657 })
658 .collect();
659
660 let driven = solve_ppp_auto_init_float(
661 &source,
662 &epochs,
663 PppAutoInitOptions::default(),
664 float_config(),
665 )
666 .expect("driver solves");
667
668 let hand_state =
669 seed_state(&source, &epochs, PppAutoInitOptions::default()).expect("seed builds");
670 let by_hand =
671 solve_float_epochs(&source, &epochs, hand_state, float_config()).expect("hand solve");
672 assert_eq!(driven, by_hand);
673 }
674
675 #[test]
676 fn auto_init_float_with_strategy_matches_manual_strategy_composition() {
677 let (source, ids) = source_and_ids();
678 let truth = [3_512_900.0, 780_500.0, 5_248_700.0];
679 let ambiguities_m: BTreeMap<String, f64> = ids
680 .iter()
681 .enumerate()
682 .map(|(idx, id)| (id.to_string(), 0.35 + idx as f64 * 0.07))
683 .collect();
684 let epochs: Vec<FloatEpoch> = [8.5, 9.25, 8.9]
685 .iter()
686 .enumerate()
687 .map(|(idx, &clock_m)| {
688 make_epoch(&source, &ids, truth, clock_m, &ambiguities_m, idx as f64)
689 })
690 .collect();
691
692 for strategy in [
693 PppAutoInitStrategy::Reference,
694 PppAutoInitStrategy::Canonical,
695 ] {
696 let driven = solve_ppp_auto_init_float_with_strategy(
697 &source,
698 &epochs,
699 PppAutoInitOptions::default(),
700 float_config(),
701 strategy,
702 )
703 .expect("strategy driver solves");
704 let manual = manual_float_with_strategy(&source, &epochs, strategy);
705 assert_eq!(driven, manual);
706 }
707 }
708
709 #[test]
710 fn auto_init_fixed_holds_integers() {
711 let (source, ids) = source_and_ids();
712 let truth = [3_512_900.0, 780_500.0, 5_248_700.0];
713 let wavelength_m = C_M_S / F_L1_HZ;
714 let cycles = [5i64, -3, 8, 2, -6, 4];
716 let ambiguities_m: BTreeMap<String, f64> = ids
717 .iter()
718 .zip(cycles.iter())
719 .map(|(id, &n)| (id.to_string(), n as f64 * wavelength_m))
720 .collect();
721 let clocks = [12.5, 12.7, 12.6];
722 let epochs: Vec<FloatEpoch> = clocks
723 .iter()
724 .enumerate()
725 .map(|(idx, &clock_m)| {
726 make_epoch(&source, &ids, truth, clock_m, &ambiguities_m, idx as f64)
727 })
728 .collect();
729
730 let fixed_config = fixed_config(&ids, wavelength_m);
731
732 let fixed = solve_ppp_auto_init_fixed(
733 &source,
734 &epochs,
735 PppAutoInitOptions::default(),
736 float_config(),
737 fixed_config,
738 )
739 .expect("fixed arc solves");
740
741 let error_m = norm3(sub3(fixed.position_m, truth));
742 assert!(
743 error_m < 1.0e-3,
744 "fixed position error {error_m} m too large"
745 );
746 for (id, &n) in ids.iter().zip(cycles.iter()) {
747 let held = fixed.fixed_ambiguities_cycles[&id.to_string()];
748 assert_eq!(held, n, "satellite {id} integer cycle");
749 }
750 }
751
752 #[test]
753 fn auto_init_fixed_with_strategy_matches_manual_strategy_composition() {
754 let (source, ids) = source_and_ids();
755 let truth = [3_512_900.0, 780_500.0, 5_248_700.0];
756 let wavelength_m = C_M_S / F_L1_HZ;
757 let cycles = [4i64, -2, 6, 1, -5, 3];
758 let ambiguities_m: BTreeMap<String, f64> = ids
759 .iter()
760 .zip(cycles.iter())
761 .map(|(id, &n)| (id.to_string(), n as f64 * wavelength_m))
762 .collect();
763 let epochs: Vec<FloatEpoch> = [11.5, 11.7, 11.6]
764 .iter()
765 .enumerate()
766 .map(|(idx, &clock_m)| {
767 make_epoch(&source, &ids, truth, clock_m, &ambiguities_m, idx as f64)
768 })
769 .collect();
770 let strategy = PppAutoInitStrategy::Canonical;
771 let fixed_config = fixed_config(&ids, wavelength_m);
772
773 let driven = solve_ppp_auto_init_fixed_with_strategy(
774 &source,
775 &epochs,
776 PppAutoInitOptions::default(),
777 float_config(),
778 fixed_config.clone(),
779 strategy,
780 )
781 .expect("strategy fixed driver solves");
782 let manual_float = manual_float_with_strategy(&source, &epochs, strategy);
783 let manual = match estimate_with_strategy(
784 EstimateInput::PppFixed {
785 source: &source,
786 epochs: &epochs,
787 float_solution: manual_float,
788 config: fixed_config,
789 },
790 EstimateOptions::new(strategy.strategy_id()),
791 )
792 .expect("manual fixed strategy")
793 {
794 EstimateOutput::PppFixed(solution) => *solution,
795 _ => unreachable!("PPP fixed estimate returns PPP fixed output"),
796 };
797
798 assert_eq!(driven, manual);
799 }
800}