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