1use std::collections::{BTreeMap, BTreeSet};
11
12use crate::ambiguity::AmbiguityId;
13use crate::astro::math::vec3;
14use crate::estimation::recipe::{EstimationRecipe, NormalRecipe, ResidualNormRecipe};
15use crate::estimation::substrate::parameters::ParameterLayout;
16use crate::estimation::substrate::qc::normalized_residual;
17use crate::observables::ObservableEphemerisSource;
18
19use super::normal::{ppp_position_covariance, solve_normal_equations, PppNormalLayout};
20use super::rows::{build_rows, residual_rows, AmbiguityBinding, PppRowError};
21use super::temporal::{estimate_temporal_correlation, temporal_position_covariance};
22use super::{
23 apply_elevation_cutoff, estimates_tropo_gradients, estimates_ztd, max_abs,
24 residual_ionosphere_unknown_count, rms, state_from_solution, tropo_gradient_unknown_count,
25 validate_float_solution_output, validate_float_solve_boundary, weighted_rms, ztd_unknown_count,
26 FloatEpoch, FloatSolution, FloatSolveConfig, FloatSolveError, FloatSolveOptions, FloatState,
27 FloatStatus, ModelContext, TroposphereOptions,
28};
29
30const RESIDUAL_SCREEN_THRESHOLD: f64 = 4.0;
31const RESIDUAL_SCREEN_MAX_PASSES: usize = 8;
32const RESIDUAL_SCREEN_ACCEPT_FACTOR: f64 = 2.0;
33const SINGLE_EPOCH_AMBIGUITY_TOLERANCE_M: f64 = f64::MAX;
34
35pub fn solve_float_epochs(
37 source: &dyn ObservableEphemerisSource,
38 epochs: &[FloatEpoch],
39 initial_state: FloatState,
40 config: FloatSolveConfig,
41) -> Result<FloatSolution, FloatSolveError> {
42 validate_float_solve_boundary(epochs, &initial_state, &config)?;
43 use crate::estimation::recipe::StrategyId;
44 use crate::estimation::strategies::{
45 estimate, EstimateError, EstimateInput, EstimateOptions, EstimateOutput,
46 };
47 match estimate(
48 EstimateInput::PppFloat {
49 source,
50 epochs,
51 initial_state,
52 config,
53 },
54 EstimateOptions::new(StrategyId::ppp_reference()),
55 ) {
56 Ok(EstimateOutput::PppFloat(solution)) => Ok(*solution),
57 Err(EstimateError::PppFloat(error)) => Err(error),
58 Ok(_) | Err(_) => {
59 unreachable!(
60 "the PPP reference strategy yields a PPP float solution or a PPP float error"
61 )
62 }
63 }
64}
65
66pub(crate) fn run_float_epochs(
74 recipe: &EstimationRecipe,
75 source: &dyn ObservableEphemerisSource,
76 epochs: &[FloatEpoch],
77 initial_state: FloatState,
78 config: FloatSolveConfig,
79) -> Result<FloatSolution, FloatSolveError> {
80 solve_float_multi_screened(source, epochs, initial_state, config, recipe.normal)
81}
82
83pub fn solve_float_epoch(
87 source: &dyn ObservableEphemerisSource,
88 epoch: FloatEpoch,
89 initial_state: FloatState,
90 mut config: FloatSolveConfig,
91) -> Result<FloatSolution, FloatSolveError> {
92 let epochs = [epoch];
93 validate_float_solve_boundary(&epochs, &initial_state, &config)?;
94 let filtered_epochs;
95 let solve_epochs = if let Some(cutoff_deg) = config.elevation_cutoff_deg {
96 filtered_epochs = apply_elevation_cutoff(
97 source,
98 &epochs,
99 &initial_state,
100 cutoff_deg,
101 config.tropo,
102 config.estimate_residual_ionosphere,
103 )?;
104 filtered_epochs.as_slice()
105 } else {
106 &epochs
107 };
108 let ambiguity_ids = solve_epochs[0]
109 .observations
110 .iter()
111 .map(|obs| AmbiguityId::new(obs.ambiguity_id.clone()))
112 .collect::<Vec<_>>();
113 config.opts.ambiguity_tolerance_m = SINGLE_EPOCH_AMBIGUITY_TOLERANCE_M;
114 let ctx = ModelContext {
115 source,
116 weights: config.weights,
117 tropo: config.tropo,
118 corrections: &config.corrections,
119 normal: NormalRecipe::PppDenseLastTie,
120 estimate_residual_ionosphere: config.estimate_residual_ionosphere,
121 };
122 iterate_multi(
123 ctx,
124 solve_epochs,
125 &ambiguity_ids,
126 initial_state,
127 config.opts,
128 1,
129 )
130}
131
132fn solve_float_multi_screened(
133 source: &dyn ObservableEphemerisSource,
134 epochs: &[FloatEpoch],
135 state: FloatState,
136 config: FloatSolveConfig,
137 normal: NormalRecipe,
138) -> Result<FloatSolution, FloatSolveError> {
139 validate_float_solve_boundary(epochs, &state, &config)?;
140 let FloatSolveConfig {
141 weights,
142 tropo,
143 corrections,
144 opts,
145 elevation_cutoff_deg,
146 residual_screen,
147 estimate_residual_ionosphere,
148 } = config;
149 let ctx = ModelContext {
150 source,
151 weights,
152 tropo,
153 corrections: &corrections,
154 normal,
155 estimate_residual_ionosphere,
156 };
157 let filtered_epochs;
158 let solve_epochs = if let Some(cutoff_deg) = elevation_cutoff_deg {
159 filtered_epochs = apply_elevation_cutoff(
160 source,
161 epochs,
162 &state,
163 cutoff_deg,
164 tropo,
165 estimate_residual_ionosphere,
166 )?;
167 filtered_epochs.as_slice()
168 } else {
169 epochs
170 };
171 let ambiguity_ids = multi_ambiguity_ids(solve_epochs);
172 let solution = iterate_multi(ctx, solve_epochs, &ambiguity_ids, state.clone(), opts, 1)?;
173
174 if !residual_screen {
175 return Ok(solution);
176 }
177
178 let unscreened_wrms = solution_weighted_rms(ctx, solve_epochs, &solution, &state);
179 match run_residual_screen(ctx, solve_epochs.to_vec(), state, opts, solution.clone(), 1)? {
180 ScreenResult::Clean => Ok(solution),
181 ScreenResult::Screened {
182 solution: screened,
183 epochs: retained,
184 } => {
185 let screened_wrms = solution_weighted_rms(
186 ctx,
187 &retained,
188 screened.as_ref(),
189 &state_from_solution(&screened, &FloatState::default_for_epochs(&retained)),
190 );
191 if screened_wrms.is_finite()
192 && unscreened_wrms.is_finite()
193 && screened_wrms * RESIDUAL_SCREEN_ACCEPT_FACTOR < unscreened_wrms
194 {
195 Ok(*screened)
196 } else {
197 Ok(solution)
198 }
199 }
200 }
201}
202
203enum ScreenResult {
204 Clean,
205 Screened {
206 solution: Box<FloatSolution>,
207 epochs: Vec<FloatEpoch>,
208 },
209}
210
211fn run_residual_screen(
212 ctx: ModelContext,
213 epochs: Vec<FloatEpoch>,
214 seed_state: FloatState,
215 opts: FloatSolveOptions,
216 solution: FloatSolution,
217 pass: usize,
218) -> Result<ScreenResult, FloatSolveError> {
219 if pass > RESIDUAL_SCREEN_MAX_PASSES {
220 return Ok(ScreenResult::Screened {
221 solution: Box::new(solution),
222 epochs,
223 });
224 }
225
226 let candidate_state = state_from_solution(&solution, &seed_state);
227 match worst_multi_residual(ctx, &epochs, &candidate_state)? {
228 Some((epoch_idx, sat)) => {
229 let pruned = exclude_observation(&epochs, epoch_idx, &sat);
230 if !multi_enough_after_prune(&pruned, ctx.tropo, ctx.estimate_residual_ionosphere) {
231 return Ok(ScreenResult::Screened {
232 solution: Box::new(solution),
233 epochs,
234 });
235 }
236 let ambiguity_ids = multi_ambiguity_ids(&pruned);
237 let candidate = iterate_multi(
238 ctx,
239 &pruned,
240 &ambiguity_ids,
241 reseed_state(&seed_state, &pruned),
242 opts,
243 1,
244 )?;
245 run_residual_screen(ctx, pruned, seed_state, opts, candidate, pass + 1)
246 }
247 None => {
248 if pass == 1 {
249 Ok(ScreenResult::Clean)
250 } else {
251 Ok(ScreenResult::Screened {
252 solution: Box::new(solution),
253 epochs,
254 })
255 }
256 }
257 }
258}
259
260fn iterate_multi(
261 ctx: ModelContext,
262 epochs: &[FloatEpoch],
263 ambiguity_ids: &[AmbiguityId],
264 state: FloatState,
265 opts: FloatSolveOptions,
266 iter: usize,
267) -> Result<FloatSolution, FloatSolveError> {
268 let mut current = state;
269 let mut iteration = iter;
270 let max_iterations = opts.max_iterations;
271
272 loop {
273 let binding = AmbiguityBinding::Estimated {
274 ids: ambiguity_ids,
275 values: ¤t.ambiguities_m,
276 };
277 let rows = build_rows(ctx, epochs, &binding, ¤t).map_err(PppRowError::into_float)?;
278 let layout = PppNormalLayout::new(
279 epochs.len(),
280 ztd_unknown_count(ctx.tropo),
281 tropo_gradient_unknown_count(ctx.tropo),
282 residual_ionosphere_unknown_count(
283 ctx.estimate_residual_ionosphere,
284 ambiguity_ids.len(),
285 ),
286 ambiguity_ids.len(),
287 );
288 let dx = solve_normal_equations(&rows, layout, ctx.normal)?;
289 let next = apply_multi_delta(
290 ¤t,
291 epochs.len(),
292 ambiguity_ids,
293 &dx,
294 ctx.tropo,
295 ctx.estimate_residual_ionosphere,
296 )?;
297 let (pos_step, clock_step, ztd_step, gradient_step, ambiguity_step) = multi_step_norms(
298 &dx,
299 epochs.len(),
300 ctx.tropo,
301 ctx.estimate_residual_ionosphere,
302 ambiguity_ids.len(),
303 );
304
305 if pos_step <= opts.position_tolerance_m
306 && clock_step <= opts.clock_tolerance_m
307 && ztd_step <= opts.ztd_tolerance_m
308 && gradient_step <= opts.ztd_tolerance_m
309 && ambiguity_step <= opts.ambiguity_tolerance_m
310 {
311 return finalize_multi(
312 ctx,
313 epochs,
314 ambiguity_ids,
315 next,
316 iteration,
317 true,
318 FloatStatus::StateTolerance,
319 );
320 }
321
322 if iteration >= max_iterations {
323 return finalize_multi(
324 ctx,
325 epochs,
326 ambiguity_ids,
327 next,
328 iteration,
329 false,
330 FloatStatus::MaxIterations,
331 );
332 }
333
334 current = next;
335 iteration += 1;
336 }
337}
338
339fn apply_multi_delta(
340 state: &FloatState,
341 n_epochs: usize,
342 ambiguity_ids: &[AmbiguityId],
343 dx: &[f64],
344 tropo: TroposphereOptions,
345 estimate_residual_ionosphere: bool,
346) -> Result<FloatState, FloatSolveError> {
347 let mut idx = 3;
348 let clock_deltas = &dx[idx..idx + n_epochs];
349 idx += n_epochs;
350 let ztd_delta = if estimates_ztd(tropo) {
351 let v = dx[idx];
352 idx += 1;
353 v
354 } else {
355 0.0
356 };
357 let (tropo_gradient_north_delta, tropo_gradient_east_delta) =
358 if estimates_tropo_gradients(tropo) {
359 let north = dx[idx];
360 let east = dx[idx + 1];
361 idx += 2;
362 (north, east)
363 } else {
364 (0.0, 0.0)
365 };
366 let mut residual_ionosphere_m = BTreeMap::new();
367 if estimate_residual_ionosphere {
368 let ionosphere_deltas = &dx[idx..idx + ambiguity_ids.len()];
369 idx += ambiguity_ids.len();
370 for (id, delta) in ambiguity_ids.iter().zip(ionosphere_deltas) {
371 let prior = state
372 .residual_ionosphere_m
373 .get(id.as_str())
374 .copied()
375 .unwrap_or(0.0);
376 residual_ionosphere_m.insert(id.as_str().to_string(), prior + delta);
377 }
378 }
379 let ambiguity_deltas = &dx[idx..];
380 let clocks_m = state
381 .clocks_m
382 .iter()
383 .zip(clock_deltas)
384 .map(|(clock, delta)| clock + delta)
385 .collect();
386 let mut ambiguities_m = BTreeMap::new();
387 for (id, delta) in ambiguity_ids.iter().zip(ambiguity_deltas) {
388 let prior = state
389 .ambiguities_m
390 .get(id.as_str())
391 .copied()
392 .ok_or_else(|| FloatSolveError::MissingAmbiguity(id.as_str().to_string()))?;
393 ambiguities_m.insert(id.as_str().to_string(), prior + delta);
394 }
395 Ok(FloatState {
396 position_m: [
397 state.position_m[0] + dx[0],
398 state.position_m[1] + dx[1],
399 state.position_m[2] + dx[2],
400 ],
401 clocks_m,
402 ambiguities_m,
403 ztd_m: state.ztd_m + ztd_delta,
404 tropo_gradient_north_m: state.tropo_gradient_north_m + tropo_gradient_north_delta,
405 tropo_gradient_east_m: state.tropo_gradient_east_m + tropo_gradient_east_delta,
406 residual_ionosphere_m,
407 })
408}
409
410fn multi_step_norms(
411 dx: &[f64],
412 n_epochs: usize,
413 tropo: TroposphereOptions,
414 estimate_residual_ionosphere: bool,
415 n_ambiguities: usize,
416) -> (f64, f64, f64, f64, f64) {
417 let pos = vec3::norm3([dx[0], dx[1], dx[2]]);
418 let mut idx = 3;
419 let clock = max_abs(&dx[idx..idx + n_epochs]);
420 idx += n_epochs;
421 let ztd = if estimates_ztd(tropo) {
422 let v = dx[idx].abs();
423 idx += 1;
424 v
425 } else {
426 0.0
427 };
428 let gradient = if estimates_tropo_gradients(tropo) {
429 let v = max_abs(&dx[idx..idx + 2]);
430 idx += 2;
431 v
432 } else {
433 0.0
434 };
435 let ionosphere = if estimate_residual_ionosphere {
436 let v = max_abs(&dx[idx..idx + n_ambiguities]);
437 idx += n_ambiguities;
438 v
439 } else {
440 0.0
441 };
442 let ambiguity = max_abs(&dx[idx..]);
443 (pos, clock, ztd, gradient, ambiguity.max(ionosphere))
444}
445
446fn finalize_multi(
447 ctx: ModelContext,
448 epochs: &[FloatEpoch],
449 ambiguity_ids: &[AmbiguityId],
450 state: FloatState,
451 iterations: usize,
452 converged: bool,
453 status: FloatStatus,
454) -> Result<FloatSolution, FloatSolveError> {
455 let residuals = residual_rows(ctx, epochs, &state.ambiguities_m, &state)
456 .map_err(PppRowError::into_float)?;
457 let binding = AmbiguityBinding::Estimated {
458 ids: ambiguity_ids,
459 values: &state.ambiguities_m,
460 };
461 let rows = build_rows(ctx, epochs, &binding, &state).map_err(PppRowError::into_float)?;
462 let covariance = ppp_position_covariance(
463 &rows,
464 PppNormalLayout::new(
465 epochs.len(),
466 ztd_unknown_count(ctx.tropo),
467 tropo_gradient_unknown_count(ctx.tropo),
468 residual_ionosphere_unknown_count(
469 ctx.estimate_residual_ionosphere,
470 ambiguity_ids.len(),
471 ),
472 ambiguity_ids.len(),
473 ),
474 state.position_m,
475 )?;
476 let code: Vec<f64> = residuals.iter().map(|r| r.code_m).collect();
477 let phase: Vec<f64> = residuals.iter().map(|r| r.phase_m).collect();
478 let temporal_correlation = estimate_temporal_correlation(&residuals, epochs);
479 let (temporal_position_covariance, temporal_position_covariance_scale_factor) =
480 temporal_position_covariance(
481 covariance.formal,
482 covariance.posterior_variance_factor,
483 temporal_correlation,
484 );
485 let solution = FloatSolution {
486 position_m: state.position_m,
487 position_covariance: covariance.scaled,
488 formal_position_covariance: covariance.formal,
489 posterior_variance_factor: covariance.posterior_variance_factor,
490 position_covariance_scale_factor: covariance.covariance_scale_factor,
491 temporal_position_covariance,
492 temporal_position_covariance_scale_factor,
493 temporal_correlation,
494 epoch_clocks_m: state.clocks_m,
495 ambiguities_m: state.ambiguities_m,
496 residual_ionosphere_m: if ctx.estimate_residual_ionosphere {
497 state.residual_ionosphere_m
498 } else {
499 BTreeMap::new()
500 },
501 ztd_residual_m: if estimates_ztd(ctx.tropo) {
502 Some(state.ztd_m)
503 } else {
504 None
505 },
506 tropo_gradient_north_m: if estimates_tropo_gradients(ctx.tropo) {
507 Some(state.tropo_gradient_north_m)
508 } else {
509 None
510 },
511 tropo_gradient_east_m: if estimates_tropo_gradients(ctx.tropo) {
512 Some(state.tropo_gradient_east_m)
513 } else {
514 None
515 },
516 tropo_gradient_covariance_m2: covariance.tropo_gradient_scaled_m2,
517 formal_tropo_gradient_covariance_m2: covariance.tropo_gradient_formal_m2,
518 residuals_m: residuals.clone(),
519 used_sats: ambiguity_ids
520 .iter()
521 .map(|id| id.as_str().to_string())
522 .collect(),
523 iterations,
524 converged,
525 status,
526 code_rms_m: rms(&code),
527 phase_rms_m: rms(&phase),
528 weighted_rms_m: weighted_rms(&residuals, ctx.weights),
529 };
530 validate_float_solution_output(&solution, epochs.len())?;
531 Ok(solution)
532}
533
534fn solution_weighted_rms(
535 ctx: ModelContext,
536 epochs: &[FloatEpoch],
537 solution: &FloatSolution,
538 seed_state: &FloatState,
539) -> f64 {
540 let state = state_from_solution(solution, seed_state);
541 match residual_rows(ctx, epochs, &state.ambiguities_m, &state) {
542 Ok(rows) => weighted_rms(&rows, ctx.weights),
543 Err(_) => f64::INFINITY,
544 }
545}
546
547fn worst_multi_residual(
548 ctx: ModelContext,
549 epochs: &[FloatEpoch],
550 state: &FloatState,
551) -> Result<Option<(usize, String)>, FloatSolveError> {
552 let rows =
553 residual_rows(ctx, epochs, &state.ambiguities_m, state).map_err(PppRowError::into_float)?;
554 let candidate = rows
555 .iter()
556 .flat_map(|r| {
557 [
558 (
559 normalized_residual(
560 ResidualNormRecipe::PppInverseSigmaMagnitude,
561 r.code_m,
562 r.code_weight,
563 ),
564 r.epoch_index,
565 r.satellite_id.clone(),
566 ),
567 (
568 normalized_residual(
569 ResidualNormRecipe::PppInverseSigmaMagnitude,
570 r.phase_m,
571 r.phase_weight,
572 ),
573 r.epoch_index,
574 r.satellite_id.clone(),
575 ),
576 ]
577 })
578 .max_by(|a, b| a.0.total_cmp(&b.0));
579 Ok(match candidate {
580 Some((normalized, epoch_idx, sat)) if normalized > RESIDUAL_SCREEN_THRESHOLD => {
581 Some((epoch_idx, sat))
582 }
583 _ => None,
584 })
585}
586
587fn exclude_observation(
588 epochs: &[FloatEpoch],
589 drop_epoch_idx: usize,
590 drop_sat: &str,
591) -> Vec<FloatEpoch> {
592 epochs
593 .iter()
594 .enumerate()
595 .filter_map(|(epoch_idx, epoch)| {
596 let mut epoch = epoch.clone();
597 if epoch_idx == drop_epoch_idx {
598 epoch
599 .observations
600 .retain(|obs| obs.satellite_id != drop_sat);
601 }
602 if epoch.observations.is_empty() {
603 None
604 } else {
605 Some(epoch)
606 }
607 })
608 .collect()
609}
610
611fn multi_enough_after_prune(
612 epochs: &[FloatEpoch],
613 tropo: TroposphereOptions,
614 estimate_residual_ionosphere: bool,
615) -> bool {
616 if epochs.len() < 2 {
617 return false;
618 }
619 let n_sats = multi_ambiguity_ids(epochs).len();
620 let n_obs: usize = epochs.iter().map(|e| e.observations.len()).sum();
621 let equations = 2 * n_obs;
622 let unknowns = ParameterLayout::ppp(
623 epochs.len(),
624 ztd_unknown_count(tropo),
625 tropo_gradient_unknown_count(tropo),
626 residual_ionosphere_unknown_count(estimate_residual_ionosphere, n_sats),
627 n_sats,
628 )
629 .dim();
630 n_sats >= 4 && equations >= unknowns
631}
632
633fn reseed_state(state: &FloatState, epochs: &[FloatEpoch]) -> FloatState {
634 FloatState {
635 position_m: state.position_m,
636 clocks_m: vec![state.clocks_m[0]; epochs.len()],
637 ambiguities_m: initial_ambiguities(epochs),
638 ztd_m: state.ztd_m,
639 tropo_gradient_north_m: state.tropo_gradient_north_m,
640 tropo_gradient_east_m: state.tropo_gradient_east_m,
641 residual_ionosphere_m: BTreeMap::new(),
642 }
643}
644
645pub(super) fn initial_ambiguities(epochs: &[FloatEpoch]) -> BTreeMap<String, f64> {
646 let mut out = BTreeMap::new();
647 for obs in epochs.iter().flat_map(|e| e.observations.iter()) {
648 out.entry(obs.ambiguity_id.clone())
649 .or_insert(obs.phase_m - obs.code_m);
650 }
651 out
652}
653
654fn multi_ambiguity_ids(epochs: &[FloatEpoch]) -> Vec<AmbiguityId> {
655 epochs
656 .iter()
657 .flat_map(|e| {
658 e.observations
659 .iter()
660 .map(|o| AmbiguityId::new(o.ambiguity_id.clone()))
661 })
662 .collect::<BTreeSet<_>>()
663 .into_iter()
664 .collect()
665}