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
448#[derive(Debug, Clone)]
449struct PreparedEpoch {
450 input_index: usize,
451 inputs: SolveInputs,
452 used: Vec<GnssSatelliteId>,
453 rejected: Vec<RejectedSat>,
454 systems: Vec<GnssSystem>,
455 clock_offset: usize,
456 obs_by_id: Vec<(GnssSatelliteId, f64)>,
457}
458
459#[derive(Debug, Clone, Copy)]
460struct RowRef {
461 epoch_index: usize,
462 satellite_id: GnssSatelliteId,
463 base_weight: f64,
464}
465
466#[derive(Debug, Clone)]
467struct PreparedStatic {
468 epochs: Vec<PreparedEpoch>,
469 rows: Vec<RowRef>,
470 base_weights: Vec<f64>,
471 x0: DVector<f64>,
472 n_params: usize,
473}
474
475#[derive(Debug, Clone)]
476struct CoreStaticSolution {
477 position: ItrfPositionM,
478 geodetic: Option<Wgs84Geodetic>,
479 per_epoch_clock: Vec<StaticClockBias>,
480 covariance: StaticCovariance,
481 geometry_quality: GeometryQuality,
482 residuals_m: Vec<StaticResidual>,
483 used_sats: Vec<Vec<GnssSatelliteId>>,
484 rejected_sats: Vec<Vec<RejectedSat>>,
485 metadata: StaticSolutionMetadata,
486}
487
488impl CoreStaticSolution {
489 fn into_public(
490 self,
491 per_epoch_influence: Vec<StaticEpochInfluence>,
492 per_satellite_influence: Vec<StaticSatelliteInfluence>,
493 per_satellite_batch_influence: Vec<StaticSatelliteBatchInfluence>,
494 ) -> StaticSolution {
495 StaticSolution {
496 position: self.position,
497 geodetic: self.geodetic,
498 per_epoch_clock: self.per_epoch_clock,
499 covariance: self.covariance,
500 per_epoch_influence,
501 per_satellite_influence,
502 per_satellite_batch_influence,
503 geometry_quality: self.geometry_quality,
504 residuals_m: self.residuals_m,
505 used_sats: self.used_sats,
506 rejected_sats: self.rejected_sats,
507 metadata: self.metadata,
508 }
509 }
510}
511
512fn solve_static_core(
513 eph: &dyn EphemerisSource,
514 epochs: &[StaticEpoch],
515 options: StaticSolveOptions,
516) -> Result<CoreStaticSolution, StaticSolveError> {
517 validate_static_options(options)?;
518 if epochs.is_empty() {
519 return Err(StaticSolveError::EmptyEpochs);
520 }
521 let model = SppModelRecipe::reference();
522 let prepared = prepare_static(eph, epochs, options, model)?;
523
524 let lost = Cell::new(None::<(usize, GnssSatelliteId)>);
525 let residual = |x: &DVector<f64>| -> DVector<f64> {
526 match residual_static_unweighted(eph, &prepared, x.as_slice(), model) {
527 Ok(values) => DVector::from_vec(values),
528 Err((epoch_index, satellite)) => {
529 lost.set(Some((epoch_index, satellite)));
530 DVector::from_vec(vec![0.0; prepared.rows.len()])
531 }
532 }
533 };
534
535 let opts = SolveOptions {
536 gtol: STATIC_SOLVER_GTOL,
537 ftol: STATIC_SOLVER_FTOL,
538 xtol: STATIC_SOLVER_XTOL,
539 max_nfev: STATIC_SOLVER_MAX_NFEV,
540 };
541 let base_weights = DVector::from_row_slice(&prepared.base_weights);
542 let problem = LeastSquaresProblem::with_weights(&residual, prepared.x0.clone(), base_weights);
543 let report_result = solve_trf_with(&problem, &opts, TrustRegionSolve::NalgebraLu);
544 if let Some((epoch_index, satellite)) = lost.get() {
545 return Err(StaticSolveError::EphemerisLost {
546 epoch_index,
547 satellite,
548 });
549 }
550 let mut report = report_result.map_err(StaticSolveError::Singular)?;
551
552 let mut final_weights = prepared.base_weights.clone();
553 let mut outer_iterations = 0usize;
554 let mut final_robust_scale_m = None;
555
556 if let Some(robust) = options.robust {
557 for _ in 0..robust.max_outer.saturating_sub(1) {
558 let post = residual_static_unweighted(eph, &prepared, report.x.as_slice(), model)
559 .map_err(|(epoch_index, satellite)| StaticSolveError::EphemerisLost {
560 epoch_index,
561 satellite,
562 })?;
563 let scale = mad_scale(&post, robust.scale_floor_m).map_err(map_robust_error)?;
564 let effective: Vec<f64> = post
565 .iter()
566 .zip(prepared.base_weights.iter())
567 .map(|(&r, &base)| base * huber_weight(r / scale, robust.huber_k))
568 .collect();
569 let weights = DVector::from_row_slice(&effective);
570 let x_prev = report.x.clone();
571 let problem = LeastSquaresProblem::with_weights(&residual, x_prev.clone(), weights);
572 let next = solve_trf_with(&problem, &opts, TrustRegionSolve::NalgebraLu);
573 if let Some((epoch_index, satellite)) = lost.get() {
574 return Err(StaticSolveError::EphemerisLost {
575 epoch_index,
576 satellite,
577 });
578 }
579 report = next.map_err(StaticSolveError::Singular)?;
580 final_weights = effective;
581 outer_iterations += 1;
582 final_robust_scale_m = Some(scale);
583 let dpos = ((report.x[0] - x_prev[0]).powi(2)
584 + (report.x[1] - x_prev[1]).powi(2)
585 + (report.x[2] - x_prev[2]).powi(2))
586 .sqrt();
587 if dpos < robust.outer_tol_m {
588 break;
589 }
590 }
591 }
592
593 finish_static(FinishStaticInput {
594 eph,
595 prepared: &prepared,
596 options,
597 model,
598 x: report.x.as_slice(),
599 jacobian: &report.jacobian,
600 iterations: report.iterations,
601 status: report.status,
602 outer_iterations,
603 final_robust_scale_m,
604 final_weights: &final_weights,
605 })
606}
607
608fn prepare_static(
609 eph: &dyn EphemerisSource,
610 epochs: &[StaticEpoch],
611 options: StaticSolveOptions,
612 model: SppModelRecipe,
613) -> Result<PreparedStatic, StaticSolveError> {
614 let mut prepared_epochs = Vec::with_capacity(epochs.len());
615 let mut rows = Vec::new();
616 let mut base_weights = Vec::new();
617 let mut x0 = vec![
618 options.initial_position_m[0],
619 options.initial_position_m[1],
620 options.initial_position_m[2],
621 ];
622 let mut clock_offset = 3usize;
623
624 for (epoch_index, epoch) in epochs.iter().enumerate() {
625 validate_epoch_weights(epoch)?;
626 if let Some(satellite) = duplicate_satellite(&epoch.measurements) {
627 return Err(StaticSolveError::DuplicateObservation {
628 epoch_index,
629 satellite,
630 });
631 }
632 if epoch.corrections.ionosphere {
633 if let Some(satellite) = epoch
634 .measurements
635 .iter()
636 .map(|m| m.satellite_id)
637 .find(|sat| spp_iono_frequency_hz(*sat, &epoch.glonass_channels).is_none())
638 {
639 return Err(StaticSolveError::IonosphereUnsupported {
640 epoch_index,
641 satellite,
642 });
643 }
644 }
645
646 let inputs = solve_inputs_for_epoch(epoch, options);
647 validate_solve_inputs(&inputs).map_err(|source| StaticSolveError::EpochInput {
648 epoch_index,
649 source,
650 })?;
651 let selection = select_sats(eph, &inputs, model);
652 let systems = clock_systems(&selection.used);
653 let weight_by_sat = measurement_weight_map(epoch);
654 let obs_by_id: Vec<(GnssSatelliteId, f64)> = inputs
655 .observations
656 .iter()
657 .map(|m| (m.satellite_id, m.pseudorange_m))
658 .collect();
659
660 for (row_idx, &satellite_id) in selection.used.iter().enumerate() {
661 let multiplier = weight_by_sat.get(&satellite_id).copied().unwrap_or(1.0);
662 let base_weight = selection.weights[row_idx] * multiplier;
663 rows.push(RowRef {
664 epoch_index,
665 satellite_id,
666 base_weight,
667 });
668 base_weights.push(base_weight);
669 }
670
671 if !systems.is_empty() {
672 x0.push(epoch.clock_initial_m);
673 x0.extend(std::iter::repeat_n(0.0, systems.len().saturating_sub(1)));
674 }
675
676 prepared_epochs.push(PreparedEpoch {
677 input_index: epoch_index,
678 inputs,
679 used: selection.used,
680 rejected: selection.rejected,
681 systems,
682 clock_offset,
683 obs_by_id,
684 });
685 clock_offset += prepared_epochs
686 .last()
687 .expect("prepared epoch just pushed")
688 .systems
689 .len();
690 }
691
692 let n_params = x0.len();
693 if rows.len() < n_params {
694 return Err(StaticSolveError::TooFewMeasurements {
695 used: rows.len(),
696 required: n_params,
697 });
698 }
699
700 Ok(PreparedStatic {
701 epochs: prepared_epochs,
702 rows,
703 base_weights,
704 x0: DVector::from_vec(x0),
705 n_params,
706 })
707}
708
709fn solve_inputs_for_epoch(epoch: &StaticEpoch, options: StaticSolveOptions) -> SolveInputs {
710 SolveInputs {
711 observations: epoch.measurements.clone(),
712 t_rx_j2000_s: epoch.t_rx_j2000_s,
713 t_rx_second_of_day_s: epoch.t_rx_second_of_day_s,
714 day_of_year: epoch.day_of_year,
715 initial_guess: [
716 options.initial_position_m[0],
717 options.initial_position_m[1],
718 options.initial_position_m[2],
719 epoch.clock_initial_m,
720 ],
721 corrections: epoch.corrections,
722 klobuchar: epoch.klobuchar,
723 beidou_klobuchar: epoch.beidou_klobuchar,
724 galileo_nequick: epoch.galileo_nequick,
725 sbas_iono: epoch.sbas_iono.clone(),
726 glonass_channels: epoch.glonass_channels.clone(),
727 met: epoch.met,
728 robust: None,
729 }
730}
731
732struct FinishStaticInput<'a> {
733 eph: &'a dyn EphemerisSource,
734 prepared: &'a PreparedStatic,
735 options: StaticSolveOptions,
736 model: SppModelRecipe,
737 x: &'a [f64],
738 jacobian: &'a DMatrix<f64>,
739 iterations: usize,
740 status: Status,
741 outer_iterations: usize,
742 final_robust_scale_m: Option<f64>,
743 final_weights: &'a [f64],
744}
745
746fn finish_static(input: FinishStaticInput<'_>) -> Result<CoreStaticSolution, StaticSolveError> {
747 let FinishStaticInput {
748 eph,
749 prepared,
750 options,
751 model,
752 x,
753 jacobian,
754 iterations,
755 status,
756 outer_iterations,
757 final_robust_scale_m,
758 final_weights,
759 } = input;
760 let position = ItrfPositionM::new(x[0], x[1], x[2]).expect("valid static position");
761 let receiver_geodetic = geodetic_from_ecef(model.frame, [x[0], x[1], x[2]]);
762 let geodetic = if options.with_geodetic {
763 Some(receiver_geodetic)
764 } else {
765 None
766 };
767 let per_epoch_clock = epoch_clocks(prepared, x);
768 let residual_values = residual_static_unweighted(eph, prepared, x, model).map_err(
769 |(epoch_index, satellite)| StaticSolveError::EphemerisLost {
770 epoch_index,
771 satellite,
772 },
773 )?;
774 let residuals_m = residual_values
775 .iter()
776 .zip(prepared.rows.iter())
777 .zip(final_weights.iter())
778 .map(|((&residual_m, row), &effective_weight)| StaticResidual {
779 epoch_index: row.epoch_index,
780 satellite_id: row.satellite_id,
781 residual_m,
782 base_weight: row.base_weight,
783 effective_weight,
784 robust_weight_ratio: effective_weight / row.base_weight,
785 })
786 .collect::<Vec<_>>();
787
788 let covariance_matrix = normal_covariance(jacobian, 1.0).map_err(StaticSolveError::Singular)?;
789 let covariance = static_covariance(&covariance_matrix, receiver_geodetic)?;
790 let svd = jacobian.clone().svd(false, false);
791 let diagnostics = singular_value_diagnostics(
792 svd.singular_values.as_slice(),
793 jacobian.nrows(),
794 jacobian.ncols(),
795 );
796 if diagnostics.rank < prepared.n_params {
797 return Err(StaticSolveError::Singular(
798 least_squares::SolveError::SingularJacobian,
799 ));
800 }
801 let gdop = covariance_trace(&covariance_matrix).sqrt();
802 let redundancy = prepared.rows.len() as isize - prepared.n_params as isize;
803 let geometry_quality = classify(
804 diagnostics.rank,
805 prepared.n_params,
806 redundancy as i32,
807 diagnostics.condition_number,
808 gdop,
809 false,
810 GeometryQualityThresholds::default(),
811 );
812 let converged = matches!(
813 status,
814 Status::GradientTolerance | Status::CostTolerance | Status::StepTolerance
815 );
816
817 Ok(CoreStaticSolution {
818 position,
819 geodetic,
820 per_epoch_clock,
821 covariance,
822 geometry_quality,
823 residuals_m,
824 used_sats: prepared
825 .epochs
826 .iter()
827 .map(|epoch| epoch.used.clone())
828 .collect(),
829 rejected_sats: prepared
830 .epochs
831 .iter()
832 .map(|epoch| epoch.rejected.clone())
833 .collect(),
834 metadata: StaticSolutionMetadata {
835 iterations,
836 converged,
837 status,
838 outer_iterations,
839 final_robust_scale_m,
840 used_measurements: prepared.rows.len(),
841 n_parameters: prepared.n_params,
842 redundancy,
843 },
844 })
845}
846
847fn residual_static_unweighted(
848 eph: &dyn EphemerisSource,
849 prepared: &PreparedStatic,
850 x: &[f64],
851 model: SppModelRecipe,
852) -> Result<Vec<f64>, (usize, GnssSatelliteId)> {
853 let mut out = Vec::with_capacity(prepared.rows.len());
854 for epoch in &prepared.epochs {
855 if epoch.used.is_empty() {
856 continue;
857 }
858 let mut local = Vec::with_capacity(3 + epoch.systems.len());
859 local.extend_from_slice(&x[0..3]);
860 for clock_idx in 0..epoch.systems.len() {
861 local.push(x[epoch.clock_offset + clock_idx]);
862 }
863 let residuals = residual_unweighted(
864 eph,
865 &epoch.used,
866 &epoch.obs_by_id,
867 &local,
868 &epoch.inputs,
869 model,
870 )
871 .map_err(|satellite| (epoch.input_index, satellite))?;
872 out.extend(residuals);
873 }
874 Ok(out)
875}
876
877fn static_covariance(
878 covariance: &DMatrix<f64>,
879 receiver: Wgs84Geodetic,
880) -> Result<StaticCovariance, StaticSolveError> {
881 let state_m2 = matrix_to_rows(covariance);
882 let position_ecef_m2 = [
883 [covariance[(0, 0)], covariance[(0, 1)], covariance[(0, 2)]],
884 [covariance[(1, 0)], covariance[(1, 1)], covariance[(1, 2)]],
885 [covariance[(2, 0)], covariance[(2, 1)], covariance[(2, 2)]],
886 ];
887 let position_enu_m2 = rotate_covariance_ecef_to_enu_m2(position_ecef_m2, receiver)
888 .map_err(|_| StaticSolveError::Singular(least_squares::SolveError::SingularJacobian))?;
889 Ok(StaticCovariance {
890 state_m2,
891 position_ecef_m2,
892 position_enu_m2,
893 })
894}
895
896fn epoch_clocks(prepared: &PreparedStatic, x: &[f64]) -> Vec<StaticClockBias> {
897 let mut clocks = Vec::new();
898 for epoch in &prepared.epochs {
899 for (clock_idx, &system) in epoch.systems.iter().enumerate() {
900 clocks.push(StaticClockBias {
901 epoch_index: epoch.input_index,
902 system,
903 clock_s: x[epoch.clock_offset + clock_idx] / C_M_S,
904 });
905 }
906 }
907 clocks
908}
909
910fn validate_static_options(options: StaticSolveOptions) -> Result<(), StaticSolveError> {
911 validate::finite_slice(&options.initial_position_m, "initial_position_m")
912 .map_err(map_static_input_error)?;
913 if let Some(robust) = options.robust {
914 if robust.max_outer == 0 {
915 return Err(StaticSolveError::InvalidInput {
916 field: "robust.max_outer",
917 kind: SppInputErrorKind::NotPositive,
918 });
919 }
920 validate::finite_positive(robust.huber_k, "robust.huber_k")
921 .map_err(map_static_input_error)?;
922 validate::finite_positive(robust.scale_floor_m, "robust.scale_floor_m")
923 .map_err(map_static_input_error)?;
924 validate::finite_positive(robust.outer_tol_m, "robust.outer_tol_m")
925 .map_err(map_static_input_error)?;
926 }
927 Ok(())
928}
929
930fn validate_epoch_weights(epoch: &StaticEpoch) -> Result<(), StaticSolveError> {
931 if let Some(weights) = &epoch.weights {
932 if weights.len() != epoch.measurements.len() {
933 return Err(StaticSolveError::InvalidInput {
934 field: "epoch.weights",
935 kind: SppInputErrorKind::OutOfRange,
936 });
937 }
938 for &weight in weights {
939 validate::finite_positive(weight, "epoch.weights").map_err(map_static_input_error)?;
940 }
941 }
942 Ok(())
943}
944
945fn measurement_weight_map(epoch: &StaticEpoch) -> BTreeMap<GnssSatelliteId, f64> {
946 epoch
947 .weights
948 .as_ref()
949 .map(|weights| {
950 epoch
951 .measurements
952 .iter()
953 .zip(weights.iter())
954 .map(|(measurement, &weight)| (measurement.satellite_id, weight))
955 .collect()
956 })
957 .unwrap_or_default()
958}
959
960fn duplicate_satellite(measurements: &[Observation]) -> Option<GnssSatelliteId> {
961 let mut ids: Vec<GnssSatelliteId> = measurements.iter().map(|m| m.satellite_id).collect();
962 ids.sort_unstable();
963 ids.windows(2)
964 .find(|pair| pair[0] == pair[1])
965 .map(|pair| pair[0])
966}
967
968fn build_influence(
969 eph: &dyn EphemerisSource,
970 epochs: &[StaticEpoch],
971 options: StaticSolveOptions,
972 full: &CoreStaticSolution,
973) -> (
974 Vec<StaticEpochInfluence>,
975 Vec<StaticSatelliteInfluence>,
976 Vec<StaticSatelliteBatchInfluence>,
977) {
978 let epoch_influence = (0..epochs.len())
979 .map(|epoch_index| {
980 let mut subset = epochs.to_vec();
981 let omitted_measurements = subset[epoch_index].measurements.len();
982 subset.remove(epoch_index);
983 let result = solve_static_core(eph, &subset, options);
984 let (status, position_delta_m, position_delta_norm_m, residual_rms_m) =
985 influence_result(full.position.as_array(), result);
986 StaticEpochInfluence {
987 epoch_index,
988 omitted_measurements,
989 status,
990 position_delta_m,
991 position_delta_norm_m,
992 residual_rms_m,
993 min_robust_weight_ratio: min_epoch_weight_ratio(full, epoch_index),
994 }
995 })
996 .collect();
997
998 let satellite_ids = full
999 .residuals_m
1000 .iter()
1001 .map(|row| row.satellite_id)
1002 .collect::<BTreeSet<_>>();
1003 let satellite_batch_influence = satellite_ids
1004 .into_iter()
1005 .map(|satellite_id| {
1006 let subset = omit_satellite_all_epochs(epochs, satellite_id);
1007 let result = solve_static_core(eph, &subset, options);
1008 let (status, position_delta_m, position_delta_norm_m, residual_rms_m) =
1009 influence_result(full.position.as_array(), result);
1010 StaticSatelliteBatchInfluence {
1011 satellite_id,
1012 omitted_measurements: full
1013 .residuals_m
1014 .iter()
1015 .filter(|row| row.satellite_id == satellite_id)
1016 .count(),
1017 status,
1018 position_delta_m,
1019 position_delta_norm_m,
1020 residual_rms_m,
1021 min_robust_weight_ratio: min_satellite_weight_ratio(full, satellite_id),
1022 }
1023 })
1024 .collect();
1025
1026 let satellite_influence = full
1027 .residuals_m
1028 .iter()
1029 .map(|row| {
1030 let subset = omit_satellite(epochs, row.epoch_index, row.satellite_id);
1031 let result = solve_static_core(eph, &subset, options);
1032 let (status, position_delta_m, position_delta_norm_m, residual_rms_m) =
1033 influence_result(full.position.as_array(), result);
1034 StaticSatelliteInfluence {
1035 epoch_index: row.epoch_index,
1036 satellite_id: row.satellite_id,
1037 status,
1038 position_delta_m,
1039 position_delta_norm_m,
1040 residual_rms_m,
1041 residual_m: row.residual_m,
1042 base_weight: row.base_weight,
1043 effective_weight: row.effective_weight,
1044 robust_weight_ratio: row.robust_weight_ratio,
1045 }
1046 })
1047 .collect();
1048
1049 (
1050 epoch_influence,
1051 satellite_influence,
1052 satellite_batch_influence,
1053 )
1054}
1055
1056fn omit_satellite(
1057 epochs: &[StaticEpoch],
1058 epoch_index: usize,
1059 satellite_id: GnssSatelliteId,
1060) -> Vec<StaticEpoch> {
1061 let mut subset = epochs.to_vec();
1062 remove_satellite_from_epoch(&mut subset[epoch_index], satellite_id);
1063 subset
1064}
1065
1066fn omit_satellite_all_epochs(
1067 epochs: &[StaticEpoch],
1068 satellite_id: GnssSatelliteId,
1069) -> Vec<StaticEpoch> {
1070 let mut subset = epochs.to_vec();
1071 for epoch in &mut subset {
1072 remove_satellite_from_epoch(epoch, satellite_id);
1073 }
1074 subset
1075}
1076
1077fn remove_satellite_from_epoch(epoch: &mut StaticEpoch, satellite_id: GnssSatelliteId) {
1078 let old_measurements = std::mem::take(&mut epoch.measurements);
1079 let old_weights = epoch.weights.take();
1080 let mut measurements = Vec::with_capacity(old_measurements.len());
1081 let mut weights = old_weights
1082 .as_ref()
1083 .map(|_| Vec::with_capacity(old_measurements.len()));
1084 for (idx, measurement) in old_measurements.into_iter().enumerate() {
1085 if measurement.satellite_id == satellite_id {
1086 continue;
1087 }
1088 measurements.push(measurement);
1089 if let (Some(old), Some(new_weights)) = (&old_weights, &mut weights) {
1090 new_weights.push(old[idx]);
1091 }
1092 }
1093 epoch.measurements = measurements;
1094 epoch.weights = weights;
1095}
1096
1097fn influence_result(
1098 full_position: [f64; 3],
1099 result: Result<CoreStaticSolution, StaticSolveError>,
1100) -> (
1101 StaticInfluenceStatus,
1102 Option<[f64; 3]>,
1103 Option<f64>,
1104 Option<f64>,
1105) {
1106 match result {
1107 Ok(solution) => {
1108 let position = solution.position.as_array();
1109 let delta = [
1110 position[0] - full_position[0],
1111 position[1] - full_position[1],
1112 position[2] - full_position[2],
1113 ];
1114 let norm = (delta[0] * delta[0] + delta[1] * delta[1] + delta[2] * delta[2]).sqrt();
1115 let residual_values = solution
1116 .residuals_m
1117 .iter()
1118 .map(|row| row.residual_m)
1119 .collect::<Vec<_>>();
1120 (
1121 StaticInfluenceStatus::Solved,
1122 Some(delta),
1123 Some(norm),
1124 Some(residual_rms(&residual_values)),
1125 )
1126 }
1127 Err(error) => (influence_status(&error), None, None, None),
1128 }
1129}
1130
1131fn influence_status(error: &StaticSolveError) -> StaticInfluenceStatus {
1132 match error {
1133 StaticSolveError::TooFewMeasurements { .. } | StaticSolveError::EmptyEpochs => {
1134 StaticInfluenceStatus::TooFewMeasurements
1135 }
1136 StaticSolveError::Singular(_) => StaticInfluenceStatus::SingularGeometry,
1137 StaticSolveError::InvalidInput { .. }
1138 | StaticSolveError::EpochInput { .. }
1139 | StaticSolveError::DuplicateObservation { .. }
1140 | StaticSolveError::IonosphereUnsupported { .. } => StaticInfluenceStatus::InvalidInput,
1141 StaticSolveError::EphemerisLost { .. } => StaticInfluenceStatus::EphemerisUnavailable,
1142 }
1143}
1144
1145fn min_epoch_weight_ratio(full: &CoreStaticSolution, epoch_index: usize) -> f64 {
1146 full.residuals_m
1147 .iter()
1148 .filter(|row| row.epoch_index == epoch_index)
1149 .map(|row| row.robust_weight_ratio)
1150 .fold(1.0, f64::min)
1151}
1152
1153fn min_satellite_weight_ratio(full: &CoreStaticSolution, satellite_id: GnssSatelliteId) -> f64 {
1154 full.residuals_m
1155 .iter()
1156 .filter(|row| row.satellite_id == satellite_id)
1157 .map(|row| row.robust_weight_ratio)
1158 .fold(1.0, f64::min)
1159}
1160
1161fn matrix_to_rows(matrix: &DMatrix<f64>) -> Vec<Vec<f64>> {
1162 (0..matrix.nrows())
1163 .map(|row| (0..matrix.ncols()).map(|col| matrix[(row, col)]).collect())
1164 .collect()
1165}
1166
1167fn covariance_trace(matrix: &DMatrix<f64>) -> f64 {
1168 (0..matrix.nrows().min(matrix.ncols()))
1169 .map(|idx| matrix[(idx, idx)])
1170 .sum()
1171}
1172
1173fn residual_rms(residuals: &[f64]) -> f64 {
1174 if residuals.is_empty() {
1175 return 0.0;
1176 }
1177 let sum_sq = residuals.iter().map(|r| r * r).sum::<f64>();
1178 (sum_sq / residuals.len() as f64).sqrt()
1179}
1180
1181fn map_static_input_error(error: validate::FieldError) -> StaticSolveError {
1182 StaticSolveError::InvalidInput {
1183 field: error.field(),
1184 kind: SppInputErrorKind::from(&error),
1185 }
1186}
1187
1188fn map_robust_error(error: RobustError) -> StaticSolveError {
1189 let field = match error.field() {
1190 "scale_floor" => "robust.scale_floor_m",
1191 "residuals" | "values" => "robust.residuals",
1192 other => other,
1193 };
1194 let kind = match error.reason() {
1195 "not finite" => SppInputErrorKind::NonFinite,
1196 "not positive" => SppInputErrorKind::NotPositive,
1197 "negative" => SppInputErrorKind::Negative,
1198 "out of range" => SppInputErrorKind::OutOfRange,
1199 _ => SppInputErrorKind::OutOfRange,
1200 };
1201 StaticSolveError::InvalidInput { field, kind }
1202}
1203
1204#[cfg(test)]
1205mod tests;