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