1pub mod auto_init;
133pub mod cycle_slip;
134mod fixed;
135mod float;
136mod kinematic;
137mod model;
138mod normal;
139mod prep;
140pub mod raim;
141mod rows;
142pub mod tec;
143mod temporal;
144mod types;
145pub mod velocity;
146
147pub use crate::dop::PositionCovariance;
148pub use auto_init::{
149 solve_ppp_auto_init_fixed, solve_ppp_auto_init_fixed_with_strategy, solve_ppp_auto_init_float,
150 solve_ppp_auto_init_float_with_strategy, PppAutoInitError, PppAutoInitOptions,
151 PppAutoInitStrategy, PppInitialGuess,
152};
153pub use cycle_slip::{
154 detect_cycle_slips, geometry_free_m, melbourne_wubbena_cycles, update_geometry_free,
155 update_melbourne_wubbena, CycleSlipConfig, CycleSlipConfigError, CycleSlipDetectorState,
156 CycleSlipError, CycleSlipFlagEpoch, CycleSlipFlagObservation, CycleSlipStateKey,
157 GeometryFreeUpdate, MelbourneWubbenaUpdate, RunningMeanVariance, SatelliteCycleSlipState,
158 DEFAULT_MINIMUM_ARC_LENGTH, DEFAULT_RUNNING_STATISTIC_K_FACTOR,
159};
160pub(crate) use fixed::run_fixed_from_float;
161pub use fixed::solve_fixed_from_float;
162#[cfg(test)]
163use float::initial_ambiguities;
164pub(crate) use float::run_float_epochs;
165pub use float::{solve_float_epoch, solve_float_epochs};
166pub use kinematic::{
167 correct_kinematic_state, predict_kinematic_state, solve_kinematic_ppp, KinematicConfig,
168 KinematicEpochSolution, KinematicEpochStatus, KinematicMotionModel,
169 KinematicPositionProcessNoise, KinematicProcessNoise, KinematicSolveError, KinematicState,
170 KinematicUpdateSummary,
171};
172pub use prep::{
173 prepare_widelane_fixed_epochs, split_float_cycle_slip_epochs, DualFrequencyEpoch,
174 DualFrequencyObservation, FloatCycleSlipEpoch, FloatCycleSlipObservation,
175 FloatCycleSlipTaggedEpoch, FloatCycleSlipTaggedObservation, PppSplitArc, PreparedFloatEpoch,
176 PreparedFloatObservation, WideLanePrepError, WideLanePrepOptions, WideLanePrepResult,
177};
178pub use raim::{
179 solve_float_epoch_with_raim, ProtectionLevels, RaimConfig, RaimError, RaimFdeError,
180 RaimFdeResult, RaimFdeStatus, RaimGeometryRow, RaimIdentification, RaimResult, RaimStatus,
181 SatelliteTestStatistic,
182};
183pub use tec::{
184 code_geometry_free_m, estimate_code_slant_tec, estimate_phase_slant_tec, estimate_tec,
185 ionospheric_pierce_point, level_slant_tec_arc, phase_geometry_free_m,
186 slant_tec_from_code_geometry_free_m, slant_tec_from_phase_geometry_free_m,
187 thin_shell_mapping_function, vertical_tec_from_slant_tec, CodeSlantTecEstimate,
188 IonosphericPiercePoint, LeveledTecSample, PhaseSlantTecEstimate, TecConfig, TecEpoch, TecError,
189 TecEstimate, TecEstimateSample, TecLevelingResult, TecLevelingSample, TecObservation,
190 TecSatelliteArc, DEFAULT_IONOSPHERIC_SHELL_HEIGHT_M, ELECTRONS_PER_TECU_M2,
191 TEC_GROUP_DELAY_COEFFICIENT,
192};
193pub use types::*;
194pub use velocity::{
195 predict_range_rate_m_s, solve_velocity, RangeRatePrediction, ReceiverVelocityState,
196 VelocityConfig, VelocityObservation, VelocityRobustConfig, VelocitySolution,
197 VelocitySolveError,
198};
199
200pub use crate::ambiguity::CycleSlipPolicy;
201
202pub mod defaults {
216 pub const POSITION_TOLERANCE_M: f64 = 1.0e-4;
220
221 pub const CLOCK_TOLERANCE_M: f64 = 1.0e-4;
225
226 pub const AMBIGUITY_TOLERANCE_M: f64 = 1.0e-4;
230
231 pub const ZTD_TOLERANCE_M: f64 = 1.0e-4;
235
236 pub const MAX_ITERATIONS: usize = 8;
241
242 pub const RATIO_THRESHOLD: f64 = 3.0;
248}
249
250use std::collections::{BTreeMap, BTreeSet};
251
252use crate::constants::F_L1_HZ;
253use crate::estimation::recipe::NormalRecipe;
254use crate::observables::{predict, ObservableEphemerisSource, ObservablesError, PredictOptions};
255use crate::ppp_corrections::{
256 self, PppCorrectionEpoch, PppCorrectionObservation, PppCorrectionsError, PppCorrectionsOptions,
257};
258use crate::sp3::Sp3;
259use crate::validate::{self, FieldError};
260
261const MAX_PPP_ITERATIONS: usize = 10_000;
262
263pub fn build_ppp_lookup(
265 sp3: &Sp3,
266 epochs: &[FloatEpoch],
267 receiver_ecef_m: [f64; 3],
268 options: &PppCorrectionsOptions,
269) -> Result<PppCorrectionLookup, PppCorrectionsError> {
270 let ppp_epochs: Vec<PppCorrectionEpoch> = epochs
271 .iter()
272 .map(|epoch| PppCorrectionEpoch {
273 epoch: epoch.epoch,
274 t_rx_j2000_s: epoch.t_rx_j2000_s,
275 observations: epoch
276 .observations
277 .iter()
278 .map(|obs| PppCorrectionObservation {
279 sat: obs.sat,
280 freq1_hz: obs.freq1_hz,
281 freq2_hz: obs.freq2_hz,
282 glonass_channel: obs.glonass_channel,
283 })
284 .collect(),
285 })
286 .collect();
287 let corrections = ppp_corrections::build(sp3, &ppp_epochs, receiver_ecef_m, options)?;
288 Ok(PppCorrectionLookup::from_options(corrections, options))
289}
290
291impl FloatState {
292 fn default_for_epochs(epochs: &[FloatEpoch]) -> Self {
293 Self {
294 position_m: [0.0; 3],
295 clocks_m: vec![0.0; epochs.len()],
296 ambiguities_m: BTreeMap::new(),
297 ztd_m: 0.0,
298 tropo_gradient_north_m: 0.0,
299 tropo_gradient_east_m: 0.0,
300 residual_ionosphere_m: BTreeMap::new(),
301 }
302 }
303}
304
305#[derive(Clone, Copy)]
313struct ModelContext<'a> {
314 source: &'a dyn ObservableEphemerisSource,
315 weights: MeasurementWeights,
316 tropo: TroposphereOptions,
317 corrections: &'a RangeCorrections,
318 normal: NormalRecipe,
319 estimate_residual_ionosphere: bool,
320}
321
322fn predict_default(
323 _source: &dyn ObservableEphemerisSource,
324 _obs: &FloatObservation,
325) -> Result<PredictOptions, FloatSolveError> {
326 Ok(PredictOptions {
327 carrier_hz: F_L1_HZ,
328 light_time: true,
329 sagnac: true,
330 })
331}
332
333fn no_ephemeris(obs: &FloatObservation, error: ObservablesError) -> FloatSolveError {
334 FloatSolveError::NoEphemeris {
335 satellite_id: obs.satellite_id.clone(),
336 reason: match error {
337 ObservablesError::NoEphemeris => NoEphemerisReason::NoEphemeris,
338 ObservablesError::InvalidInput { .. } => NoEphemerisReason::Reason(error.to_string()),
339 ObservablesError::Ephemeris(err) => NoEphemerisReason::Reason(err.to_string()),
340 ObservablesError::Media(err) => NoEphemerisReason::Reason(err.to_string()),
341 },
342 }
343}
344
345fn missing_satellite_clock(obs: &FloatObservation) -> FloatSolveError {
346 FloatSolveError::NoEphemeris {
347 satellite_id: obs.satellite_id.clone(),
348 reason: NoEphemerisReason::MissingSatelliteClock,
349 }
350}
351
352fn missing_correction(obs: &FloatObservation, correction: MissingCorrection) -> FloatSolveError {
353 FloatSolveError::MissingCorrection {
354 satellite_id: obs.satellite_id.clone(),
355 correction,
356 }
357}
358
359fn invalid_clock_count(expected: usize, actual: usize) -> FloatSolveError {
360 FloatSolveError::InvalidClockCount { expected, actual }
361}
362
363fn invalid_solve_option(field: &'static str, reason: &'static str) -> FloatSolveError {
364 FloatSolveError::InvalidSolveOption { field, reason }
365}
366
367fn insufficient_after_elevation_cutoff(
368 cutoff_deg: f64,
369 retained_observations: usize,
370 required_observations: usize,
371) -> FloatSolveError {
372 FloatSolveError::InsufficientObservationsAfterElevationCutoff {
373 cutoff_deg,
374 retained_observations,
375 required_observations,
376 }
377}
378
379pub(super) fn invalid_input(error: FieldError) -> FloatSolveError {
380 invalid_input_field(error.field(), error.reason())
381}
382
383fn invalid_input_field(field: &'static str, reason: &'static str) -> FloatSolveError {
384 FloatSolveError::InvalidInput { field, reason }
385}
386
387fn invalid_fixed_input(error: FieldError) -> FixedSolveError {
388 FixedSolveError::Float(invalid_input(error))
389}
390
391pub(super) fn validate_float_solve_boundary(
392 epochs: &[FloatEpoch],
393 state: &FloatState,
394 config: &FloatSolveConfig,
395) -> Result<(), FloatSolveError> {
396 validate_epochs(epochs)?;
397 validate_float_state(state, epochs.len())?;
398 validate_float_config(config)
399}
400
401pub(super) fn validate_fixed_solve_boundary(
402 epochs: &[FloatEpoch],
403 solution: &FloatSolution,
404 config: &FixedSolveConfig,
405) -> Result<(), FixedSolveError> {
406 validate_epochs(epochs).map_err(FixedSolveError::Float)?;
407 validate_float_solution(solution, epochs.len())?;
408 validate_fixed_config(config)
409}
410
411fn validate_epochs(epochs: &[FloatEpoch]) -> Result<(), FloatSolveError> {
412 for epoch in epochs {
413 validate_epoch(epoch)?;
414 }
415 Ok(())
416}
417
418fn validate_epoch(epoch: &FloatEpoch) -> Result<(), FloatSolveError> {
419 validate::civil_datetime_with_second_policy(
420 epoch.epoch.year as i64,
421 epoch.epoch.month as i64,
422 epoch.epoch.day as i64,
423 epoch.epoch.hour as i64,
424 epoch.epoch.minute as i64,
425 epoch.epoch.second,
426 validate::CivilSecondPolicy::Continuous,
427 )
428 .map_err(invalid_input)?;
429 validate::finite(epoch.jd_whole, "ppp epoch jd_whole").map_err(invalid_input)?;
430 validate::finite(epoch.jd_fraction, "ppp epoch jd_fraction").map_err(invalid_input)?;
431 validate::finite(epoch.t_rx_j2000_s, "ppp epoch t_rx_j2000_s").map_err(invalid_input)?;
432 for obs in &epoch.observations {
433 validate_observation(obs)?;
434 }
435 Ok(())
436}
437
438fn validate_observation(obs: &FloatObservation) -> Result<(), FloatSolveError> {
439 validate::finite(obs.code_m, "ppp observation code_m").map_err(invalid_input)?;
440 validate::finite(obs.phase_m, "ppp observation phase_m").map_err(invalid_input)?;
441 validate::finite(obs.freq1_hz, "ppp observation freq1_hz").map_err(invalid_input)?;
442 validate::finite(obs.freq2_hz, "ppp observation freq2_hz").map_err(invalid_input)?;
443 Ok(())
444}
445
446fn validate_float_state(state: &FloatState, n_epochs: usize) -> Result<(), FloatSolveError> {
447 validate_state_clock_count(state, n_epochs)?;
448 validate::finite_vec3(state.position_m, "ppp state position_m").map_err(invalid_input)?;
449 validate::finite_slice(&state.clocks_m, "ppp state clocks_m").map_err(invalid_input)?;
450 for value in state.ambiguities_m.values() {
451 validate::finite(*value, "ppp state ambiguities_m").map_err(invalid_input)?;
452 }
453 for value in state.residual_ionosphere_m.values() {
454 validate::finite(*value, "ppp state residual_ionosphere_m").map_err(invalid_input)?;
455 }
456 validate::finite(state.ztd_m, "ppp state ztd_m").map_err(invalid_input)?;
457 validate::finite(
458 state.tropo_gradient_north_m,
459 "ppp state tropo_gradient_north_m",
460 )
461 .map_err(invalid_input)?;
462 validate::finite(
463 state.tropo_gradient_east_m,
464 "ppp state tropo_gradient_east_m",
465 )
466 .map_err(invalid_input)?;
467 Ok(())
468}
469
470fn validate_float_solution(
471 solution: &FloatSolution,
472 n_epochs: usize,
473) -> Result<(), FixedSolveError> {
474 validate_solution_clock_count(solution, n_epochs)?;
475 validate::finite_vec3(solution.position_m, "ppp float_solution position_m")
476 .map_err(invalid_fixed_input)?;
477 validate_position_covariance(
478 &solution.position_covariance,
479 "ppp float_solution position_covariance",
480 false,
481 )
482 .map_err(FixedSolveError::Float)?;
483 validate_position_covariance(
484 &solution.formal_position_covariance,
485 "ppp float_solution formal_position_covariance",
486 true,
487 )
488 .map_err(FixedSolveError::Float)?;
489 validate::finite_nonneg(
490 solution.posterior_variance_factor,
491 "ppp float_solution posterior_variance_factor",
492 )
493 .map_err(invalid_fixed_input)?;
494 validate::finite_nonneg(
495 solution.position_covariance_scale_factor,
496 "ppp float_solution position_covariance_scale_factor",
497 )
498 .map_err(invalid_fixed_input)?;
499 validate_position_covariance(
500 &solution.temporal_position_covariance,
501 "ppp float_solution temporal_position_covariance",
502 true,
503 )
504 .map_err(FixedSolveError::Float)?;
505 validate::finite_nonneg(
506 solution.temporal_position_covariance_scale_factor,
507 "ppp float_solution temporal_position_covariance_scale_factor",
508 )
509 .map_err(invalid_fixed_input)?;
510 validate_temporal_correlation(
511 solution.temporal_correlation,
512 "ppp float_solution temporal_correlation",
513 )
514 .map_err(FixedSolveError::Float)?;
515 validate::finite_slice(
516 &solution.epoch_clocks_m,
517 "ppp float_solution epoch_clocks_m",
518 )
519 .map_err(invalid_fixed_input)?;
520 for value in solution.ambiguities_m.values() {
521 validate::finite(*value, "ppp float_solution ambiguities_m")
522 .map_err(invalid_fixed_input)?;
523 }
524 for value in solution.residual_ionosphere_m.values() {
525 validate::finite(*value, "ppp float_solution residual_ionosphere_m")
526 .map_err(invalid_fixed_input)?;
527 }
528 if let Some(ztd_m) = solution.ztd_residual_m {
529 validate::finite(ztd_m, "ppp float_solution ztd_residual_m")
530 .map_err(invalid_fixed_input)?;
531 }
532 validate_optional_tropo_gradient_solution(solution).map_err(FixedSolveError::Float)?;
533 for residual in &solution.residuals_m {
534 validate::finite(residual.code_m, "ppp float_solution residual code_m")
535 .map_err(invalid_fixed_input)?;
536 validate::finite(residual.phase_m, "ppp float_solution residual phase_m")
537 .map_err(invalid_fixed_input)?;
538 validate::finite(
539 residual.code_weight,
540 "ppp float_solution residual code_weight",
541 )
542 .map_err(invalid_fixed_input)?;
543 validate::finite(
544 residual.phase_weight,
545 "ppp float_solution residual phase_weight",
546 )
547 .map_err(invalid_fixed_input)?;
548 }
549 validate::finite_nonneg(solution.code_rms_m, "ppp float_solution code_rms_m")
550 .map_err(invalid_fixed_input)?;
551 validate::finite_nonneg(solution.phase_rms_m, "ppp float_solution phase_rms_m")
552 .map_err(invalid_fixed_input)?;
553 validate::finite_nonneg(solution.weighted_rms_m, "ppp float_solution weighted_rms_m")
554 .map_err(invalid_fixed_input)?;
555 Ok(())
556}
557
558pub(super) fn validate_float_solution_output(
559 solution: &FloatSolution,
560 n_epochs: usize,
561) -> Result<(), FloatSolveError> {
562 validate_float_solution_clock_count(solution, n_epochs)?;
563 validate::finite_vec3(solution.position_m, "ppp float_solution position_m")
564 .map_err(invalid_input)?;
565 validate_position_covariance(
566 &solution.position_covariance,
567 "ppp float_solution position_covariance",
568 false,
569 )?;
570 validate_position_covariance(
571 &solution.formal_position_covariance,
572 "ppp float_solution formal_position_covariance",
573 true,
574 )?;
575 validate::finite_nonneg(
576 solution.posterior_variance_factor,
577 "ppp float_solution posterior_variance_factor",
578 )
579 .map_err(invalid_input)?;
580 validate::finite_nonneg(
581 solution.position_covariance_scale_factor,
582 "ppp float_solution position_covariance_scale_factor",
583 )
584 .map_err(invalid_input)?;
585 validate_position_covariance(
586 &solution.temporal_position_covariance,
587 "ppp float_solution temporal_position_covariance",
588 true,
589 )?;
590 validate::finite_nonneg(
591 solution.temporal_position_covariance_scale_factor,
592 "ppp float_solution temporal_position_covariance_scale_factor",
593 )
594 .map_err(invalid_input)?;
595 validate_temporal_correlation(
596 solution.temporal_correlation,
597 "ppp float_solution temporal_correlation",
598 )?;
599 validate::finite_slice(
600 &solution.epoch_clocks_m,
601 "ppp float_solution epoch_clocks_m",
602 )
603 .map_err(invalid_input)?;
604 for value in solution.ambiguities_m.values() {
605 validate::finite(*value, "ppp float_solution ambiguities_m").map_err(invalid_input)?;
606 }
607 for value in solution.residual_ionosphere_m.values() {
608 validate::finite(*value, "ppp float_solution residual_ionosphere_m")
609 .map_err(invalid_input)?;
610 }
611 if let Some(ztd_m) = solution.ztd_residual_m {
612 validate::finite(ztd_m, "ppp float_solution ztd_residual_m").map_err(invalid_input)?;
613 }
614 validate_optional_tropo_gradient_solution(solution)?;
615 for residual in &solution.residuals_m {
616 validate::finite(residual.code_m, "ppp float_solution residual code_m")
617 .map_err(invalid_input)?;
618 validate::finite(residual.phase_m, "ppp float_solution residual phase_m")
619 .map_err(invalid_input)?;
620 validate::finite(
621 residual.code_weight,
622 "ppp float_solution residual code_weight",
623 )
624 .map_err(invalid_input)?;
625 validate::finite(
626 residual.phase_weight,
627 "ppp float_solution residual phase_weight",
628 )
629 .map_err(invalid_input)?;
630 }
631 validate::finite_nonneg(solution.code_rms_m, "ppp float_solution code_rms_m")
632 .map_err(invalid_input)?;
633 validate::finite_nonneg(solution.phase_rms_m, "ppp float_solution phase_rms_m")
634 .map_err(invalid_input)?;
635 validate::finite_nonneg(solution.weighted_rms_m, "ppp float_solution weighted_rms_m")
636 .map_err(invalid_input)?;
637 Ok(())
638}
639
640fn validate_optional_tropo_gradient_solution(
641 solution: &FloatSolution,
642) -> Result<(), FloatSolveError> {
643 match (
644 solution.tropo_gradient_north_m,
645 solution.tropo_gradient_east_m,
646 solution.tropo_gradient_covariance_m2,
647 solution.formal_tropo_gradient_covariance_m2,
648 ) {
649 (None, None, None, None) => Ok(()),
650 (Some(north), Some(east), Some(covariance), Some(formal_covariance)) => {
651 validate::finite(north, "ppp float_solution tropo_gradient_north_m")
652 .map_err(invalid_input)?;
653 validate::finite(east, "ppp float_solution tropo_gradient_east_m")
654 .map_err(invalid_input)?;
655 validate_2x2_covariance_matrix(
656 covariance,
657 "ppp float_solution tropo_gradient_covariance_m2",
658 false,
659 )?;
660 validate_2x2_covariance_matrix(
661 formal_covariance,
662 "ppp float_solution formal_tropo_gradient_covariance_m2",
663 true,
664 )
665 }
666 _ => Err(FloatSolveError::InvalidInput {
667 field: "ppp float_solution tropo gradients",
668 reason: "must be all present or all absent",
669 }),
670 }
671}
672
673fn validate_2x2_covariance_matrix(
674 matrix: [[f64; 2]; 2],
675 label: &'static str,
676 require_positive_diagonal: bool,
677) -> Result<(), FloatSolveError> {
678 for row in matrix {
679 validate::finite_slice(&row, label).map_err(invalid_input)?;
680 }
681 for (idx, row) in matrix.iter().enumerate() {
682 if require_positive_diagonal {
683 validate::finite_positive(row[idx], label).map_err(invalid_input)?;
684 } else {
685 validate::finite_nonneg(row[idx], label).map_err(invalid_input)?;
686 }
687 for (jdx, other_row) in matrix.iter().enumerate().skip(idx + 1) {
688 let scale = row[jdx].abs().max(other_row[idx].abs()).max(1.0);
689 if (row[jdx] - other_row[idx]).abs() > 1.0e-10 * scale {
690 return Err(FloatSolveError::InvalidInput {
691 field: label,
692 reason: "symmetric",
693 });
694 }
695 }
696 }
697 Ok(())
698}
699
700fn validate_position_covariance(
701 covariance: &crate::dop::PositionCovariance,
702 label: &'static str,
703 require_positive_diagonal: bool,
704) -> Result<(), FloatSolveError> {
705 validate_covariance_matrix(
706 covariance.ecef_m2,
707 label,
708 "ecef_m2",
709 require_positive_diagonal,
710 )?;
711 validate_covariance_matrix(
712 covariance.enu_m2,
713 label,
714 "enu_m2",
715 require_positive_diagonal,
716 )
717}
718
719fn validate_covariance_matrix(
720 matrix: [[f64; 3]; 3],
721 label: &'static str,
722 frame: &'static str,
723 require_positive_diagonal: bool,
724) -> Result<(), FloatSolveError> {
725 for row in matrix {
726 validate::finite_slice(&row, label).map_err(invalid_input)?;
727 }
728 for (idx, row) in matrix.iter().enumerate() {
729 if require_positive_diagonal {
730 validate::finite_positive(row[idx], label).map_err(invalid_input)?;
731 } else {
732 validate::finite_nonneg(row[idx], label).map_err(invalid_input)?;
733 }
734 for (jdx, other_row) in matrix.iter().enumerate().skip(idx + 1) {
735 let scale = row[jdx].abs().max(other_row[idx].abs()).max(1.0);
736 if (row[jdx] - other_row[idx]).abs() > 1.0e-10 * scale {
737 return Err(FloatSolveError::InvalidInput {
738 field: label,
739 reason: frame,
740 });
741 }
742 }
743 }
744 Ok(())
745}
746
747fn validate_temporal_correlation(
748 temporal: TemporalCorrelationSummary,
749 label: &'static str,
750) -> Result<(), FloatSolveError> {
751 validate::finite_nonneg(temporal.lag1_autocorrelation, label).map_err(invalid_input)?;
752 if temporal.lag1_autocorrelation > 0.99 {
753 return Err(FloatSolveError::InvalidInput {
754 field: label,
755 reason: "lag1_autocorrelation",
756 });
757 }
758 validate::finite_nonneg(temporal.decorrelation_time_epochs, label).map_err(invalid_input)?;
759 if let Some(seconds) = temporal.decorrelation_time_s {
760 validate::finite_nonneg(seconds, label).map_err(invalid_input)?;
761 }
762 validate::finite_nonneg(temporal.effective_sample_count, label).map_err(invalid_input)?;
763 validate::finite_nonneg(temporal.variance_inflation_factor, label).map_err(invalid_input)?;
764 if temporal.variance_inflation_factor < 1.0 {
765 return Err(FloatSolveError::InvalidInput {
766 field: label,
767 reason: "variance_inflation_factor",
768 });
769 }
770 if temporal.effective_sample_count > temporal.nominal_sample_count as f64 + 1.0e-9 {
771 return Err(FloatSolveError::InvalidInput {
772 field: label,
773 reason: "effective_sample_count",
774 });
775 }
776 Ok(())
777}
778
779fn validate_float_config(config: &FloatSolveConfig) -> Result<(), FloatSolveError> {
780 validate_common_config(
781 config.weights,
782 config.tropo,
783 &config.corrections,
784 config.opts,
785 config.elevation_cutoff_deg,
786 )
787}
788
789fn validate_fixed_config(config: &FixedSolveConfig) -> Result<(), FixedSolveError> {
790 validate_common_config(
791 config.weights,
792 config.tropo,
793 &config.corrections,
794 config.opts,
795 config.elevation_cutoff_deg,
796 )
797 .map_err(FixedSolveError::Float)?;
798 validate_fixed_ambiguity_options(&config.ambiguity)
799}
800
801fn validate_common_config(
802 weights: MeasurementWeights,
803 tropo: TroposphereOptions,
804 corrections: &RangeCorrections,
805 opts: FloatSolveOptions,
806 elevation_cutoff_deg: Option<f64>,
807) -> Result<(), FloatSolveError> {
808 validate_measurement_weights(weights)?;
809 validate_troposphere_options(tropo)?;
810 validate_range_corrections(corrections)?;
811 validate_elevation_cutoff(elevation_cutoff_deg)?;
812 validate_float_solve_options(opts)
813}
814
815fn validate_elevation_cutoff(cutoff_deg: Option<f64>) -> Result<(), FloatSolveError> {
816 if let Some(cutoff_deg) = cutoff_deg {
817 validate::finite_in_range(cutoff_deg, -90.0, 90.0, "elevation_cutoff_deg")
818 .map_err(invalid_input)?;
819 }
820 Ok(())
821}
822
823fn validate_measurement_weights(weights: MeasurementWeights) -> Result<(), FloatSolveError> {
824 validate::finite_positive(weights.code, "ppp measurement weight code")
825 .map_err(invalid_input)?;
826 validate::finite_positive(weights.phase, "ppp measurement weight phase")
827 .map_err(invalid_input)?;
828 Ok(())
829}
830
831fn validate_troposphere_options(tropo: TroposphereOptions) -> Result<(), FloatSolveError> {
832 if !tropo.enabled {
833 return Ok(());
834 }
835 validate::finite_positive(tropo.met.pressure_hpa, "ppp tropo pressure_hpa")
836 .map_err(invalid_input)?;
837 validate::finite_positive(tropo.met.temperature_k, "ppp tropo temperature_k")
838 .map_err(invalid_input)?;
839 validate::fraction(tropo.met.relative_humidity, "ppp tropo relative_humidity")
840 .map_err(invalid_input)?;
841 Ok(())
842}
843
844fn validate_range_corrections(corrections: &RangeCorrections) -> Result<(), FloatSolveError> {
845 if let Some(receiver) = &corrections.receiver_antenna {
846 validate::finite_positive(receiver.freq1_hz, "ppp receiver antenna freq1_hz")
847 .map_err(invalid_input)?;
848 validate::finite_positive(receiver.freq2_hz, "ppp receiver antenna freq2_hz")
849 .map_err(invalid_input)?;
850 if receiver.freq1_hz == receiver.freq2_hz {
851 return Err(invalid_input_field(
852 "ppp receiver antenna frequency pair",
853 "must differ",
854 ));
855 }
856 for frequency in &receiver.frequencies {
857 validate_receiver_antenna_frequency(frequency)?;
858 }
859 }
860 if let Some(clock) = &corrections.satellite_clock {
861 for records in clock.series.values() {
862 validate::require_strictly_increasing(
863 records.iter().map(|&(t_gps_s, _)| t_gps_s),
864 "ppp satellite clock epoch_s",
865 )
866 .map_err(invalid_input)?;
867 for &(t_gps_s, bias_s) in records {
868 validate::finite(t_gps_s, "ppp satellite clock epoch_s").map_err(invalid_input)?;
869 validate::finite(bias_s, "ppp satellite clock bias_s").map_err(invalid_input)?;
870 }
871 }
872 }
873 for vector in corrections.ppp.tide.values() {
874 validate::finite_vec3(*vector, "ppp correction tide vector_m").map_err(invalid_input)?;
875 }
876 for vector in corrections.ppp.pole_tide.values() {
877 validate::finite_vec3(*vector, "ppp correction pole_tide vector_m")
878 .map_err(invalid_input)?;
879 }
880 for vector in corrections.ppp.ocean_loading.values() {
881 validate::finite_vec3(*vector, "ppp correction ocean_loading vector_m")
882 .map_err(invalid_input)?;
883 }
884 for value in corrections.ppp.windup_m.values() {
885 validate::finite(*value, "ppp correction windup_m").map_err(invalid_input)?;
886 }
887 for vector in corrections.ppp.sat_pco_ecef.values() {
888 validate::finite_vec3(*vector, "ppp correction sat_pco_ecef").map_err(invalid_input)?;
889 }
890 for value in corrections.ppp.sat_pcv_m.values() {
891 validate::finite(*value, "ppp correction sat_pcv_m").map_err(invalid_input)?;
892 }
893 for value in corrections.ppp.code_bias_m.values() {
894 validate::finite(*value, "ppp correction code_bias_m").map_err(invalid_input)?;
895 }
896 for value in corrections.ppp.ssr_code_bias_m.values() {
897 validate::finite(*value, "ppp correction ssr_code_bias_m").map_err(invalid_input)?;
898 }
899 for value in corrections.ppp.phase_bias_m.values() {
900 validate::finite(*value, "ppp correction phase_bias_m").map_err(invalid_input)?;
901 }
902 Ok(())
903}
904
905fn validate_receiver_antenna_frequency(
906 frequency: &ReceiverAntennaFrequency,
907) -> Result<(), FloatSolveError> {
908 validate::finite_vec3(frequency.pco_m, "ppp receiver antenna pco_m").map_err(invalid_input)?;
909 for sample in &frequency.pcv_samples {
910 validate_pcv_sample(sample)?;
911 }
912 Ok(())
913}
914
915fn validate_pcv_sample(sample: &PcvSample) -> Result<(), FloatSolveError> {
916 if let Some(azimuth_deg) = sample.azimuth_deg {
917 validate::finite(azimuth_deg, "ppp receiver antenna pcv azimuth_deg")
918 .map_err(invalid_input)?;
919 }
920 validate::finite_in_range(
921 sample.zenith_deg,
922 0.0,
923 180.0,
924 "ppp receiver antenna pcv zenith_deg",
925 )
926 .map_err(invalid_input)?;
927 validate::finite(sample.value_m, "ppp receiver antenna pcv value_m").map_err(invalid_input)?;
928 Ok(())
929}
930
931fn validate_fixed_ambiguity_options(
932 ambiguity: &FixedAmbiguityOptions,
933) -> Result<(), FixedSolveError> {
934 validate::finite_nonneg(
935 ambiguity.ratio_threshold,
936 "ppp fixed ambiguity ratio_threshold",
937 )
938 .map_err(invalid_fixed_input)?;
939 for value in ambiguity.wavelengths_m.values() {
940 validate::finite_positive(*value, "ppp fixed ambiguity wavelength_m")
941 .map_err(invalid_fixed_input)?;
942 }
943 for value in ambiguity.offsets_m.values() {
944 validate::finite(*value, "ppp fixed ambiguity offset_m").map_err(invalid_fixed_input)?;
945 }
946 Ok(())
947}
948
949fn validate_float_solve_options(opts: FloatSolveOptions) -> Result<(), FloatSolveError> {
950 if opts.max_iterations == 0 {
951 return Err(invalid_solve_option("max_iterations", "must be positive"));
952 }
953 if opts.max_iterations > MAX_PPP_ITERATIONS {
954 return Err(invalid_solve_option(
955 "max_iterations",
956 "exceeds the PPP iteration cap",
957 ));
958 }
959 validate_tolerance("position_tolerance_m", opts.position_tolerance_m)?;
960 validate_tolerance("clock_tolerance_m", opts.clock_tolerance_m)?;
961 validate_tolerance("ambiguity_tolerance_m", opts.ambiguity_tolerance_m)?;
962 validate_tolerance("ztd_tolerance_m", opts.ztd_tolerance_m)
963}
964
965fn validate_tolerance(field: &'static str, value: f64) -> Result<(), FloatSolveError> {
966 if validate::finite(value, field).is_err() {
967 return Err(invalid_solve_option(field, "must be finite"));
968 }
969 if value <= 0.0 {
970 return Err(invalid_solve_option(field, "must be positive"));
971 }
972 Ok(())
973}
974
975fn validate_state_clock_count(state: &FloatState, n_epochs: usize) -> Result<(), FloatSolveError> {
976 if state.clocks_m.len() == n_epochs {
977 Ok(())
978 } else {
979 Err(invalid_clock_count(n_epochs, state.clocks_m.len()))
980 }
981}
982
983fn validate_solution_clock_count(
984 solution: &FloatSolution,
985 n_epochs: usize,
986) -> Result<(), FixedSolveError> {
987 if solution.epoch_clocks_m.len() == n_epochs {
988 Ok(())
989 } else {
990 Err(FixedSolveError::Float(invalid_clock_count(
991 n_epochs,
992 solution.epoch_clocks_m.len(),
993 )))
994 }
995}
996
997fn validate_float_solution_clock_count(
998 solution: &FloatSolution,
999 n_epochs: usize,
1000) -> Result<(), FloatSolveError> {
1001 if solution.epoch_clocks_m.len() == n_epochs {
1002 Ok(())
1003 } else {
1004 Err(invalid_clock_count(n_epochs, solution.epoch_clocks_m.len()))
1005 }
1006}
1007
1008fn state_from_solution(solution: &FloatSolution, prior: &FloatState) -> FloatState {
1009 FloatState {
1010 position_m: solution.position_m,
1011 clocks_m: solution.epoch_clocks_m.clone(),
1012 ambiguities_m: solution.ambiguities_m.clone(),
1013 ztd_m: solution.ztd_residual_m.unwrap_or(prior.ztd_m),
1014 tropo_gradient_north_m: solution
1015 .tropo_gradient_north_m
1016 .unwrap_or(prior.tropo_gradient_north_m),
1017 tropo_gradient_east_m: solution
1018 .tropo_gradient_east_m
1019 .unwrap_or(prior.tropo_gradient_east_m),
1020 residual_ionosphere_m: if solution.residual_ionosphere_m.is_empty() {
1021 prior.residual_ionosphere_m.clone()
1022 } else {
1023 solution.residual_ionosphere_m.clone()
1024 },
1025 }
1026}
1027
1028fn estimates_ztd(tropo: TroposphereOptions) -> bool {
1029 tropo.enabled && tropo.estimate_ztd
1030}
1031
1032fn estimates_tropo_gradients(tropo: TroposphereOptions) -> bool {
1033 tropo.enabled && tropo.estimate_tropo_gradients
1034}
1035
1036fn ztd_unknown_count(tropo: TroposphereOptions) -> usize {
1037 usize::from(estimates_ztd(tropo))
1038}
1039
1040fn apply_elevation_cutoff(
1044 source: &dyn ObservableEphemerisSource,
1045 epochs: &[FloatEpoch],
1046 state: &FloatState,
1047 cutoff_deg: f64,
1048 tropo: TroposphereOptions,
1049 estimate_residual_ionosphere: bool,
1050) -> Result<Vec<FloatEpoch>, FloatSolveError> {
1051 let mut retained = Vec::with_capacity(epochs.len());
1052 for (epoch_idx, epoch) in epochs.iter().enumerate() {
1053 let mut observations = Vec::with_capacity(epoch.observations.len());
1054 for obs in &epoch.observations {
1055 let options = predict_default(source, obs)?;
1056 let pred = predict(
1057 source,
1058 obs.sat,
1059 state.position_m,
1060 epoch.t_rx_j2000_s,
1061 options,
1062 )
1063 .map_err(|e| no_ephemeris(obs, e))?;
1064 validate::finite(pred.elevation_deg, "ppp predicted elevation_deg")
1065 .map_err(invalid_input)?;
1066 if pred.elevation_deg >= cutoff_deg {
1067 observations.push(obs.clone());
1068 }
1069 }
1070 let mut epoch = epoch.clone();
1071 epoch.observations = observations;
1072 retained.push(epoch);
1073 debug_assert_eq!(retained.len(), epoch_idx + 1);
1074 }
1075 validate_elevation_cutoff_retained(&retained, cutoff_deg, tropo, estimate_residual_ionosphere)?;
1076 Ok(retained)
1077}
1078
1079fn validate_elevation_cutoff_retained(
1080 epochs: &[FloatEpoch],
1081 cutoff_deg: f64,
1082 tropo: TroposphereOptions,
1083 estimate_residual_ionosphere: bool,
1084) -> Result<(), FloatSolveError> {
1085 let retained_observations = epochs.iter().map(|e| e.observations.len()).sum::<usize>();
1086 let active_sats = epochs
1087 .iter()
1088 .flat_map(|e| e.observations.iter().map(|o| o.ambiguity_id.as_str()))
1089 .collect::<BTreeSet<_>>();
1090 let unknowns = crate::estimation::substrate::parameters::ParameterLayout::ppp(
1091 epochs.len(),
1092 ztd_unknown_count(tropo),
1093 tropo_gradient_unknown_count(tropo),
1094 residual_ionosphere_unknown_count(estimate_residual_ionosphere, active_sats.len()),
1095 active_sats.len(),
1096 )
1097 .dim();
1098 let required_observations = unknowns.div_ceil(2).max(4);
1099 if active_sats.len() < 4 || retained_observations < required_observations {
1100 return Err(insufficient_after_elevation_cutoff(
1101 cutoff_deg,
1102 retained_observations,
1103 required_observations,
1104 ));
1105 }
1106 Ok(())
1107}
1108
1109fn tropo_gradient_unknown_count(tropo: TroposphereOptions) -> usize {
1110 if estimates_tropo_gradients(tropo) {
1111 2
1112 } else {
1113 0
1114 }
1115}
1116
1117fn residual_ionosphere_unknown_count(estimate: bool, ambiguity_count: usize) -> usize {
1118 if estimate {
1119 ambiguity_count
1120 } else {
1121 0
1122 }
1123}
1124
1125fn rms(values: &[f64]) -> f64 {
1126 if values.is_empty() {
1127 return 0.0;
1128 }
1129 (values.iter().map(|v| v * v).sum::<f64>() / values.len() as f64).sqrt()
1130}
1131
1132fn weighted_rms(rows: &[FloatResidual], weights: MeasurementWeights) -> f64 {
1133 let mut values = Vec::with_capacity(rows.len() * 2);
1134 for row in rows {
1135 values.push(row.code_m * row.code_weight);
1136 values.push(row.phase_m * row.phase_weight);
1137 }
1138 if values.is_empty() {
1139 rms(&[0.0 * weights.code, 0.0 * weights.phase])
1140 } else {
1141 rms(&values)
1142 }
1143}
1144
1145fn max_abs(xs: &[f64]) -> f64 {
1146 xs.iter().map(|x| x.abs()).fold(0.0, f64::max)
1147}
1148
1149#[cfg(test)]
1150mod tests;