1pub 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
206pub 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#[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;