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 types;
144pub mod velocity;
145
146pub use crate::dop::PositionCovariance;
147pub use auto_init::{
148 solve_ppp_auto_init_fixed, solve_ppp_auto_init_fixed_with_strategy, solve_ppp_auto_init_float,
149 solve_ppp_auto_init_float_with_strategy, PppAutoInitError, PppAutoInitOptions,
150 PppAutoInitStrategy, PppInitialGuess,
151};
152pub use cycle_slip::{
153 detect_cycle_slips, geometry_free_m, melbourne_wubbena_cycles, update_geometry_free,
154 update_melbourne_wubbena, CycleSlipConfig, CycleSlipConfigError, CycleSlipDetectorState,
155 CycleSlipError, CycleSlipFlagEpoch, CycleSlipFlagObservation, CycleSlipStateKey,
156 GeometryFreeUpdate, MelbourneWubbenaUpdate, RunningMeanVariance, SatelliteCycleSlipState,
157 DEFAULT_MINIMUM_ARC_LENGTH, DEFAULT_RUNNING_STATISTIC_K_FACTOR,
158};
159pub(crate) use fixed::run_fixed_from_float;
160pub use fixed::solve_fixed_from_float;
161#[cfg(test)]
162use float::initial_ambiguities;
163pub(crate) use float::run_float_epochs;
164pub use float::{solve_float_epoch, solve_float_epochs};
165pub use kinematic::{
166 correct_kinematic_state, predict_kinematic_state, solve_kinematic_ppp, KinematicConfig,
167 KinematicEpochSolution, KinematicEpochStatus, KinematicMotionModel,
168 KinematicPositionProcessNoise, KinematicProcessNoise, KinematicSolveError, KinematicState,
169 KinematicUpdateSummary,
170};
171pub use prep::{
172 prepare_widelane_fixed_epochs, split_float_cycle_slip_epochs, DualFrequencyEpoch,
173 DualFrequencyObservation, FloatCycleSlipEpoch, FloatCycleSlipObservation,
174 FloatCycleSlipTaggedEpoch, FloatCycleSlipTaggedObservation, PppSplitArc, PreparedFloatEpoch,
175 PreparedFloatObservation, WideLanePrepError, WideLanePrepOptions, WideLanePrepResult,
176};
177pub use raim::{
178 solve_float_epoch_with_raim, ProtectionLevels, RaimConfig, RaimError, RaimFdeError,
179 RaimFdeResult, RaimFdeStatus, RaimGeometryRow, RaimIdentification, RaimResult, RaimStatus,
180 SatelliteTestStatistic,
181};
182pub use tec::{
183 code_geometry_free_m, estimate_code_slant_tec, estimate_phase_slant_tec, estimate_tec,
184 ionospheric_pierce_point, level_slant_tec_arc, phase_geometry_free_m,
185 slant_tec_from_code_geometry_free_m, slant_tec_from_phase_geometry_free_m,
186 thin_shell_mapping_function, vertical_tec_from_slant_tec, CodeSlantTecEstimate,
187 IonosphericPiercePoint, LeveledTecSample, PhaseSlantTecEstimate, TecConfig, TecEpoch, TecError,
188 TecEstimate, TecEstimateSample, TecLevelingResult, TecLevelingSample, TecObservation,
189 TecSatelliteArc, DEFAULT_IONOSPHERIC_SHELL_HEIGHT_M, ELECTRONS_PER_TECU_M2,
190 TEC_GROUP_DELAY_COEFFICIENT,
191};
192pub use types::*;
193pub use velocity::{
194 predict_range_rate_m_s, solve_velocity, RangeRatePrediction, ReceiverVelocityState,
195 VelocityConfig, VelocityObservation, VelocityRobustConfig, VelocitySolution,
196 VelocitySolveError,
197};
198
199pub use crate::ambiguity::CycleSlipPolicy;
200
201pub mod defaults {
215 pub const POSITION_TOLERANCE_M: f64 = 1.0e-4;
219
220 pub const CLOCK_TOLERANCE_M: f64 = 1.0e-4;
224
225 pub const AMBIGUITY_TOLERANCE_M: f64 = 1.0e-4;
229
230 pub const ZTD_TOLERANCE_M: f64 = 1.0e-4;
234
235 pub const MAX_ITERATIONS: usize = 8;
240
241 pub const RATIO_THRESHOLD: f64 = 3.0;
247}
248
249use std::collections::BTreeMap;
250
251use crate::constants::F_L1_HZ;
252use crate::estimation::recipe::NormalRecipe;
253use crate::observables::{ObservableEphemerisSource, ObservablesError, PredictOptions};
254use crate::ppp_corrections::{
255 self, PppCorrectionEpoch, PppCorrectionObservation, PppCorrectionsError, PppCorrectionsOptions,
256};
257use crate::sp3::Sp3;
258use crate::validate::{self, FieldError};
259
260const MAX_PPP_ITERATIONS: usize = 10_000;
261
262pub fn build_ppp_lookup(
264 sp3: &Sp3,
265 epochs: &[FloatEpoch],
266 receiver_ecef_m: [f64; 3],
267 options: &PppCorrectionsOptions,
268) -> Result<PppCorrectionLookup, PppCorrectionsError> {
269 let ppp_epochs: Vec<PppCorrectionEpoch> = epochs
270 .iter()
271 .map(|epoch| PppCorrectionEpoch {
272 epoch: epoch.epoch,
273 t_rx_j2000_s: epoch.t_rx_j2000_s,
274 observations: epoch
275 .observations
276 .iter()
277 .map(|obs| PppCorrectionObservation {
278 sat: obs.sat,
279 freq1_hz: obs.freq1_hz,
280 freq2_hz: obs.freq2_hz,
281 glonass_channel: obs.glonass_channel,
282 })
283 .collect(),
284 })
285 .collect();
286 let corrections = ppp_corrections::build(sp3, &ppp_epochs, receiver_ecef_m, options)?;
287 Ok(PppCorrectionLookup::from_options(corrections, options))
288}
289
290impl FloatState {
291 fn default_for_epochs(epochs: &[FloatEpoch]) -> Self {
292 Self {
293 position_m: [0.0; 3],
294 clocks_m: vec![0.0; epochs.len()],
295 ambiguities_m: BTreeMap::new(),
296 ztd_m: 0.0,
297 }
298 }
299}
300
301#[derive(Clone, Copy)]
309struct ModelContext<'a> {
310 source: &'a dyn ObservableEphemerisSource,
311 weights: MeasurementWeights,
312 tropo: TroposphereOptions,
313 corrections: &'a RangeCorrections,
314 normal: NormalRecipe,
315}
316
317fn predict_default(
318 _source: &dyn ObservableEphemerisSource,
319 _obs: &FloatObservation,
320) -> Result<PredictOptions, FloatSolveError> {
321 Ok(PredictOptions {
322 carrier_hz: F_L1_HZ,
323 light_time: true,
324 sagnac: true,
325 })
326}
327
328fn no_ephemeris(obs: &FloatObservation, error: ObservablesError) -> FloatSolveError {
329 FloatSolveError::NoEphemeris {
330 satellite_id: obs.satellite_id.clone(),
331 reason: match error {
332 ObservablesError::NoEphemeris => NoEphemerisReason::NoEphemeris,
333 ObservablesError::InvalidInput { .. } => NoEphemerisReason::Reason(error.to_string()),
334 ObservablesError::Ephemeris(err) => NoEphemerisReason::Reason(err.to_string()),
335 ObservablesError::Media(err) => NoEphemerisReason::Reason(err.to_string()),
336 },
337 }
338}
339
340fn missing_satellite_clock(obs: &FloatObservation) -> FloatSolveError {
341 FloatSolveError::NoEphemeris {
342 satellite_id: obs.satellite_id.clone(),
343 reason: NoEphemerisReason::MissingSatelliteClock,
344 }
345}
346
347fn missing_correction(obs: &FloatObservation, correction: MissingCorrection) -> FloatSolveError {
348 FloatSolveError::MissingCorrection {
349 satellite_id: obs.satellite_id.clone(),
350 correction,
351 }
352}
353
354fn invalid_clock_count(expected: usize, actual: usize) -> FloatSolveError {
355 FloatSolveError::InvalidClockCount { expected, actual }
356}
357
358fn invalid_solve_option(field: &'static str, reason: &'static str) -> FloatSolveError {
359 FloatSolveError::InvalidSolveOption { field, reason }
360}
361
362pub(super) fn invalid_input(error: FieldError) -> FloatSolveError {
363 invalid_input_field(error.field(), error.reason())
364}
365
366fn invalid_input_field(field: &'static str, reason: &'static str) -> FloatSolveError {
367 FloatSolveError::InvalidInput { field, reason }
368}
369
370fn invalid_fixed_input(error: FieldError) -> FixedSolveError {
371 FixedSolveError::Float(invalid_input(error))
372}
373
374pub(super) fn validate_float_solve_boundary(
375 epochs: &[FloatEpoch],
376 state: &FloatState,
377 config: &FloatSolveConfig,
378) -> Result<(), FloatSolveError> {
379 validate_epochs(epochs)?;
380 validate_float_state(state, epochs.len())?;
381 validate_float_config(config)
382}
383
384pub(super) fn validate_fixed_solve_boundary(
385 epochs: &[FloatEpoch],
386 solution: &FloatSolution,
387 config: &FixedSolveConfig,
388) -> Result<(), FixedSolveError> {
389 validate_epochs(epochs).map_err(FixedSolveError::Float)?;
390 validate_float_solution(solution, epochs.len())?;
391 validate_fixed_config(config)
392}
393
394fn validate_epochs(epochs: &[FloatEpoch]) -> Result<(), FloatSolveError> {
395 for epoch in epochs {
396 validate_epoch(epoch)?;
397 }
398 Ok(())
399}
400
401fn validate_epoch(epoch: &FloatEpoch) -> Result<(), FloatSolveError> {
402 validate::civil_datetime_with_second_policy(
403 epoch.epoch.year as i64,
404 epoch.epoch.month as i64,
405 epoch.epoch.day as i64,
406 epoch.epoch.hour as i64,
407 epoch.epoch.minute as i64,
408 epoch.epoch.second,
409 validate::CivilSecondPolicy::Continuous,
410 )
411 .map_err(invalid_input)?;
412 validate::finite(epoch.jd_whole, "ppp epoch jd_whole").map_err(invalid_input)?;
413 validate::finite(epoch.jd_fraction, "ppp epoch jd_fraction").map_err(invalid_input)?;
414 validate::finite(epoch.t_rx_j2000_s, "ppp epoch t_rx_j2000_s").map_err(invalid_input)?;
415 for obs in &epoch.observations {
416 validate_observation(obs)?;
417 }
418 Ok(())
419}
420
421fn validate_observation(obs: &FloatObservation) -> Result<(), FloatSolveError> {
422 validate::finite(obs.code_m, "ppp observation code_m").map_err(invalid_input)?;
423 validate::finite(obs.phase_m, "ppp observation phase_m").map_err(invalid_input)?;
424 validate::finite(obs.freq1_hz, "ppp observation freq1_hz").map_err(invalid_input)?;
425 validate::finite(obs.freq2_hz, "ppp observation freq2_hz").map_err(invalid_input)?;
426 Ok(())
427}
428
429fn validate_float_state(state: &FloatState, n_epochs: usize) -> Result<(), FloatSolveError> {
430 validate_state_clock_count(state, n_epochs)?;
431 validate::finite_vec3(state.position_m, "ppp state position_m").map_err(invalid_input)?;
432 validate::finite_slice(&state.clocks_m, "ppp state clocks_m").map_err(invalid_input)?;
433 for value in state.ambiguities_m.values() {
434 validate::finite(*value, "ppp state ambiguities_m").map_err(invalid_input)?;
435 }
436 validate::finite(state.ztd_m, "ppp state ztd_m").map_err(invalid_input)?;
437 Ok(())
438}
439
440fn validate_float_solution(
441 solution: &FloatSolution,
442 n_epochs: usize,
443) -> Result<(), FixedSolveError> {
444 validate_solution_clock_count(solution, n_epochs)?;
445 validate::finite_vec3(solution.position_m, "ppp float_solution position_m")
446 .map_err(invalid_fixed_input)?;
447 validate_position_covariance(
448 &solution.position_covariance,
449 "ppp float_solution position_covariance",
450 false,
451 )
452 .map_err(FixedSolveError::Float)?;
453 validate_position_covariance(
454 &solution.formal_position_covariance,
455 "ppp float_solution formal_position_covariance",
456 true,
457 )
458 .map_err(FixedSolveError::Float)?;
459 validate::finite_nonneg(
460 solution.posterior_variance_factor,
461 "ppp float_solution posterior_variance_factor",
462 )
463 .map_err(invalid_fixed_input)?;
464 validate::finite_nonneg(
465 solution.position_covariance_scale_factor,
466 "ppp float_solution position_covariance_scale_factor",
467 )
468 .map_err(invalid_fixed_input)?;
469 validate::finite_slice(
470 &solution.epoch_clocks_m,
471 "ppp float_solution epoch_clocks_m",
472 )
473 .map_err(invalid_fixed_input)?;
474 for value in solution.ambiguities_m.values() {
475 validate::finite(*value, "ppp float_solution ambiguities_m")
476 .map_err(invalid_fixed_input)?;
477 }
478 if let Some(ztd_m) = solution.ztd_residual_m {
479 validate::finite(ztd_m, "ppp float_solution ztd_residual_m")
480 .map_err(invalid_fixed_input)?;
481 }
482 for residual in &solution.residuals_m {
483 validate::finite(residual.code_m, "ppp float_solution residual code_m")
484 .map_err(invalid_fixed_input)?;
485 validate::finite(residual.phase_m, "ppp float_solution residual phase_m")
486 .map_err(invalid_fixed_input)?;
487 validate::finite(
488 residual.code_weight,
489 "ppp float_solution residual code_weight",
490 )
491 .map_err(invalid_fixed_input)?;
492 validate::finite(
493 residual.phase_weight,
494 "ppp float_solution residual phase_weight",
495 )
496 .map_err(invalid_fixed_input)?;
497 }
498 validate::finite_nonneg(solution.code_rms_m, "ppp float_solution code_rms_m")
499 .map_err(invalid_fixed_input)?;
500 validate::finite_nonneg(solution.phase_rms_m, "ppp float_solution phase_rms_m")
501 .map_err(invalid_fixed_input)?;
502 validate::finite_nonneg(solution.weighted_rms_m, "ppp float_solution weighted_rms_m")
503 .map_err(invalid_fixed_input)?;
504 Ok(())
505}
506
507pub(super) fn validate_float_solution_output(
508 solution: &FloatSolution,
509 n_epochs: usize,
510) -> Result<(), FloatSolveError> {
511 validate_float_solution_clock_count(solution, n_epochs)?;
512 validate::finite_vec3(solution.position_m, "ppp float_solution position_m")
513 .map_err(invalid_input)?;
514 validate_position_covariance(
515 &solution.position_covariance,
516 "ppp float_solution position_covariance",
517 false,
518 )?;
519 validate_position_covariance(
520 &solution.formal_position_covariance,
521 "ppp float_solution formal_position_covariance",
522 true,
523 )?;
524 validate::finite_nonneg(
525 solution.posterior_variance_factor,
526 "ppp float_solution posterior_variance_factor",
527 )
528 .map_err(invalid_input)?;
529 validate::finite_nonneg(
530 solution.position_covariance_scale_factor,
531 "ppp float_solution position_covariance_scale_factor",
532 )
533 .map_err(invalid_input)?;
534 validate::finite_slice(
535 &solution.epoch_clocks_m,
536 "ppp float_solution epoch_clocks_m",
537 )
538 .map_err(invalid_input)?;
539 for value in solution.ambiguities_m.values() {
540 validate::finite(*value, "ppp float_solution ambiguities_m").map_err(invalid_input)?;
541 }
542 if let Some(ztd_m) = solution.ztd_residual_m {
543 validate::finite(ztd_m, "ppp float_solution ztd_residual_m").map_err(invalid_input)?;
544 }
545 for residual in &solution.residuals_m {
546 validate::finite(residual.code_m, "ppp float_solution residual code_m")
547 .map_err(invalid_input)?;
548 validate::finite(residual.phase_m, "ppp float_solution residual phase_m")
549 .map_err(invalid_input)?;
550 validate::finite(
551 residual.code_weight,
552 "ppp float_solution residual code_weight",
553 )
554 .map_err(invalid_input)?;
555 validate::finite(
556 residual.phase_weight,
557 "ppp float_solution residual phase_weight",
558 )
559 .map_err(invalid_input)?;
560 }
561 validate::finite_nonneg(solution.code_rms_m, "ppp float_solution code_rms_m")
562 .map_err(invalid_input)?;
563 validate::finite_nonneg(solution.phase_rms_m, "ppp float_solution phase_rms_m")
564 .map_err(invalid_input)?;
565 validate::finite_nonneg(solution.weighted_rms_m, "ppp float_solution weighted_rms_m")
566 .map_err(invalid_input)?;
567 Ok(())
568}
569
570fn validate_position_covariance(
571 covariance: &crate::dop::PositionCovariance,
572 label: &'static str,
573 require_positive_diagonal: bool,
574) -> Result<(), FloatSolveError> {
575 validate_covariance_matrix(
576 covariance.ecef_m2,
577 label,
578 "ecef_m2",
579 require_positive_diagonal,
580 )?;
581 validate_covariance_matrix(
582 covariance.enu_m2,
583 label,
584 "enu_m2",
585 require_positive_diagonal,
586 )
587}
588
589fn validate_covariance_matrix(
590 matrix: [[f64; 3]; 3],
591 label: &'static str,
592 frame: &'static str,
593 require_positive_diagonal: bool,
594) -> Result<(), FloatSolveError> {
595 for row in matrix {
596 validate::finite_slice(&row, label).map_err(invalid_input)?;
597 }
598 for (idx, row) in matrix.iter().enumerate() {
599 if require_positive_diagonal {
600 validate::finite_positive(row[idx], label).map_err(invalid_input)?;
601 } else {
602 validate::finite_nonneg(row[idx], label).map_err(invalid_input)?;
603 }
604 for (jdx, other_row) in matrix.iter().enumerate().skip(idx + 1) {
605 let scale = row[jdx].abs().max(other_row[idx].abs()).max(1.0);
606 if (row[jdx] - other_row[idx]).abs() > 1.0e-10 * scale {
607 return Err(FloatSolveError::InvalidInput {
608 field: label,
609 reason: frame,
610 });
611 }
612 }
613 }
614 Ok(())
615}
616
617fn validate_float_config(config: &FloatSolveConfig) -> Result<(), FloatSolveError> {
618 validate_common_config(
619 config.weights,
620 config.tropo,
621 &config.corrections,
622 config.opts,
623 )
624}
625
626fn validate_fixed_config(config: &FixedSolveConfig) -> Result<(), FixedSolveError> {
627 validate_common_config(
628 config.weights,
629 config.tropo,
630 &config.corrections,
631 config.opts,
632 )
633 .map_err(FixedSolveError::Float)?;
634 validate_fixed_ambiguity_options(&config.ambiguity)
635}
636
637fn validate_common_config(
638 weights: MeasurementWeights,
639 tropo: TroposphereOptions,
640 corrections: &RangeCorrections,
641 opts: FloatSolveOptions,
642) -> Result<(), FloatSolveError> {
643 validate_measurement_weights(weights)?;
644 validate_troposphere_options(tropo)?;
645 validate_range_corrections(corrections)?;
646 validate_float_solve_options(opts)
647}
648
649fn validate_measurement_weights(weights: MeasurementWeights) -> Result<(), FloatSolveError> {
650 validate::finite_positive(weights.code, "ppp measurement weight code")
651 .map_err(invalid_input)?;
652 validate::finite_positive(weights.phase, "ppp measurement weight phase")
653 .map_err(invalid_input)?;
654 Ok(())
655}
656
657fn validate_troposphere_options(tropo: TroposphereOptions) -> Result<(), FloatSolveError> {
658 if !tropo.enabled {
659 return Ok(());
660 }
661 validate::finite_positive(tropo.met.pressure_hpa, "ppp tropo pressure_hpa")
662 .map_err(invalid_input)?;
663 validate::finite_positive(tropo.met.temperature_k, "ppp tropo temperature_k")
664 .map_err(invalid_input)?;
665 validate::fraction(tropo.met.relative_humidity, "ppp tropo relative_humidity")
666 .map_err(invalid_input)?;
667 Ok(())
668}
669
670fn validate_range_corrections(corrections: &RangeCorrections) -> Result<(), FloatSolveError> {
671 if let Some(receiver) = &corrections.receiver_antenna {
672 validate::finite_positive(receiver.freq1_hz, "ppp receiver antenna freq1_hz")
673 .map_err(invalid_input)?;
674 validate::finite_positive(receiver.freq2_hz, "ppp receiver antenna freq2_hz")
675 .map_err(invalid_input)?;
676 if receiver.freq1_hz == receiver.freq2_hz {
677 return Err(invalid_input_field(
678 "ppp receiver antenna frequency pair",
679 "must differ",
680 ));
681 }
682 for frequency in &receiver.frequencies {
683 validate_receiver_antenna_frequency(frequency)?;
684 }
685 }
686 if let Some(clock) = &corrections.satellite_clock {
687 for records in clock.series.values() {
688 validate::require_strictly_increasing(
689 records.iter().map(|&(t_gps_s, _)| t_gps_s),
690 "ppp satellite clock epoch_s",
691 )
692 .map_err(invalid_input)?;
693 for &(t_gps_s, bias_s) in records {
694 validate::finite(t_gps_s, "ppp satellite clock epoch_s").map_err(invalid_input)?;
695 validate::finite(bias_s, "ppp satellite clock bias_s").map_err(invalid_input)?;
696 }
697 }
698 }
699 for vector in corrections.ppp.tide.values() {
700 validate::finite_vec3(*vector, "ppp correction tide vector_m").map_err(invalid_input)?;
701 }
702 for vector in corrections.ppp.pole_tide.values() {
703 validate::finite_vec3(*vector, "ppp correction pole_tide vector_m")
704 .map_err(invalid_input)?;
705 }
706 for vector in corrections.ppp.ocean_loading.values() {
707 validate::finite_vec3(*vector, "ppp correction ocean_loading vector_m")
708 .map_err(invalid_input)?;
709 }
710 for value in corrections.ppp.windup_m.values() {
711 validate::finite(*value, "ppp correction windup_m").map_err(invalid_input)?;
712 }
713 for vector in corrections.ppp.sat_pco_ecef.values() {
714 validate::finite_vec3(*vector, "ppp correction sat_pco_ecef").map_err(invalid_input)?;
715 }
716 for value in corrections.ppp.sat_pcv_m.values() {
717 validate::finite(*value, "ppp correction sat_pcv_m").map_err(invalid_input)?;
718 }
719 for value in corrections.ppp.code_bias_m.values() {
720 validate::finite(*value, "ppp correction code_bias_m").map_err(invalid_input)?;
721 }
722 for value in corrections.ppp.ssr_code_bias_m.values() {
723 validate::finite(*value, "ppp correction ssr_code_bias_m").map_err(invalid_input)?;
724 }
725 for value in corrections.ppp.phase_bias_m.values() {
726 validate::finite(*value, "ppp correction phase_bias_m").map_err(invalid_input)?;
727 }
728 Ok(())
729}
730
731fn validate_receiver_antenna_frequency(
732 frequency: &ReceiverAntennaFrequency,
733) -> Result<(), FloatSolveError> {
734 validate::finite_vec3(frequency.pco_m, "ppp receiver antenna pco_m").map_err(invalid_input)?;
735 for sample in &frequency.pcv_samples {
736 validate_pcv_sample(sample)?;
737 }
738 Ok(())
739}
740
741fn validate_pcv_sample(sample: &PcvSample) -> Result<(), FloatSolveError> {
742 if let Some(azimuth_deg) = sample.azimuth_deg {
743 validate::finite(azimuth_deg, "ppp receiver antenna pcv azimuth_deg")
744 .map_err(invalid_input)?;
745 }
746 validate::finite_in_range(
747 sample.zenith_deg,
748 0.0,
749 180.0,
750 "ppp receiver antenna pcv zenith_deg",
751 )
752 .map_err(invalid_input)?;
753 validate::finite(sample.value_m, "ppp receiver antenna pcv value_m").map_err(invalid_input)?;
754 Ok(())
755}
756
757fn validate_fixed_ambiguity_options(
758 ambiguity: &FixedAmbiguityOptions,
759) -> Result<(), FixedSolveError> {
760 validate::finite_nonneg(
761 ambiguity.ratio_threshold,
762 "ppp fixed ambiguity ratio_threshold",
763 )
764 .map_err(invalid_fixed_input)?;
765 for value in ambiguity.wavelengths_m.values() {
766 validate::finite_positive(*value, "ppp fixed ambiguity wavelength_m")
767 .map_err(invalid_fixed_input)?;
768 }
769 for value in ambiguity.offsets_m.values() {
770 validate::finite(*value, "ppp fixed ambiguity offset_m").map_err(invalid_fixed_input)?;
771 }
772 Ok(())
773}
774
775fn validate_float_solve_options(opts: FloatSolveOptions) -> Result<(), FloatSolveError> {
776 if opts.max_iterations == 0 {
777 return Err(invalid_solve_option("max_iterations", "must be positive"));
778 }
779 if opts.max_iterations > MAX_PPP_ITERATIONS {
780 return Err(invalid_solve_option(
781 "max_iterations",
782 "exceeds the PPP iteration cap",
783 ));
784 }
785 validate_tolerance("position_tolerance_m", opts.position_tolerance_m)?;
786 validate_tolerance("clock_tolerance_m", opts.clock_tolerance_m)?;
787 validate_tolerance("ambiguity_tolerance_m", opts.ambiguity_tolerance_m)?;
788 validate_tolerance("ztd_tolerance_m", opts.ztd_tolerance_m)
789}
790
791fn validate_tolerance(field: &'static str, value: f64) -> Result<(), FloatSolveError> {
792 if validate::finite(value, field).is_err() {
793 return Err(invalid_solve_option(field, "must be finite"));
794 }
795 if value <= 0.0 {
796 return Err(invalid_solve_option(field, "must be positive"));
797 }
798 Ok(())
799}
800
801fn validate_state_clock_count(state: &FloatState, n_epochs: usize) -> Result<(), FloatSolveError> {
802 if state.clocks_m.len() == n_epochs {
803 Ok(())
804 } else {
805 Err(invalid_clock_count(n_epochs, state.clocks_m.len()))
806 }
807}
808
809fn validate_solution_clock_count(
810 solution: &FloatSolution,
811 n_epochs: usize,
812) -> Result<(), FixedSolveError> {
813 if solution.epoch_clocks_m.len() == n_epochs {
814 Ok(())
815 } else {
816 Err(FixedSolveError::Float(invalid_clock_count(
817 n_epochs,
818 solution.epoch_clocks_m.len(),
819 )))
820 }
821}
822
823fn validate_float_solution_clock_count(
824 solution: &FloatSolution,
825 n_epochs: usize,
826) -> Result<(), FloatSolveError> {
827 if solution.epoch_clocks_m.len() == n_epochs {
828 Ok(())
829 } else {
830 Err(invalid_clock_count(n_epochs, solution.epoch_clocks_m.len()))
831 }
832}
833
834fn state_from_solution(solution: &FloatSolution, prior: &FloatState) -> FloatState {
835 FloatState {
836 position_m: solution.position_m,
837 clocks_m: solution.epoch_clocks_m.clone(),
838 ambiguities_m: solution.ambiguities_m.clone(),
839 ztd_m: solution.ztd_residual_m.unwrap_or(prior.ztd_m),
840 }
841}
842
843fn estimates_ztd(tropo: TroposphereOptions) -> bool {
844 tropo.enabled && tropo.estimate_ztd
845}
846
847fn ztd_unknown_count(tropo: TroposphereOptions) -> usize {
848 usize::from(estimates_ztd(tropo))
849}
850
851fn rms(values: &[f64]) -> f64 {
852 if values.is_empty() {
853 return 0.0;
854 }
855 (values.iter().map(|v| v * v).sum::<f64>() / values.len() as f64).sqrt()
856}
857
858fn weighted_rms(rows: &[FloatResidual], weights: MeasurementWeights) -> f64 {
859 let mut values = Vec::with_capacity(rows.len() * 2);
860 for row in rows {
861 values.push(row.code_m * row.code_weight);
862 values.push(row.phase_m * row.phase_weight);
863 }
864 if values.is_empty() {
865 rms(&[0.0 * weights.code, 0.0 * weights.phase])
866 } else {
867 rms(&values)
868 }
869}
870
871fn max_abs(xs: &[f64]) -> f64 {
872 xs.iter().map(|x| x.abs()).fold(0.0, f64::max)
873}
874
875#[cfg(test)]
876mod tests;