Skip to main content

sidereon_core/precise_positioning/
mod.rs

1//! Static multi-epoch PPP float/fixed positioning.
2//!
3//! This module owns the language-independent static PPP orchestration: float
4//! carrier ambiguities, integer ambiguity resolution, and the fixed-ambiguity
5//! re-solve from SP3-backed ionosphere-free code and phase observations. The
6//! observation-only wide-lane/narrow-lane and cycle-slip preparation that runs
7//! ahead of the solve lives in the [`prep`] leaf submodule.
8//!
9//! ```
10//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
11//! # use std::collections::BTreeMap;
12//! # use sidereon_core::constants::F_L1_HZ;
13//! # use sidereon_core::observables::{
14//! #     predict, ObservableEphemerisSource, ObservableState, ObservablesError, PredictOptions,
15//! # };
16//! # use sidereon_core::ppp_corrections::CivilDateTime;
17//! # use sidereon_core::precise_positioning::{
18//! #     solve_kinematic_ppp, FloatEpoch, FloatObservation, KinematicConfig, KinematicState,
19//! # };
20//! # use sidereon_core::{GnssSatelliteId, GnssSystem};
21//! #
22//! # struct Source {
23//! #     states: BTreeMap<GnssSatelliteId, [f64; 3]>,
24//! # }
25//! #
26//! # impl ObservableEphemerisSource for Source {
27//! #     fn observable_state_at_j2000_s(
28//! #         &self,
29//! #         sat: GnssSatelliteId,
30//! #         _t_j2000_s: f64,
31//! #     ) -> Result<ObservableState, ObservablesError> {
32//! #         Ok(ObservableState {
33//! #             position_ecef_m: *self.states.get(&sat).ok_or(ObservablesError::NoEphemeris)?,
34//! #             clock_s: Some(0.0),
35//! #         })
36//! #     }
37//! # }
38//! #
39//! # fn diagonal_covariance(dimension: usize, variance_m2: f64) -> Vec<Vec<f64>> {
40//! #     let mut covariance = vec![vec![0.0; dimension]; dimension];
41//! #     for (idx, row) in covariance.iter_mut().enumerate() {
42//! #         row[idx] = variance_m2;
43//! #     }
44//! #     covariance
45//! # }
46//! #
47//! # let sats = [
48//! #     (1u8, [20_200_000.0, 13_000_000.0, 21_500_000.0]),
49//! #     (2, [-21_300_000.0, 14_500_000.0, 20_700_000.0]),
50//! #     (3, [15_200_000.0, -22_000_000.0, 19_500_000.0]),
51//! #     (4, [-18_200_000.0, -16_000_000.0, 21_000_000.0]),
52//! #     (5, [22_000_000.0, -12_000_000.0, 20_200_000.0]),
53//! # ];
54//! # let ids = sats
55//! #     .iter()
56//! #     .map(|(prn, _)| GnssSatelliteId::new(GnssSystem::Gps, *prn))
57//! #     .collect::<Result<Vec<_>, _>>()?;
58//! # let source = Source {
59//! #     states: ids
60//! #         .iter()
61//! #         .zip(sats.iter())
62//! #         .map(|(id, (_, position))| (*id, *position))
63//! #         .collect(),
64//! # };
65//! # let truth = [3_512_900.0, 780_500.0, 5_248_700.0];
66//! # let clock_m = 12.5;
67//! # let ambiguities_m = ids
68//! #     .iter()
69//! #     .enumerate()
70//! #     .map(|(idx, id)| (id.to_string(), 0.25 + idx as f64 * 0.1))
71//! #     .collect::<BTreeMap<_, _>>();
72//! # let observations = ids
73//! #     .iter()
74//! #     .map(|id| {
75//! #         let prediction = predict(
76//! #             &source,
77//! #             *id,
78//! #             truth,
79//! #             0.0,
80//! #             PredictOptions {
81//! #                 carrier_hz: F_L1_HZ,
82//! #                 light_time: true,
83//! #                 sagnac: true,
84//! #             },
85//! #         )?;
86//! #         let code_m = prediction.geometric_range_m + clock_m;
87//! #         let ambiguity_m = ambiguities_m.get(&id.to_string()).copied().unwrap();
88//! #         Ok(FloatObservation {
89//! #             sat: *id,
90//! #             satellite_id: id.to_string(),
91//! #             ambiguity_id: id.to_string(),
92//! #             code_m,
93//! #             phase_m: code_m + ambiguity_m,
94//! #             freq1_hz: 0.0,
95//! #             freq2_hz: 0.0,
96//! #         })
97//! #     })
98//! #     .collect::<Result<Vec<_>, ObservablesError>>()?;
99//! # let epoch = FloatEpoch {
100//! #     epoch: CivilDateTime {
101//! #         year: 2020,
102//! #         month: 6,
103//! #         day: 24,
104//! #         hour: 12,
105//! #         minute: 0,
106//! #         second: 0.0,
107//! #     },
108//! #     jd_whole: 2_459_024.5,
109//! #     jd_fraction: 0.5,
110//! #     t_rx_j2000_s: 0.0,
111//! #     observations,
112//! # };
113//! # let initial_state = KinematicState {
114//! #     position_m: [truth[0] + 5.0, truth[1] - 4.0, truth[2] + 3.0],
115//! #     clock_m: 0.0,
116//! #     ztd_residual_m: 0.0,
117//! #     ambiguities_m,
118//! # };
119//! # let config = KinematicConfig {
120//! #     initial_covariance_m2: diagonal_covariance(initial_state.dimension(), 1.0e8),
121//! #     initial_state,
122//! #     ..KinematicConfig::default()
123//! # };
124//! let solutions = solve_kinematic_ppp(&source, &[epoch], config)?;
125//! assert_eq!(solutions.len(), 1);
126//! assert!(solutions[0].innovation_rms_m.is_finite());
127//! # Ok(())
128//! # }
129//! ```
130
131pub mod cycle_slip;
132mod fixed;
133mod float;
134mod kinematic;
135mod model;
136mod normal;
137mod prep;
138pub mod raim;
139mod rows;
140pub mod tec;
141mod types;
142pub mod velocity;
143
144pub use cycle_slip::{
145    detect_cycle_slips, geometry_free_m, melbourne_wubbena_cycles, update_geometry_free,
146    update_melbourne_wubbena, CycleSlipConfig, CycleSlipConfigError, CycleSlipDetectorState,
147    CycleSlipError, CycleSlipFlagEpoch, CycleSlipFlagObservation, CycleSlipStateKey,
148    GeometryFreeUpdate, MelbourneWubbenaUpdate, RunningMeanVariance, SatelliteCycleSlipState,
149    DEFAULT_MINIMUM_ARC_LENGTH, DEFAULT_RUNNING_STATISTIC_K_FACTOR,
150};
151pub(crate) use fixed::run_fixed_from_float;
152pub use fixed::solve_fixed_from_float;
153#[cfg(test)]
154use float::initial_ambiguities;
155pub(crate) use float::run_float_epochs;
156pub use float::{solve_float_epoch, solve_float_epochs};
157pub use kinematic::{
158    correct_kinematic_state, predict_kinematic_state, solve_kinematic_ppp, KinematicConfig,
159    KinematicEpochSolution, KinematicEpochStatus, KinematicMotionModel,
160    KinematicPositionProcessNoise, KinematicProcessNoise, KinematicSolveError, KinematicState,
161    KinematicUpdateSummary,
162};
163pub use prep::{
164    prepare_widelane_fixed_epochs, split_float_cycle_slip_epochs, DualFrequencyEpoch,
165    DualFrequencyObservation, FloatCycleSlipEpoch, FloatCycleSlipObservation,
166    FloatCycleSlipTaggedEpoch, FloatCycleSlipTaggedObservation, PppSplitArc, PreparedFloatEpoch,
167    PreparedFloatObservation, WideLanePrepError, WideLanePrepOptions, WideLanePrepResult,
168};
169pub use raim::{
170    solve_float_epoch_with_raim, ProtectionLevels, RaimConfig, RaimError, RaimFdeError,
171    RaimFdeResult, RaimFdeStatus, RaimGeometryRow, RaimIdentification, RaimResult, RaimStatus,
172    SatelliteTestStatistic,
173};
174pub use tec::{
175    code_geometry_free_m, estimate_code_slant_tec, estimate_phase_slant_tec, estimate_tec,
176    ionospheric_pierce_point, level_slant_tec_arc, phase_geometry_free_m,
177    slant_tec_from_code_geometry_free_m, slant_tec_from_phase_geometry_free_m,
178    thin_shell_mapping_function, vertical_tec_from_slant_tec, CodeSlantTecEstimate,
179    IonosphericPiercePoint, LeveledTecSample, PhaseSlantTecEstimate, TecConfig, TecEpoch, TecError,
180    TecEstimate, TecEstimateSample, TecLevelingResult, TecLevelingSample, TecObservation,
181    TecSatelliteArc, DEFAULT_IONOSPHERIC_SHELL_HEIGHT_M, ELECTRONS_PER_TECU_M2,
182    TEC_GROUP_DELAY_COEFFICIENT,
183};
184pub use types::*;
185pub use velocity::{
186    predict_range_rate_m_s, solve_velocity, RangeRatePrediction, ReceiverVelocityState,
187    VelocityConfig, VelocityObservation, VelocityRobustConfig, VelocitySolution,
188    VelocitySolveError,
189};
190
191pub use crate::ambiguity::CycleSlipPolicy;
192
193use std::collections::BTreeMap;
194
195use crate::constants::F_L1_HZ;
196use crate::estimation::recipe::NormalRecipe;
197use crate::observables::{ObservableEphemerisSource, ObservablesError, PredictOptions};
198use crate::ppp_corrections::{
199    self, PppCorrectionEpoch, PppCorrectionObservation, PppCorrectionsError, PppCorrectionsOptions,
200};
201use crate::sp3::Sp3;
202use crate::validate::{self, FieldError};
203
204const MAX_PPP_ITERATIONS: usize = 10_000;
205
206/// Build indexed PPP correction lookups at the static-arc seed position.
207pub fn build_ppp_lookup(
208    sp3: &Sp3,
209    epochs: &[FloatEpoch],
210    receiver_ecef_m: [f64; 3],
211    options: &PppCorrectionsOptions,
212) -> Result<PppCorrectionLookup, PppCorrectionsError> {
213    let ppp_epochs: Vec<PppCorrectionEpoch> = epochs
214        .iter()
215        .map(|epoch| PppCorrectionEpoch {
216            epoch: epoch.epoch,
217            t_rx_j2000_s: epoch.t_rx_j2000_s,
218            observations: epoch
219                .observations
220                .iter()
221                .map(|obs| PppCorrectionObservation {
222                    sat: obs.sat,
223                    freq1_hz: obs.freq1_hz,
224                    freq2_hz: obs.freq2_hz,
225                })
226                .collect(),
227        })
228        .collect();
229    let corrections = ppp_corrections::build(sp3, &ppp_epochs, receiver_ecef_m, options)?;
230    Ok(PppCorrectionLookup::from_options(corrections, options))
231}
232
233impl FloatState {
234    fn default_for_epochs(epochs: &[FloatEpoch]) -> Self {
235        Self {
236            position_m: [0.0; 3],
237            clocks_m: vec![0.0; epochs.len()],
238            ambiguities_m: BTreeMap::new(),
239            ztd_m: 0.0,
240        }
241    }
242}
243
244/// Measurement-model invariants shared across a whole static PPP solve: the
245/// ephemeris source, measurement weighting, troposphere controls, the precomputed
246/// range corrections, and the normal-equation operation-order recipe. Bundled so
247/// the iterated solve, residual, and finalize helpers take one context argument
248/// instead of repeating the same parameters everywhere; carrying `normal` here
249/// lets the resolved recipe reach the solve seam without threading a parameter
250/// through every iterate/screen helper.
251#[derive(Clone, Copy)]
252struct ModelContext<'a> {
253    source: &'a dyn ObservableEphemerisSource,
254    weights: MeasurementWeights,
255    tropo: TroposphereOptions,
256    corrections: &'a RangeCorrections,
257    normal: NormalRecipe,
258}
259
260fn predict_default(
261    _source: &dyn ObservableEphemerisSource,
262    _obs: &FloatObservation,
263) -> Result<PredictOptions, FloatSolveError> {
264    Ok(PredictOptions {
265        carrier_hz: F_L1_HZ,
266        light_time: true,
267        sagnac: true,
268    })
269}
270
271fn no_ephemeris(obs: &FloatObservation, error: ObservablesError) -> FloatSolveError {
272    FloatSolveError::NoEphemeris {
273        satellite_id: obs.satellite_id.clone(),
274        reason: match error {
275            ObservablesError::NoEphemeris => NoEphemerisReason::NoEphemeris,
276            ObservablesError::InvalidInput { .. } => NoEphemerisReason::Reason(error.to_string()),
277            ObservablesError::Ephemeris(err) => NoEphemerisReason::Reason(err.to_string()),
278        },
279    }
280}
281
282fn missing_satellite_clock(obs: &FloatObservation) -> FloatSolveError {
283    FloatSolveError::NoEphemeris {
284        satellite_id: obs.satellite_id.clone(),
285        reason: NoEphemerisReason::MissingSatelliteClock,
286    }
287}
288
289fn missing_correction(obs: &FloatObservation, correction: MissingCorrection) -> FloatSolveError {
290    FloatSolveError::MissingCorrection {
291        satellite_id: obs.satellite_id.clone(),
292        correction,
293    }
294}
295
296fn invalid_clock_count(expected: usize, actual: usize) -> FloatSolveError {
297    FloatSolveError::InvalidClockCount { expected, actual }
298}
299
300fn invalid_solve_option(field: &'static str, reason: &'static str) -> FloatSolveError {
301    FloatSolveError::InvalidSolveOption { field, reason }
302}
303
304pub(super) fn invalid_input(error: FieldError) -> FloatSolveError {
305    invalid_input_field(error.field(), error.reason())
306}
307
308fn invalid_input_field(field: &'static str, reason: &'static str) -> FloatSolveError {
309    FloatSolveError::InvalidInput { field, reason }
310}
311
312fn invalid_fixed_input(error: FieldError) -> FixedSolveError {
313    FixedSolveError::Float(invalid_input(error))
314}
315
316pub(super) fn validate_float_solve_boundary(
317    epochs: &[FloatEpoch],
318    state: &FloatState,
319    config: &FloatSolveConfig,
320) -> Result<(), FloatSolveError> {
321    validate_epochs(epochs)?;
322    validate_float_state(state, epochs.len())?;
323    validate_float_config(config)
324}
325
326pub(super) fn validate_fixed_solve_boundary(
327    epochs: &[FloatEpoch],
328    solution: &FloatSolution,
329    config: &FixedSolveConfig,
330) -> Result<(), FixedSolveError> {
331    validate_epochs(epochs).map_err(FixedSolveError::Float)?;
332    validate_float_solution(solution, epochs.len())?;
333    validate_fixed_config(config)
334}
335
336fn validate_epochs(epochs: &[FloatEpoch]) -> Result<(), FloatSolveError> {
337    for epoch in epochs {
338        validate_epoch(epoch)?;
339    }
340    Ok(())
341}
342
343fn validate_epoch(epoch: &FloatEpoch) -> Result<(), FloatSolveError> {
344    validate::civil_datetime_with_second_policy(
345        epoch.epoch.year as i64,
346        epoch.epoch.month as i64,
347        epoch.epoch.day as i64,
348        epoch.epoch.hour as i64,
349        epoch.epoch.minute as i64,
350        epoch.epoch.second,
351        validate::CivilSecondPolicy::Continuous,
352    )
353    .map_err(invalid_input)?;
354    validate::finite(epoch.jd_whole, "ppp epoch jd_whole").map_err(invalid_input)?;
355    validate::finite(epoch.jd_fraction, "ppp epoch jd_fraction").map_err(invalid_input)?;
356    validate::finite(epoch.t_rx_j2000_s, "ppp epoch t_rx_j2000_s").map_err(invalid_input)?;
357    for obs in &epoch.observations {
358        validate_observation(obs)?;
359    }
360    Ok(())
361}
362
363fn validate_observation(obs: &FloatObservation) -> Result<(), FloatSolveError> {
364    validate::finite(obs.code_m, "ppp observation code_m").map_err(invalid_input)?;
365    validate::finite(obs.phase_m, "ppp observation phase_m").map_err(invalid_input)?;
366    validate::finite(obs.freq1_hz, "ppp observation freq1_hz").map_err(invalid_input)?;
367    validate::finite(obs.freq2_hz, "ppp observation freq2_hz").map_err(invalid_input)?;
368    Ok(())
369}
370
371fn validate_float_state(state: &FloatState, n_epochs: usize) -> Result<(), FloatSolveError> {
372    validate_state_clock_count(state, n_epochs)?;
373    validate::finite_vec3(state.position_m, "ppp state position_m").map_err(invalid_input)?;
374    validate::finite_slice(&state.clocks_m, "ppp state clocks_m").map_err(invalid_input)?;
375    for value in state.ambiguities_m.values() {
376        validate::finite(*value, "ppp state ambiguities_m").map_err(invalid_input)?;
377    }
378    validate::finite(state.ztd_m, "ppp state ztd_m").map_err(invalid_input)?;
379    Ok(())
380}
381
382fn validate_float_solution(
383    solution: &FloatSolution,
384    n_epochs: usize,
385) -> Result<(), FixedSolveError> {
386    validate_solution_clock_count(solution, n_epochs)?;
387    validate::finite_vec3(solution.position_m, "ppp float_solution position_m")
388        .map_err(invalid_fixed_input)?;
389    validate::finite_slice(
390        &solution.epoch_clocks_m,
391        "ppp float_solution epoch_clocks_m",
392    )
393    .map_err(invalid_fixed_input)?;
394    for value in solution.ambiguities_m.values() {
395        validate::finite(*value, "ppp float_solution ambiguities_m")
396            .map_err(invalid_fixed_input)?;
397    }
398    if let Some(ztd_m) = solution.ztd_residual_m {
399        validate::finite(ztd_m, "ppp float_solution ztd_residual_m")
400            .map_err(invalid_fixed_input)?;
401    }
402    for residual in &solution.residuals_m {
403        validate::finite(residual.code_m, "ppp float_solution residual code_m")
404            .map_err(invalid_fixed_input)?;
405        validate::finite(residual.phase_m, "ppp float_solution residual phase_m")
406            .map_err(invalid_fixed_input)?;
407        validate::finite(
408            residual.code_weight,
409            "ppp float_solution residual code_weight",
410        )
411        .map_err(invalid_fixed_input)?;
412        validate::finite(
413            residual.phase_weight,
414            "ppp float_solution residual phase_weight",
415        )
416        .map_err(invalid_fixed_input)?;
417    }
418    validate::finite_nonneg(solution.code_rms_m, "ppp float_solution code_rms_m")
419        .map_err(invalid_fixed_input)?;
420    validate::finite_nonneg(solution.phase_rms_m, "ppp float_solution phase_rms_m")
421        .map_err(invalid_fixed_input)?;
422    validate::finite_nonneg(solution.weighted_rms_m, "ppp float_solution weighted_rms_m")
423        .map_err(invalid_fixed_input)?;
424    Ok(())
425}
426
427pub(super) fn validate_float_solution_output(
428    solution: &FloatSolution,
429    n_epochs: usize,
430) -> Result<(), FloatSolveError> {
431    validate_float_solution_clock_count(solution, n_epochs)?;
432    validate::finite_vec3(solution.position_m, "ppp float_solution position_m")
433        .map_err(invalid_input)?;
434    validate::finite_slice(
435        &solution.epoch_clocks_m,
436        "ppp float_solution epoch_clocks_m",
437    )
438    .map_err(invalid_input)?;
439    for value in solution.ambiguities_m.values() {
440        validate::finite(*value, "ppp float_solution ambiguities_m").map_err(invalid_input)?;
441    }
442    if let Some(ztd_m) = solution.ztd_residual_m {
443        validate::finite(ztd_m, "ppp float_solution ztd_residual_m").map_err(invalid_input)?;
444    }
445    for residual in &solution.residuals_m {
446        validate::finite(residual.code_m, "ppp float_solution residual code_m")
447            .map_err(invalid_input)?;
448        validate::finite(residual.phase_m, "ppp float_solution residual phase_m")
449            .map_err(invalid_input)?;
450        validate::finite(
451            residual.code_weight,
452            "ppp float_solution residual code_weight",
453        )
454        .map_err(invalid_input)?;
455        validate::finite(
456            residual.phase_weight,
457            "ppp float_solution residual phase_weight",
458        )
459        .map_err(invalid_input)?;
460    }
461    validate::finite_nonneg(solution.code_rms_m, "ppp float_solution code_rms_m")
462        .map_err(invalid_input)?;
463    validate::finite_nonneg(solution.phase_rms_m, "ppp float_solution phase_rms_m")
464        .map_err(invalid_input)?;
465    validate::finite_nonneg(solution.weighted_rms_m, "ppp float_solution weighted_rms_m")
466        .map_err(invalid_input)?;
467    Ok(())
468}
469
470fn validate_float_config(config: &FloatSolveConfig) -> Result<(), FloatSolveError> {
471    validate_common_config(
472        config.weights,
473        config.tropo,
474        &config.corrections,
475        config.opts,
476    )
477}
478
479fn validate_fixed_config(config: &FixedSolveConfig) -> Result<(), FixedSolveError> {
480    validate_common_config(
481        config.weights,
482        config.tropo,
483        &config.corrections,
484        config.opts,
485    )
486    .map_err(FixedSolveError::Float)?;
487    validate_fixed_ambiguity_options(&config.ambiguity)
488}
489
490fn validate_common_config(
491    weights: MeasurementWeights,
492    tropo: TroposphereOptions,
493    corrections: &RangeCorrections,
494    opts: FloatSolveOptions,
495) -> Result<(), FloatSolveError> {
496    validate_measurement_weights(weights)?;
497    validate_troposphere_options(tropo)?;
498    validate_range_corrections(corrections)?;
499    validate_float_solve_options(opts)
500}
501
502fn validate_measurement_weights(weights: MeasurementWeights) -> Result<(), FloatSolveError> {
503    validate::finite_positive(weights.code, "ppp measurement weight code")
504        .map_err(invalid_input)?;
505    validate::finite_positive(weights.phase, "ppp measurement weight phase")
506        .map_err(invalid_input)?;
507    Ok(())
508}
509
510fn validate_troposphere_options(tropo: TroposphereOptions) -> Result<(), FloatSolveError> {
511    if !tropo.enabled {
512        return Ok(());
513    }
514    validate::finite_positive(tropo.met.pressure_hpa, "ppp tropo pressure_hpa")
515        .map_err(invalid_input)?;
516    validate::finite_positive(tropo.met.temperature_k, "ppp tropo temperature_k")
517        .map_err(invalid_input)?;
518    validate::fraction(tropo.met.relative_humidity, "ppp tropo relative_humidity")
519        .map_err(invalid_input)?;
520    Ok(())
521}
522
523fn validate_range_corrections(corrections: &RangeCorrections) -> Result<(), FloatSolveError> {
524    if let Some(receiver) = &corrections.receiver_antenna {
525        validate::finite_positive(receiver.freq1_hz, "ppp receiver antenna freq1_hz")
526            .map_err(invalid_input)?;
527        validate::finite_positive(receiver.freq2_hz, "ppp receiver antenna freq2_hz")
528            .map_err(invalid_input)?;
529        if receiver.freq1_hz == receiver.freq2_hz {
530            return Err(invalid_input_field(
531                "ppp receiver antenna frequency pair",
532                "must differ",
533            ));
534        }
535        for frequency in &receiver.frequencies {
536            validate_receiver_antenna_frequency(frequency)?;
537        }
538    }
539    if let Some(clock) = &corrections.satellite_clock {
540        for records in clock.series.values() {
541            validate::require_strictly_increasing(
542                records.iter().map(|&(t_gps_s, _)| t_gps_s),
543                "ppp satellite clock epoch_s",
544            )
545            .map_err(invalid_input)?;
546            for &(t_gps_s, bias_s) in records {
547                validate::finite(t_gps_s, "ppp satellite clock epoch_s").map_err(invalid_input)?;
548                validate::finite(bias_s, "ppp satellite clock bias_s").map_err(invalid_input)?;
549            }
550        }
551    }
552    for vector in corrections.ppp.tide.values() {
553        validate::finite_vec3(*vector, "ppp correction tide vector_m").map_err(invalid_input)?;
554    }
555    for value in corrections.ppp.windup_m.values() {
556        validate::finite(*value, "ppp correction windup_m").map_err(invalid_input)?;
557    }
558    for vector in corrections.ppp.sat_pco_ecef.values() {
559        validate::finite_vec3(*vector, "ppp correction sat_pco_ecef").map_err(invalid_input)?;
560    }
561    for value in corrections.ppp.sat_pcv_m.values() {
562        validate::finite(*value, "ppp correction sat_pcv_m").map_err(invalid_input)?;
563    }
564    Ok(())
565}
566
567fn validate_receiver_antenna_frequency(
568    frequency: &ReceiverAntennaFrequency,
569) -> Result<(), FloatSolveError> {
570    validate::finite_vec3(frequency.pco_m, "ppp receiver antenna pco_m").map_err(invalid_input)?;
571    for sample in &frequency.pcv_samples {
572        validate_pcv_sample(sample)?;
573    }
574    Ok(())
575}
576
577fn validate_pcv_sample(sample: &PcvSample) -> Result<(), FloatSolveError> {
578    if let Some(azimuth_deg) = sample.azimuth_deg {
579        validate::finite(azimuth_deg, "ppp receiver antenna pcv azimuth_deg")
580            .map_err(invalid_input)?;
581    }
582    validate::finite_in_range(
583        sample.zenith_deg,
584        0.0,
585        180.0,
586        "ppp receiver antenna pcv zenith_deg",
587    )
588    .map_err(invalid_input)?;
589    validate::finite(sample.value_m, "ppp receiver antenna pcv value_m").map_err(invalid_input)?;
590    Ok(())
591}
592
593fn validate_fixed_ambiguity_options(
594    ambiguity: &FixedAmbiguityOptions,
595) -> Result<(), FixedSolveError> {
596    validate::finite_nonneg(
597        ambiguity.ratio_threshold,
598        "ppp fixed ambiguity ratio_threshold",
599    )
600    .map_err(invalid_fixed_input)?;
601    for value in ambiguity.wavelengths_m.values() {
602        validate::finite_positive(*value, "ppp fixed ambiguity wavelength_m")
603            .map_err(invalid_fixed_input)?;
604    }
605    for value in ambiguity.offsets_m.values() {
606        validate::finite(*value, "ppp fixed ambiguity offset_m").map_err(invalid_fixed_input)?;
607    }
608    Ok(())
609}
610
611fn validate_float_solve_options(opts: FloatSolveOptions) -> Result<(), FloatSolveError> {
612    if opts.max_iterations == 0 {
613        return Err(invalid_solve_option("max_iterations", "must be positive"));
614    }
615    if opts.max_iterations > MAX_PPP_ITERATIONS {
616        return Err(invalid_solve_option(
617            "max_iterations",
618            "exceeds the PPP iteration cap",
619        ));
620    }
621    validate_tolerance("position_tolerance_m", opts.position_tolerance_m)?;
622    validate_tolerance("clock_tolerance_m", opts.clock_tolerance_m)?;
623    validate_tolerance("ambiguity_tolerance_m", opts.ambiguity_tolerance_m)?;
624    validate_tolerance("ztd_tolerance_m", opts.ztd_tolerance_m)
625}
626
627fn validate_tolerance(field: &'static str, value: f64) -> Result<(), FloatSolveError> {
628    if validate::finite(value, field).is_err() {
629        return Err(invalid_solve_option(field, "must be finite"));
630    }
631    if value <= 0.0 {
632        return Err(invalid_solve_option(field, "must be positive"));
633    }
634    Ok(())
635}
636
637fn validate_state_clock_count(state: &FloatState, n_epochs: usize) -> Result<(), FloatSolveError> {
638    if state.clocks_m.len() == n_epochs {
639        Ok(())
640    } else {
641        Err(invalid_clock_count(n_epochs, state.clocks_m.len()))
642    }
643}
644
645fn validate_solution_clock_count(
646    solution: &FloatSolution,
647    n_epochs: usize,
648) -> Result<(), FixedSolveError> {
649    if solution.epoch_clocks_m.len() == n_epochs {
650        Ok(())
651    } else {
652        Err(FixedSolveError::Float(invalid_clock_count(
653            n_epochs,
654            solution.epoch_clocks_m.len(),
655        )))
656    }
657}
658
659fn validate_float_solution_clock_count(
660    solution: &FloatSolution,
661    n_epochs: usize,
662) -> Result<(), FloatSolveError> {
663    if solution.epoch_clocks_m.len() == n_epochs {
664        Ok(())
665    } else {
666        Err(invalid_clock_count(n_epochs, solution.epoch_clocks_m.len()))
667    }
668}
669
670fn state_from_solution(solution: &FloatSolution, prior: &FloatState) -> FloatState {
671    FloatState {
672        position_m: solution.position_m,
673        clocks_m: solution.epoch_clocks_m.clone(),
674        ambiguities_m: solution.ambiguities_m.clone(),
675        ztd_m: solution.ztd_residual_m.unwrap_or(prior.ztd_m),
676    }
677}
678
679fn estimates_ztd(tropo: TroposphereOptions) -> bool {
680    tropo.enabled && tropo.estimate_ztd
681}
682
683fn ztd_unknown_count(tropo: TroposphereOptions) -> usize {
684    usize::from(estimates_ztd(tropo))
685}
686
687fn rms(values: &[f64]) -> f64 {
688    if values.is_empty() {
689        return 0.0;
690    }
691    (values.iter().map(|v| v * v).sum::<f64>() / values.len() as f64).sqrt()
692}
693
694fn weighted_rms(rows: &[FloatResidual], weights: MeasurementWeights) -> f64 {
695    let mut values = Vec::with_capacity(rows.len() * 2);
696    for row in rows {
697        values.push(row.code_m * row.code_weight);
698        values.push(row.phase_m * row.phase_weight);
699    }
700    if values.is_empty() {
701        rms(&[0.0 * weights.code, 0.0 * weights.phase])
702    } else {
703        rms(&values)
704    }
705}
706
707fn max_abs(xs: &[f64]) -> f64 {
708    xs.iter().map(|x| x.abs()).fold(0.0, f64::max)
709}
710
711#[cfg(test)]
712mod tests;