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