1use std::cell::Cell;
33use std::collections::{BTreeMap, BTreeSet};
34
35use nalgebra::{DMatrix, DVector};
36
37use crate::astro::math::least_squares::{
38 self, normal_covariance, singular_value_diagnostics, solve_trf_with, LeastSquaresProblem,
39 SolveOptions, Status, TrustRegionSolve,
40};
41use crate::astro::math::robust::{huber_weight, mad_scale, RobustError};
42use crate::dop::rotate_covariance_ecef_to_enu_m2;
43use crate::estimation::substrate::frames::geodetic_from_ecef;
44use crate::frame::{ItrfPositionM, Wgs84Geodetic};
45use crate::geometry_quality::{classify, GeometryQuality, GeometryQualityThresholds};
46use crate::id::{GnssSatelliteId, GnssSystem};
47use crate::sbas::SbasIonoGrid;
48use crate::spp::{
49 clock_systems, residual_unweighted, select_sats, spp_iono_frequency_hz, validate_solve_inputs,
50 Corrections, EphemerisSource, GalileoNequickCoeffs, KlobucharCoeffs, Observation, RejectedSat,
51 RobustConfig, SolveInputs, SppError, SppInputErrorKind, SppModelRecipe, SurfaceMet, C_M_S,
52};
53use crate::validate;
54
55const STATIC_SOLVER_GTOL: f64 = 1e-14;
56const STATIC_SOLVER_FTOL: f64 = 1e-15;
57const STATIC_SOLVER_XTOL: f64 = 1e-14;
58const STATIC_SOLVER_MAX_NFEV: usize = 400;
59
60#[derive(Debug, Clone, PartialEq)]
67pub struct StaticEpoch {
68 pub measurements: Vec<Observation>,
70 pub weights: Option<Vec<f64>>,
73 pub t_rx_j2000_s: f64,
75 pub t_rx_second_of_day_s: f64,
77 pub day_of_year: f64,
79 pub clock_initial_m: f64,
81 pub corrections: Corrections,
83 pub klobuchar: KlobucharCoeffs,
85 pub beidou_klobuchar: Option<KlobucharCoeffs>,
87 pub galileo_nequick: Option<GalileoNequickCoeffs>,
89 pub sbas_iono: Option<SbasIonoGrid>,
91 pub glonass_channels: BTreeMap<u8, i8>,
93 pub met: SurfaceMet,
95}
96
97impl StaticEpoch {
98 pub fn from_solve_inputs(inputs: SolveInputs) -> Self {
106 Self {
107 measurements: inputs.observations,
108 weights: None,
109 t_rx_j2000_s: inputs.t_rx_j2000_s,
110 t_rx_second_of_day_s: inputs.t_rx_second_of_day_s,
111 day_of_year: inputs.day_of_year,
112 clock_initial_m: inputs.initial_guess[3],
113 corrections: inputs.corrections,
114 klobuchar: inputs.klobuchar,
115 beidou_klobuchar: inputs.beidou_klobuchar,
116 galileo_nequick: inputs.galileo_nequick,
117 sbas_iono: inputs.sbas_iono,
118 glonass_channels: inputs.glonass_channels,
119 met: inputs.met,
120 }
121 }
122}
123
124#[derive(Debug, Clone, Copy, PartialEq)]
126pub struct StaticSolveOptions {
127 pub initial_position_m: [f64; 3],
129 pub with_geodetic: bool,
131 pub robust: Option<RobustConfig>,
133}
134
135impl StaticSolveOptions {
136 pub fn from_solve_inputs(inputs: &SolveInputs, with_geodetic: bool) -> Self {
141 Self {
142 initial_position_m: [
143 inputs.initial_guess[0],
144 inputs.initial_guess[1],
145 inputs.initial_guess[2],
146 ],
147 with_geodetic,
148 robust: inputs.robust,
149 }
150 }
151}
152
153impl Default for StaticSolveOptions {
154 fn default() -> Self {
155 Self {
156 initial_position_m: [0.0; 3],
157 with_geodetic: false,
158 robust: None,
159 }
160 }
161}
162
163#[derive(Debug, Clone, Copy, PartialEq)]
165pub struct StaticClockBias {
166 pub epoch_index: usize,
168 pub system: GnssSystem,
170 pub clock_s: f64,
172}
173
174#[derive(Debug, Clone, PartialEq)]
180pub struct StaticCovariance {
181 pub state_m2: Vec<Vec<f64>>,
183 pub position_ecef_m2: [[f64; 3]; 3],
185 pub position_enu_m2: [[f64; 3]; 3],
187}
188
189#[derive(Debug, Clone, Copy, PartialEq)]
191pub struct StaticResidual {
192 pub epoch_index: usize,
194 pub satellite_id: GnssSatelliteId,
196 pub residual_m: f64,
198 pub base_weight: f64,
200 pub effective_weight: f64,
202 pub robust_weight_ratio: f64,
204}
205
206#[derive(Debug, Clone, Copy, PartialEq, Eq)]
208pub enum StaticInfluenceStatus {
209 Solved,
211 TooFewMeasurements,
213 SingularGeometry,
215 InvalidInput,
217 EphemerisUnavailable,
219 SolveFailed,
221}
222
223#[derive(Debug, Clone, PartialEq)]
225pub struct StaticEpochInfluence {
226 pub epoch_index: usize,
228 pub omitted_measurements: usize,
230 pub status: StaticInfluenceStatus,
232 pub position_delta_m: Option<[f64; 3]>,
234 pub position_delta_norm_m: Option<f64>,
236 pub residual_rms_m: Option<f64>,
238 pub min_robust_weight_ratio: f64,
240}
241
242#[derive(Debug, Clone, PartialEq)]
244pub struct StaticSatelliteInfluence {
245 pub epoch_index: usize,
247 pub satellite_id: GnssSatelliteId,
249 pub status: StaticInfluenceStatus,
251 pub position_delta_m: Option<[f64; 3]>,
253 pub position_delta_norm_m: Option<f64>,
255 pub residual_rms_m: Option<f64>,
257 pub residual_m: f64,
259 pub base_weight: f64,
261 pub effective_weight: f64,
263 pub robust_weight_ratio: f64,
265}
266
267#[derive(Debug, Clone, PartialEq)]
269pub struct StaticSatelliteBatchInfluence {
270 pub satellite_id: GnssSatelliteId,
272 pub omitted_measurements: usize,
274 pub status: StaticInfluenceStatus,
276 pub position_delta_m: Option<[f64; 3]>,
278 pub position_delta_norm_m: Option<f64>,
280 pub residual_rms_m: Option<f64>,
282 pub min_robust_weight_ratio: f64,
284}
285
286#[derive(Debug, Clone, PartialEq)]
288pub struct StaticSolutionMetadata {
289 pub iterations: usize,
291 pub converged: bool,
293 pub status: Status,
295 pub outer_iterations: usize,
297 pub final_robust_scale_m: Option<f64>,
299 pub used_measurements: usize,
301 pub n_parameters: usize,
303 pub redundancy: isize,
305}
306
307#[derive(Debug, Clone, PartialEq)]
309pub struct StaticSolution {
310 pub position: ItrfPositionM,
312 pub geodetic: Option<Wgs84Geodetic>,
314 pub per_epoch_clock: Vec<StaticClockBias>,
316 pub covariance: StaticCovariance,
318 pub per_epoch_influence: Vec<StaticEpochInfluence>,
320 pub per_satellite_influence: Vec<StaticSatelliteInfluence>,
322 pub per_satellite_batch_influence: Vec<StaticSatelliteBatchInfluence>,
324 pub geometry_quality: GeometryQuality,
326 pub residuals_m: Vec<StaticResidual>,
328 pub used_sats: Vec<Vec<GnssSatelliteId>>,
330 pub rejected_sats: Vec<Vec<RejectedSat>>,
332 pub metadata: StaticSolutionMetadata,
334}
335
336impl StaticSolution {
337 pub fn residual_rms_m(&self) -> f64 {
339 residual_rms(
340 &self
341 .residuals_m
342 .iter()
343 .map(|r| r.residual_m)
344 .collect::<Vec<_>>(),
345 )
346 }
347}
348
349#[derive(Debug, Clone)]
351pub enum StaticSolveError {
352 EmptyEpochs,
354 InvalidInput {
356 field: &'static str,
358 kind: SppInputErrorKind,
360 },
361 EpochInput {
363 epoch_index: usize,
365 source: SppError,
367 },
368 DuplicateObservation {
370 epoch_index: usize,
372 satellite: GnssSatelliteId,
374 },
375 IonosphereUnsupported {
377 epoch_index: usize,
379 satellite: GnssSatelliteId,
381 },
382 TooFewMeasurements {
384 used: usize,
386 required: usize,
388 },
389 EphemerisLost {
391 epoch_index: usize,
393 satellite: GnssSatelliteId,
395 },
396 Singular(least_squares::SolveError),
398}
399
400impl core::fmt::Display for StaticSolveError {
401 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
402 match self {
403 Self::EmptyEpochs => write!(f, "no static epochs supplied"),
404 Self::InvalidInput { field, kind } => {
405 write!(f, "invalid static solve input {field}: {kind}")
406 }
407 Self::EpochInput {
408 epoch_index,
409 source,
410 } => write!(f, "invalid static epoch {epoch_index}: {source}"),
411 Self::DuplicateObservation {
412 epoch_index,
413 satellite,
414 } => write!(
415 f,
416 "static epoch {epoch_index} observes satellite {satellite} more than once"
417 ),
418 Self::IonosphereUnsupported {
419 epoch_index,
420 satellite,
421 } => write!(
422 f,
423 "static epoch {epoch_index} has no ionosphere carrier model for {satellite}"
424 ),
425 Self::TooFewMeasurements { used, required } => write!(
426 f,
427 "only {used} usable static measurements; need at least {required}"
428 ),
429 Self::EphemerisLost {
430 epoch_index,
431 satellite,
432 } => write!(
433 f,
434 "static epoch {epoch_index} satellite {satellite} lost ephemeris during the solve"
435 ),
436 Self::Singular(error) => write!(f, "static geometry is singular: {error}"),
437 }
438 }
439}
440
441impl std::error::Error for StaticSolveError {
442 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
443 match self {
444 Self::EpochInput { source, .. } => Some(source),
445 Self::Singular(error) => Some(error),
446 _ => None,
447 }
448 }
449}
450
451pub fn solve_static(
457 eph: &dyn EphemerisSource,
458 epochs: &[StaticEpoch],
459 options: StaticSolveOptions,
460) -> Result<StaticSolution, StaticSolveError> {
461 let core = solve_static_core(eph, epochs, options)?;
462 let (per_epoch_influence, per_satellite_influence, per_satellite_batch_influence) =
463 build_influence(eph, epochs, options, &core);
464 Ok(core.into_public(
465 per_epoch_influence,
466 per_satellite_influence,
467 per_satellite_batch_influence,
468 ))
469}
470
471pub(crate) fn solve_static_without_influence(
472 eph: &dyn EphemerisSource,
473 epochs: &[StaticEpoch],
474 options: StaticSolveOptions,
475) -> Result<StaticSolution, StaticSolveError> {
476 let core = solve_static_core(eph, epochs, options)?;
477 Ok(core.into_public(Vec::new(), Vec::new(), Vec::new()))
478}
479
480#[derive(Debug, Clone)]
481struct PreparedEpoch {
482 input_index: usize,
483 inputs: SolveInputs,
484 used: Vec<GnssSatelliteId>,
485 rejected: Vec<RejectedSat>,
486 systems: Vec<GnssSystem>,
487 clock_offset: usize,
488 obs_by_id: Vec<(GnssSatelliteId, f64)>,
489}
490
491#[derive(Debug, Clone, Copy)]
492struct RowRef {
493 epoch_index: usize,
494 satellite_id: GnssSatelliteId,
495 base_weight: f64,
496}
497
498#[derive(Debug, Clone)]
499struct PreparedStatic {
500 epochs: Vec<PreparedEpoch>,
501 rows: Vec<RowRef>,
502 base_weights: Vec<f64>,
503 x0: DVector<f64>,
504 n_params: usize,
505}
506
507#[derive(Debug, Clone)]
508struct CoreStaticSolution {
509 position: ItrfPositionM,
510 geodetic: Option<Wgs84Geodetic>,
511 per_epoch_clock: Vec<StaticClockBias>,
512 covariance: StaticCovariance,
513 geometry_quality: GeometryQuality,
514 residuals_m: Vec<StaticResidual>,
515 used_sats: Vec<Vec<GnssSatelliteId>>,
516 rejected_sats: Vec<Vec<RejectedSat>>,
517 metadata: StaticSolutionMetadata,
518}
519
520impl CoreStaticSolution {
521 fn into_public(
522 self,
523 per_epoch_influence: Vec<StaticEpochInfluence>,
524 per_satellite_influence: Vec<StaticSatelliteInfluence>,
525 per_satellite_batch_influence: Vec<StaticSatelliteBatchInfluence>,
526 ) -> StaticSolution {
527 StaticSolution {
528 position: self.position,
529 geodetic: self.geodetic,
530 per_epoch_clock: self.per_epoch_clock,
531 covariance: self.covariance,
532 per_epoch_influence,
533 per_satellite_influence,
534 per_satellite_batch_influence,
535 geometry_quality: self.geometry_quality,
536 residuals_m: self.residuals_m,
537 used_sats: self.used_sats,
538 rejected_sats: self.rejected_sats,
539 metadata: self.metadata,
540 }
541 }
542}
543
544fn solve_static_core(
545 eph: &dyn EphemerisSource,
546 epochs: &[StaticEpoch],
547 options: StaticSolveOptions,
548) -> Result<CoreStaticSolution, StaticSolveError> {
549 validate_static_options(options)?;
550 if epochs.is_empty() {
551 return Err(StaticSolveError::EmptyEpochs);
552 }
553 let model = SppModelRecipe::reference();
554 let prepared = prepare_static(eph, epochs, options, model)?;
555
556 let lost = Cell::new(None::<(usize, GnssSatelliteId)>);
557 let residual = |x: &DVector<f64>| -> DVector<f64> {
558 match residual_static_unweighted(eph, &prepared, x.as_slice(), model) {
559 Ok(values) => DVector::from_vec(values),
560 Err((epoch_index, satellite)) => {
561 lost.set(Some((epoch_index, satellite)));
562 DVector::from_vec(vec![0.0; prepared.rows.len()])
563 }
564 }
565 };
566
567 let opts = SolveOptions {
568 gtol: STATIC_SOLVER_GTOL,
569 ftol: STATIC_SOLVER_FTOL,
570 xtol: STATIC_SOLVER_XTOL,
571 max_nfev: STATIC_SOLVER_MAX_NFEV,
572 };
573 let base_weights = DVector::from_row_slice(&prepared.base_weights);
574 let problem = LeastSquaresProblem::with_weights(&residual, prepared.x0.clone(), base_weights);
575 let report_result = solve_trf_with(&problem, &opts, TrustRegionSolve::NalgebraLu);
576 if let Some((epoch_index, satellite)) = lost.get() {
577 return Err(StaticSolveError::EphemerisLost {
578 epoch_index,
579 satellite,
580 });
581 }
582 let mut report = report_result.map_err(StaticSolveError::Singular)?;
583
584 let mut final_weights = prepared.base_weights.clone();
585 let mut outer_iterations = 0usize;
586 let mut final_robust_scale_m = None;
587
588 if let Some(robust) = options.robust {
589 for _ in 0..robust.max_outer.saturating_sub(1) {
590 let post = residual_static_unweighted(eph, &prepared, report.x.as_slice(), model)
591 .map_err(|(epoch_index, satellite)| StaticSolveError::EphemerisLost {
592 epoch_index,
593 satellite,
594 })?;
595 let scale = mad_scale(&post, robust.scale_floor_m).map_err(map_robust_error)?;
596 let effective: Vec<f64> = post
597 .iter()
598 .zip(prepared.base_weights.iter())
599 .map(|(&r, &base)| base * huber_weight(r / scale, robust.huber_k))
600 .collect();
601 let weights = DVector::from_row_slice(&effective);
602 let x_prev = report.x.clone();
603 let problem = LeastSquaresProblem::with_weights(&residual, x_prev.clone(), weights);
604 let next = solve_trf_with(&problem, &opts, TrustRegionSolve::NalgebraLu);
605 if let Some((epoch_index, satellite)) = lost.get() {
606 return Err(StaticSolveError::EphemerisLost {
607 epoch_index,
608 satellite,
609 });
610 }
611 report = next.map_err(StaticSolveError::Singular)?;
612 final_weights = effective;
613 outer_iterations += 1;
614 final_robust_scale_m = Some(scale);
615 let dpos = ((report.x[0] - x_prev[0]).powi(2)
616 + (report.x[1] - x_prev[1]).powi(2)
617 + (report.x[2] - x_prev[2]).powi(2))
618 .sqrt();
619 if dpos < robust.outer_tol_m {
620 break;
621 }
622 }
623 }
624
625 finish_static(FinishStaticInput {
626 eph,
627 prepared: &prepared,
628 options,
629 model,
630 x: report.x.as_slice(),
631 jacobian: &report.jacobian,
632 iterations: report.iterations,
633 status: report.status,
634 outer_iterations,
635 final_robust_scale_m,
636 final_weights: &final_weights,
637 })
638}
639
640fn prepare_static(
641 eph: &dyn EphemerisSource,
642 epochs: &[StaticEpoch],
643 options: StaticSolveOptions,
644 model: SppModelRecipe,
645) -> Result<PreparedStatic, StaticSolveError> {
646 let mut prepared_epochs = Vec::with_capacity(epochs.len());
647 let mut rows = Vec::new();
648 let mut base_weights = Vec::new();
649 let mut x0 = vec![
650 options.initial_position_m[0],
651 options.initial_position_m[1],
652 options.initial_position_m[2],
653 ];
654 let mut clock_offset = 3usize;
655
656 for (epoch_index, epoch) in epochs.iter().enumerate() {
657 validate_epoch_weights(epoch)?;
658 if let Some(satellite) = duplicate_satellite(&epoch.measurements) {
659 return Err(StaticSolveError::DuplicateObservation {
660 epoch_index,
661 satellite,
662 });
663 }
664 if epoch.corrections.ionosphere {
665 if let Some(satellite) = epoch
666 .measurements
667 .iter()
668 .map(|m| m.satellite_id)
669 .find(|sat| spp_iono_frequency_hz(*sat, &epoch.glonass_channels).is_none())
670 {
671 return Err(StaticSolveError::IonosphereUnsupported {
672 epoch_index,
673 satellite,
674 });
675 }
676 }
677
678 let inputs = solve_inputs_for_epoch(epoch, options);
679 validate_solve_inputs(&inputs).map_err(|source| StaticSolveError::EpochInput {
680 epoch_index,
681 source,
682 })?;
683 let selection = select_sats(eph, &inputs, model);
684 let systems = clock_systems(&selection.used);
685 let weight_by_sat = measurement_weight_map(epoch);
686 let obs_by_id: Vec<(GnssSatelliteId, f64)> = inputs
687 .observations
688 .iter()
689 .map(|m| (m.satellite_id, m.pseudorange_m))
690 .collect();
691
692 for (row_idx, &satellite_id) in selection.used.iter().enumerate() {
693 let multiplier = weight_by_sat.get(&satellite_id).copied().unwrap_or(1.0);
694 let base_weight = selection.weights[row_idx] * multiplier;
695 rows.push(RowRef {
696 epoch_index,
697 satellite_id,
698 base_weight,
699 });
700 base_weights.push(base_weight);
701 }
702
703 if !systems.is_empty() {
704 x0.push(epoch.clock_initial_m);
705 x0.extend(std::iter::repeat_n(0.0, systems.len().saturating_sub(1)));
706 }
707
708 prepared_epochs.push(PreparedEpoch {
709 input_index: epoch_index,
710 inputs,
711 used: selection.used,
712 rejected: selection.rejected,
713 systems,
714 clock_offset,
715 obs_by_id,
716 });
717 clock_offset += prepared_epochs
718 .last()
719 .expect("prepared epoch just pushed")
720 .systems
721 .len();
722 }
723
724 let n_params = x0.len();
725 if rows.len() < n_params {
726 return Err(StaticSolveError::TooFewMeasurements {
727 used: rows.len(),
728 required: n_params,
729 });
730 }
731
732 Ok(PreparedStatic {
733 epochs: prepared_epochs,
734 rows,
735 base_weights,
736 x0: DVector::from_vec(x0),
737 n_params,
738 })
739}
740
741fn solve_inputs_for_epoch(epoch: &StaticEpoch, options: StaticSolveOptions) -> SolveInputs {
742 SolveInputs {
743 observations: epoch.measurements.clone(),
744 t_rx_j2000_s: epoch.t_rx_j2000_s,
745 t_rx_second_of_day_s: epoch.t_rx_second_of_day_s,
746 day_of_year: epoch.day_of_year,
747 initial_guess: [
748 options.initial_position_m[0],
749 options.initial_position_m[1],
750 options.initial_position_m[2],
751 epoch.clock_initial_m,
752 ],
753 corrections: epoch.corrections,
754 klobuchar: epoch.klobuchar,
755 beidou_klobuchar: epoch.beidou_klobuchar,
756 galileo_nequick: epoch.galileo_nequick,
757 sbas_iono: epoch.sbas_iono.clone(),
758 glonass_channels: epoch.glonass_channels.clone(),
759 met: epoch.met,
760 robust: None,
761 }
762}
763
764struct FinishStaticInput<'a> {
765 eph: &'a dyn EphemerisSource,
766 prepared: &'a PreparedStatic,
767 options: StaticSolveOptions,
768 model: SppModelRecipe,
769 x: &'a [f64],
770 jacobian: &'a DMatrix<f64>,
771 iterations: usize,
772 status: Status,
773 outer_iterations: usize,
774 final_robust_scale_m: Option<f64>,
775 final_weights: &'a [f64],
776}
777
778fn finish_static(input: FinishStaticInput<'_>) -> Result<CoreStaticSolution, StaticSolveError> {
779 let FinishStaticInput {
780 eph,
781 prepared,
782 options,
783 model,
784 x,
785 jacobian,
786 iterations,
787 status,
788 outer_iterations,
789 final_robust_scale_m,
790 final_weights,
791 } = input;
792 let position = ItrfPositionM::new(x[0], x[1], x[2]).expect("valid static position");
793 let receiver_geodetic = geodetic_from_ecef(model.frame, [x[0], x[1], x[2]]);
794 let geodetic = if options.with_geodetic {
795 Some(receiver_geodetic)
796 } else {
797 None
798 };
799 let per_epoch_clock = epoch_clocks(prepared, x);
800 let residual_values = residual_static_unweighted(eph, prepared, x, model).map_err(
801 |(epoch_index, satellite)| StaticSolveError::EphemerisLost {
802 epoch_index,
803 satellite,
804 },
805 )?;
806 let residuals_m = residual_values
807 .iter()
808 .zip(prepared.rows.iter())
809 .zip(final_weights.iter())
810 .map(|((&residual_m, row), &effective_weight)| StaticResidual {
811 epoch_index: row.epoch_index,
812 satellite_id: row.satellite_id,
813 residual_m,
814 base_weight: row.base_weight,
815 effective_weight,
816 robust_weight_ratio: effective_weight / row.base_weight,
817 })
818 .collect::<Vec<_>>();
819
820 let covariance_matrix = normal_covariance(jacobian, 1.0).map_err(StaticSolveError::Singular)?;
821 let covariance = static_covariance(&covariance_matrix, receiver_geodetic)?;
822 let svd = jacobian.clone().svd(false, false);
823 let diagnostics = singular_value_diagnostics(
824 svd.singular_values.as_slice(),
825 jacobian.nrows(),
826 jacobian.ncols(),
827 );
828 if diagnostics.rank < prepared.n_params {
829 return Err(StaticSolveError::Singular(
830 least_squares::SolveError::SingularJacobian,
831 ));
832 }
833 let gdop = covariance_trace(&covariance_matrix).sqrt();
834 let redundancy = prepared.rows.len() as isize - prepared.n_params as isize;
835 let geometry_quality = classify(
836 diagnostics.rank,
837 prepared.n_params,
838 redundancy as i32,
839 diagnostics.condition_number,
840 gdop,
841 false,
842 GeometryQualityThresholds::default(),
843 );
844 let converged = matches!(
845 status,
846 Status::GradientTolerance | Status::CostTolerance | Status::StepTolerance
847 );
848
849 Ok(CoreStaticSolution {
850 position,
851 geodetic,
852 per_epoch_clock,
853 covariance,
854 geometry_quality,
855 residuals_m,
856 used_sats: prepared
857 .epochs
858 .iter()
859 .map(|epoch| epoch.used.clone())
860 .collect(),
861 rejected_sats: prepared
862 .epochs
863 .iter()
864 .map(|epoch| epoch.rejected.clone())
865 .collect(),
866 metadata: StaticSolutionMetadata {
867 iterations,
868 converged,
869 status,
870 outer_iterations,
871 final_robust_scale_m,
872 used_measurements: prepared.rows.len(),
873 n_parameters: prepared.n_params,
874 redundancy,
875 },
876 })
877}
878
879fn residual_static_unweighted(
880 eph: &dyn EphemerisSource,
881 prepared: &PreparedStatic,
882 x: &[f64],
883 model: SppModelRecipe,
884) -> Result<Vec<f64>, (usize, GnssSatelliteId)> {
885 let mut out = Vec::with_capacity(prepared.rows.len());
886 for epoch in &prepared.epochs {
887 if epoch.used.is_empty() {
888 continue;
889 }
890 let mut local = Vec::with_capacity(3 + epoch.systems.len());
891 local.extend_from_slice(&x[0..3]);
892 for clock_idx in 0..epoch.systems.len() {
893 local.push(x[epoch.clock_offset + clock_idx]);
894 }
895 let residuals = residual_unweighted(
896 eph,
897 &epoch.used,
898 &epoch.obs_by_id,
899 &local,
900 &epoch.inputs,
901 model,
902 )
903 .map_err(|satellite| (epoch.input_index, satellite))?;
904 out.extend(residuals);
905 }
906 Ok(out)
907}
908
909fn static_covariance(
910 covariance: &DMatrix<f64>,
911 receiver: Wgs84Geodetic,
912) -> Result<StaticCovariance, StaticSolveError> {
913 let state_m2 = matrix_to_rows(covariance);
914 let position_ecef_m2 = [
915 [covariance[(0, 0)], covariance[(0, 1)], covariance[(0, 2)]],
916 [covariance[(1, 0)], covariance[(1, 1)], covariance[(1, 2)]],
917 [covariance[(2, 0)], covariance[(2, 1)], covariance[(2, 2)]],
918 ];
919 let position_enu_m2 = rotate_covariance_ecef_to_enu_m2(position_ecef_m2, receiver)
920 .map_err(|_| StaticSolveError::Singular(least_squares::SolveError::SingularJacobian))?;
921 Ok(StaticCovariance {
922 state_m2,
923 position_ecef_m2,
924 position_enu_m2,
925 })
926}
927
928fn epoch_clocks(prepared: &PreparedStatic, x: &[f64]) -> Vec<StaticClockBias> {
929 let mut clocks = Vec::new();
930 for epoch in &prepared.epochs {
931 for (clock_idx, &system) in epoch.systems.iter().enumerate() {
932 clocks.push(StaticClockBias {
933 epoch_index: epoch.input_index,
934 system,
935 clock_s: x[epoch.clock_offset + clock_idx] / C_M_S,
936 });
937 }
938 }
939 clocks
940}
941
942fn validate_static_options(options: StaticSolveOptions) -> Result<(), StaticSolveError> {
943 validate::finite_slice(&options.initial_position_m, "initial_position_m")
944 .map_err(map_static_input_error)?;
945 if let Some(robust) = options.robust {
946 if robust.max_outer == 0 {
947 return Err(StaticSolveError::InvalidInput {
948 field: "robust.max_outer",
949 kind: SppInputErrorKind::NotPositive,
950 });
951 }
952 validate::finite_positive(robust.huber_k, "robust.huber_k")
953 .map_err(map_static_input_error)?;
954 validate::finite_positive(robust.scale_floor_m, "robust.scale_floor_m")
955 .map_err(map_static_input_error)?;
956 validate::finite_positive(robust.outer_tol_m, "robust.outer_tol_m")
957 .map_err(map_static_input_error)?;
958 }
959 Ok(())
960}
961
962fn validate_epoch_weights(epoch: &StaticEpoch) -> Result<(), StaticSolveError> {
963 if let Some(weights) = &epoch.weights {
964 if weights.len() != epoch.measurements.len() {
965 return Err(StaticSolveError::InvalidInput {
966 field: "epoch.weights",
967 kind: SppInputErrorKind::OutOfRange,
968 });
969 }
970 for &weight in weights {
971 validate::finite_positive(weight, "epoch.weights").map_err(map_static_input_error)?;
972 }
973 }
974 Ok(())
975}
976
977fn measurement_weight_map(epoch: &StaticEpoch) -> BTreeMap<GnssSatelliteId, f64> {
978 epoch
979 .weights
980 .as_ref()
981 .map(|weights| {
982 epoch
983 .measurements
984 .iter()
985 .zip(weights.iter())
986 .map(|(measurement, &weight)| (measurement.satellite_id, weight))
987 .collect()
988 })
989 .unwrap_or_default()
990}
991
992fn duplicate_satellite(measurements: &[Observation]) -> Option<GnssSatelliteId> {
993 let mut ids: Vec<GnssSatelliteId> = measurements.iter().map(|m| m.satellite_id).collect();
994 ids.sort_unstable();
995 ids.windows(2)
996 .find(|pair| pair[0] == pair[1])
997 .map(|pair| pair[0])
998}
999
1000fn build_influence(
1001 eph: &dyn EphemerisSource,
1002 epochs: &[StaticEpoch],
1003 options: StaticSolveOptions,
1004 full: &CoreStaticSolution,
1005) -> (
1006 Vec<StaticEpochInfluence>,
1007 Vec<StaticSatelliteInfluence>,
1008 Vec<StaticSatelliteBatchInfluence>,
1009) {
1010 let epoch_influence = (0..epochs.len())
1011 .map(|epoch_index| {
1012 let mut subset = epochs.to_vec();
1013 let omitted_measurements = subset[epoch_index].measurements.len();
1014 subset.remove(epoch_index);
1015 let result = solve_static_core(eph, &subset, options);
1016 let (status, position_delta_m, position_delta_norm_m, residual_rms_m) =
1017 influence_result(full.position.as_array(), result);
1018 StaticEpochInfluence {
1019 epoch_index,
1020 omitted_measurements,
1021 status,
1022 position_delta_m,
1023 position_delta_norm_m,
1024 residual_rms_m,
1025 min_robust_weight_ratio: min_epoch_weight_ratio(full, epoch_index),
1026 }
1027 })
1028 .collect();
1029
1030 let satellite_ids = full
1031 .residuals_m
1032 .iter()
1033 .map(|row| row.satellite_id)
1034 .collect::<BTreeSet<_>>();
1035 let satellite_batch_influence = satellite_ids
1036 .into_iter()
1037 .map(|satellite_id| {
1038 let subset = omit_satellite_all_epochs(epochs, satellite_id);
1039 let result = solve_static_core(eph, &subset, options);
1040 let (status, position_delta_m, position_delta_norm_m, residual_rms_m) =
1041 influence_result(full.position.as_array(), result);
1042 StaticSatelliteBatchInfluence {
1043 satellite_id,
1044 omitted_measurements: full
1045 .residuals_m
1046 .iter()
1047 .filter(|row| row.satellite_id == satellite_id)
1048 .count(),
1049 status,
1050 position_delta_m,
1051 position_delta_norm_m,
1052 residual_rms_m,
1053 min_robust_weight_ratio: min_satellite_weight_ratio(full, satellite_id),
1054 }
1055 })
1056 .collect();
1057
1058 let satellite_influence = full
1059 .residuals_m
1060 .iter()
1061 .map(|row| {
1062 let subset = omit_satellite(epochs, row.epoch_index, row.satellite_id);
1063 let result = solve_static_core(eph, &subset, options);
1064 let (status, position_delta_m, position_delta_norm_m, residual_rms_m) =
1065 influence_result(full.position.as_array(), result);
1066 StaticSatelliteInfluence {
1067 epoch_index: row.epoch_index,
1068 satellite_id: row.satellite_id,
1069 status,
1070 position_delta_m,
1071 position_delta_norm_m,
1072 residual_rms_m,
1073 residual_m: row.residual_m,
1074 base_weight: row.base_weight,
1075 effective_weight: row.effective_weight,
1076 robust_weight_ratio: row.robust_weight_ratio,
1077 }
1078 })
1079 .collect();
1080
1081 (
1082 epoch_influence,
1083 satellite_influence,
1084 satellite_batch_influence,
1085 )
1086}
1087
1088fn omit_satellite(
1089 epochs: &[StaticEpoch],
1090 epoch_index: usize,
1091 satellite_id: GnssSatelliteId,
1092) -> Vec<StaticEpoch> {
1093 let mut subset = epochs.to_vec();
1094 remove_satellite_from_epoch(&mut subset[epoch_index], satellite_id);
1095 subset
1096}
1097
1098fn omit_satellite_all_epochs(
1099 epochs: &[StaticEpoch],
1100 satellite_id: GnssSatelliteId,
1101) -> Vec<StaticEpoch> {
1102 let mut subset = epochs.to_vec();
1103 for epoch in &mut subset {
1104 remove_satellite_from_epoch(epoch, satellite_id);
1105 }
1106 subset
1107}
1108
1109fn remove_satellite_from_epoch(epoch: &mut StaticEpoch, satellite_id: GnssSatelliteId) {
1110 let old_measurements = std::mem::take(&mut epoch.measurements);
1111 let old_weights = epoch.weights.take();
1112 let mut measurements = Vec::with_capacity(old_measurements.len());
1113 let mut weights = old_weights
1114 .as_ref()
1115 .map(|_| Vec::with_capacity(old_measurements.len()));
1116 for (idx, measurement) in old_measurements.into_iter().enumerate() {
1117 if measurement.satellite_id == satellite_id {
1118 continue;
1119 }
1120 measurements.push(measurement);
1121 if let (Some(old), Some(new_weights)) = (&old_weights, &mut weights) {
1122 new_weights.push(old[idx]);
1123 }
1124 }
1125 epoch.measurements = measurements;
1126 epoch.weights = weights;
1127}
1128
1129fn influence_result(
1130 full_position: [f64; 3],
1131 result: Result<CoreStaticSolution, StaticSolveError>,
1132) -> (
1133 StaticInfluenceStatus,
1134 Option<[f64; 3]>,
1135 Option<f64>,
1136 Option<f64>,
1137) {
1138 match result {
1139 Ok(solution) => {
1140 let position = solution.position.as_array();
1141 let delta = [
1142 position[0] - full_position[0],
1143 position[1] - full_position[1],
1144 position[2] - full_position[2],
1145 ];
1146 let norm = (delta[0] * delta[0] + delta[1] * delta[1] + delta[2] * delta[2]).sqrt();
1147 let residual_values = solution
1148 .residuals_m
1149 .iter()
1150 .map(|row| row.residual_m)
1151 .collect::<Vec<_>>();
1152 (
1153 StaticInfluenceStatus::Solved,
1154 Some(delta),
1155 Some(norm),
1156 Some(residual_rms(&residual_values)),
1157 )
1158 }
1159 Err(error) => (influence_status(&error), None, None, None),
1160 }
1161}
1162
1163fn influence_status(error: &StaticSolveError) -> StaticInfluenceStatus {
1164 match error {
1165 StaticSolveError::TooFewMeasurements { .. } | StaticSolveError::EmptyEpochs => {
1166 StaticInfluenceStatus::TooFewMeasurements
1167 }
1168 StaticSolveError::Singular(_) => StaticInfluenceStatus::SingularGeometry,
1169 StaticSolveError::InvalidInput { .. }
1170 | StaticSolveError::EpochInput { .. }
1171 | StaticSolveError::DuplicateObservation { .. }
1172 | StaticSolveError::IonosphereUnsupported { .. } => StaticInfluenceStatus::InvalidInput,
1173 StaticSolveError::EphemerisLost { .. } => StaticInfluenceStatus::EphemerisUnavailable,
1174 }
1175}
1176
1177fn min_epoch_weight_ratio(full: &CoreStaticSolution, epoch_index: usize) -> f64 {
1178 full.residuals_m
1179 .iter()
1180 .filter(|row| row.epoch_index == epoch_index)
1181 .map(|row| row.robust_weight_ratio)
1182 .fold(1.0, f64::min)
1183}
1184
1185fn min_satellite_weight_ratio(full: &CoreStaticSolution, satellite_id: GnssSatelliteId) -> f64 {
1186 full.residuals_m
1187 .iter()
1188 .filter(|row| row.satellite_id == satellite_id)
1189 .map(|row| row.robust_weight_ratio)
1190 .fold(1.0, f64::min)
1191}
1192
1193fn matrix_to_rows(matrix: &DMatrix<f64>) -> Vec<Vec<f64>> {
1194 (0..matrix.nrows())
1195 .map(|row| (0..matrix.ncols()).map(|col| matrix[(row, col)]).collect())
1196 .collect()
1197}
1198
1199fn covariance_trace(matrix: &DMatrix<f64>) -> f64 {
1200 (0..matrix.nrows().min(matrix.ncols()))
1201 .map(|idx| matrix[(idx, idx)])
1202 .sum()
1203}
1204
1205fn residual_rms(residuals: &[f64]) -> f64 {
1206 if residuals.is_empty() {
1207 return 0.0;
1208 }
1209 let sum_sq = residuals.iter().map(|r| r * r).sum::<f64>();
1210 (sum_sq / residuals.len() as f64).sqrt()
1211}
1212
1213fn map_static_input_error(error: validate::FieldError) -> StaticSolveError {
1214 StaticSolveError::InvalidInput {
1215 field: error.field(),
1216 kind: SppInputErrorKind::from(&error),
1217 }
1218}
1219
1220fn map_robust_error(error: RobustError) -> StaticSolveError {
1221 let field = match error.field() {
1222 "scale_floor" => "robust.scale_floor_m",
1223 "residuals" | "values" => "robust.residuals",
1224 other => other,
1225 };
1226 let kind = match error.reason() {
1227 "not finite" => SppInputErrorKind::NonFinite,
1228 "not positive" => SppInputErrorKind::NotPositive,
1229 "negative" => SppInputErrorKind::Negative,
1230 "out of range" => SppInputErrorKind::OutOfRange,
1231 _ => SppInputErrorKind::OutOfRange,
1232 };
1233 StaticSolveError::InvalidInput { field, kind }
1234}
1235
1236#[cfg(test)]
1237mod tests;