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::{
22 estimates_ztd, max_abs, rms, state_from_solution, validate_float_solution_output,
23 validate_float_solve_boundary, weighted_rms, ztd_unknown_count, FloatEpoch, FloatSolution,
24 FloatSolveConfig, FloatSolveError, FloatSolveOptions, FloatState, FloatStatus, ModelContext,
25 TroposphereOptions,
26};
27
28const RESIDUAL_SCREEN_THRESHOLD: f64 = 4.0;
29const RESIDUAL_SCREEN_MAX_PASSES: usize = 8;
30const RESIDUAL_SCREEN_ACCEPT_FACTOR: f64 = 2.0;
31const SINGLE_EPOCH_AMBIGUITY_TOLERANCE_M: f64 = f64::MAX;
32
33pub fn solve_float_epochs(
35 source: &dyn ObservableEphemerisSource,
36 epochs: &[FloatEpoch],
37 initial_state: FloatState,
38 config: FloatSolveConfig,
39) -> Result<FloatSolution, FloatSolveError> {
40 validate_float_solve_boundary(epochs, &initial_state, &config)?;
41 use crate::estimation::recipe::StrategyId;
42 use crate::estimation::strategies::{
43 estimate, EstimateError, EstimateInput, EstimateOptions, EstimateOutput,
44 };
45 match estimate(
46 EstimateInput::PppFloat {
47 source,
48 epochs,
49 initial_state,
50 config,
51 },
52 EstimateOptions::new(StrategyId::ppp_reference()),
53 ) {
54 Ok(EstimateOutput::PppFloat(solution)) => Ok(*solution),
55 Err(EstimateError::PppFloat(error)) => Err(error),
56 Ok(_) | Err(_) => {
57 unreachable!(
58 "the PPP reference strategy yields a PPP float solution or a PPP float error"
59 )
60 }
61 }
62}
63
64pub(crate) fn run_float_epochs(
72 recipe: &EstimationRecipe,
73 source: &dyn ObservableEphemerisSource,
74 epochs: &[FloatEpoch],
75 initial_state: FloatState,
76 config: FloatSolveConfig,
77) -> Result<FloatSolution, FloatSolveError> {
78 solve_float_multi_screened(source, epochs, initial_state, config, recipe.normal)
79}
80
81pub fn solve_float_epoch(
85 source: &dyn ObservableEphemerisSource,
86 epoch: FloatEpoch,
87 initial_state: FloatState,
88 mut config: FloatSolveConfig,
89) -> Result<FloatSolution, FloatSolveError> {
90 let epochs = [epoch];
91 validate_float_solve_boundary(&epochs, &initial_state, &config)?;
92 let ambiguity_ids = epochs[0]
93 .observations
94 .iter()
95 .map(|obs| AmbiguityId::new(obs.ambiguity_id.clone()))
96 .collect::<Vec<_>>();
97 config.opts.ambiguity_tolerance_m = SINGLE_EPOCH_AMBIGUITY_TOLERANCE_M;
98 let ctx = ModelContext {
99 source,
100 weights: config.weights,
101 tropo: config.tropo,
102 corrections: &config.corrections,
103 normal: NormalRecipe::PppDenseLastTie,
104 };
105 iterate_multi(ctx, &epochs, &ambiguity_ids, initial_state, config.opts, 1)
106}
107
108fn solve_float_multi_screened(
109 source: &dyn ObservableEphemerisSource,
110 epochs: &[FloatEpoch],
111 state: FloatState,
112 config: FloatSolveConfig,
113 normal: NormalRecipe,
114) -> Result<FloatSolution, FloatSolveError> {
115 validate_float_solve_boundary(epochs, &state, &config)?;
116 let FloatSolveConfig {
117 weights,
118 tropo,
119 corrections,
120 opts,
121 residual_screen,
122 } = config;
123 let ctx = ModelContext {
124 source,
125 weights,
126 tropo,
127 corrections: &corrections,
128 normal,
129 };
130 let ambiguity_ids = multi_ambiguity_ids(epochs);
131 let solution = iterate_multi(ctx, epochs, &ambiguity_ids, state.clone(), opts, 1)?;
132
133 if !residual_screen {
134 return Ok(solution);
135 }
136
137 let unscreened_wrms = solution_weighted_rms(ctx, epochs, &solution, &state);
138 match run_residual_screen(ctx, epochs.to_vec(), state, opts, solution.clone(), 1)? {
139 ScreenResult::Clean => Ok(solution),
140 ScreenResult::Screened {
141 solution: screened,
142 epochs: retained,
143 } => {
144 let screened_wrms = solution_weighted_rms(
145 ctx,
146 &retained,
147 screened.as_ref(),
148 &state_from_solution(&screened, &FloatState::default_for_epochs(&retained)),
149 );
150 if screened_wrms.is_finite()
151 && unscreened_wrms.is_finite()
152 && screened_wrms * RESIDUAL_SCREEN_ACCEPT_FACTOR < unscreened_wrms
153 {
154 Ok(*screened)
155 } else {
156 Ok(solution)
157 }
158 }
159 }
160}
161
162enum ScreenResult {
163 Clean,
164 Screened {
165 solution: Box<FloatSolution>,
166 epochs: Vec<FloatEpoch>,
167 },
168}
169
170fn run_residual_screen(
171 ctx: ModelContext,
172 epochs: Vec<FloatEpoch>,
173 seed_state: FloatState,
174 opts: FloatSolveOptions,
175 solution: FloatSolution,
176 pass: usize,
177) -> Result<ScreenResult, FloatSolveError> {
178 if pass > RESIDUAL_SCREEN_MAX_PASSES {
179 return Ok(ScreenResult::Screened {
180 solution: Box::new(solution),
181 epochs,
182 });
183 }
184
185 let candidate_state = state_from_solution(&solution, &seed_state);
186 match worst_multi_residual(ctx, &epochs, &candidate_state)? {
187 Some((epoch_idx, sat)) => {
188 let pruned = exclude_observation(&epochs, epoch_idx, &sat);
189 if !multi_enough_after_prune(&pruned, ctx.tropo) {
190 return Ok(ScreenResult::Screened {
191 solution: Box::new(solution),
192 epochs,
193 });
194 }
195 let ambiguity_ids = multi_ambiguity_ids(&pruned);
196 let candidate = iterate_multi(
197 ctx,
198 &pruned,
199 &ambiguity_ids,
200 reseed_state(&seed_state, &pruned),
201 opts,
202 1,
203 )?;
204 run_residual_screen(ctx, pruned, seed_state, opts, candidate, pass + 1)
205 }
206 None => {
207 if pass == 1 {
208 Ok(ScreenResult::Clean)
209 } else {
210 Ok(ScreenResult::Screened {
211 solution: Box::new(solution),
212 epochs,
213 })
214 }
215 }
216 }
217}
218
219fn iterate_multi(
220 ctx: ModelContext,
221 epochs: &[FloatEpoch],
222 ambiguity_ids: &[AmbiguityId],
223 state: FloatState,
224 opts: FloatSolveOptions,
225 iter: usize,
226) -> Result<FloatSolution, FloatSolveError> {
227 let mut current = state;
228 let mut iteration = iter;
229 let max_iterations = opts.max_iterations;
230
231 loop {
232 let binding = AmbiguityBinding::Estimated {
233 ids: ambiguity_ids,
234 values: ¤t.ambiguities_m,
235 };
236 let rows = build_rows(ctx, epochs, &binding, ¤t).map_err(PppRowError::into_float)?;
237 let layout = PppNormalLayout::new(
238 epochs.len(),
239 ztd_unknown_count(ctx.tropo),
240 ambiguity_ids.len(),
241 );
242 let dx = solve_normal_equations(&rows, layout, ctx.normal)?;
243 let next = apply_multi_delta(¤t, epochs.len(), ambiguity_ids, &dx, ctx.tropo)?;
244 let (pos_step, clock_step, ztd_step, ambiguity_step) =
245 multi_step_norms(&dx, epochs.len(), ctx.tropo);
246
247 if pos_step <= opts.position_tolerance_m
248 && clock_step <= opts.clock_tolerance_m
249 && ztd_step <= opts.ztd_tolerance_m
250 && ambiguity_step <= opts.ambiguity_tolerance_m
251 {
252 return finalize_multi(
253 ctx,
254 epochs,
255 ambiguity_ids,
256 next,
257 iteration,
258 true,
259 FloatStatus::StateTolerance,
260 );
261 }
262
263 if iteration >= max_iterations {
264 return finalize_multi(
265 ctx,
266 epochs,
267 ambiguity_ids,
268 next,
269 iteration,
270 false,
271 FloatStatus::MaxIterations,
272 );
273 }
274
275 current = next;
276 iteration += 1;
277 }
278}
279
280fn apply_multi_delta(
281 state: &FloatState,
282 n_epochs: usize,
283 ambiguity_ids: &[AmbiguityId],
284 dx: &[f64],
285 tropo: TroposphereOptions,
286) -> Result<FloatState, FloatSolveError> {
287 let mut idx = 3;
288 let clock_deltas = &dx[idx..idx + n_epochs];
289 idx += n_epochs;
290 let ztd_delta = if estimates_ztd(tropo) {
291 let v = dx[idx];
292 idx += 1;
293 v
294 } else {
295 0.0
296 };
297 let ambiguity_deltas = &dx[idx..];
298 let clocks_m = state
299 .clocks_m
300 .iter()
301 .zip(clock_deltas)
302 .map(|(clock, delta)| clock + delta)
303 .collect();
304 let mut ambiguities_m = BTreeMap::new();
305 for (id, delta) in ambiguity_ids.iter().zip(ambiguity_deltas) {
306 let prior = state
307 .ambiguities_m
308 .get(id.as_str())
309 .copied()
310 .ok_or_else(|| FloatSolveError::MissingAmbiguity(id.as_str().to_string()))?;
311 ambiguities_m.insert(id.as_str().to_string(), prior + delta);
312 }
313 Ok(FloatState {
314 position_m: [
315 state.position_m[0] + dx[0],
316 state.position_m[1] + dx[1],
317 state.position_m[2] + dx[2],
318 ],
319 clocks_m,
320 ambiguities_m,
321 ztd_m: state.ztd_m + ztd_delta,
322 })
323}
324
325fn multi_step_norms(
326 dx: &[f64],
327 n_epochs: usize,
328 tropo: TroposphereOptions,
329) -> (f64, f64, f64, f64) {
330 let pos = vec3::norm3([dx[0], dx[1], dx[2]]);
331 let mut idx = 3;
332 let clock = max_abs(&dx[idx..idx + n_epochs]);
333 idx += n_epochs;
334 let ztd = if estimates_ztd(tropo) {
335 let v = dx[idx].abs();
336 idx += 1;
337 v
338 } else {
339 0.0
340 };
341 let ambiguity = max_abs(&dx[idx..]);
342 (pos, clock, ztd, ambiguity)
343}
344
345fn finalize_multi(
346 ctx: ModelContext,
347 epochs: &[FloatEpoch],
348 ambiguity_ids: &[AmbiguityId],
349 state: FloatState,
350 iterations: usize,
351 converged: bool,
352 status: FloatStatus,
353) -> Result<FloatSolution, FloatSolveError> {
354 let residuals = residual_rows(ctx, epochs, &state.ambiguities_m, &state)
355 .map_err(PppRowError::into_float)?;
356 let binding = AmbiguityBinding::Estimated {
357 ids: ambiguity_ids,
358 values: &state.ambiguities_m,
359 };
360 let rows = build_rows(ctx, epochs, &binding, &state).map_err(PppRowError::into_float)?;
361 let covariance = ppp_position_covariance(
362 &rows,
363 PppNormalLayout::new(
364 epochs.len(),
365 ztd_unknown_count(ctx.tropo),
366 ambiguity_ids.len(),
367 ),
368 state.position_m,
369 )?;
370 let code: Vec<f64> = residuals.iter().map(|r| r.code_m).collect();
371 let phase: Vec<f64> = residuals.iter().map(|r| r.phase_m).collect();
372 let solution = FloatSolution {
373 position_m: state.position_m,
374 position_covariance: covariance.scaled,
375 formal_position_covariance: covariance.formal,
376 posterior_variance_factor: covariance.posterior_variance_factor,
377 position_covariance_scale_factor: covariance.covariance_scale_factor,
378 epoch_clocks_m: state.clocks_m,
379 ambiguities_m: state.ambiguities_m,
380 ztd_residual_m: if estimates_ztd(ctx.tropo) {
381 Some(state.ztd_m)
382 } else {
383 None
384 },
385 residuals_m: residuals.clone(),
386 used_sats: ambiguity_ids
387 .iter()
388 .map(|id| id.as_str().to_string())
389 .collect(),
390 iterations,
391 converged,
392 status,
393 code_rms_m: rms(&code),
394 phase_rms_m: rms(&phase),
395 weighted_rms_m: weighted_rms(&residuals, ctx.weights),
396 };
397 validate_float_solution_output(&solution, epochs.len())?;
398 Ok(solution)
399}
400
401fn solution_weighted_rms(
402 ctx: ModelContext,
403 epochs: &[FloatEpoch],
404 solution: &FloatSolution,
405 seed_state: &FloatState,
406) -> f64 {
407 let state = state_from_solution(solution, seed_state);
408 match residual_rows(ctx, epochs, &state.ambiguities_m, &state) {
409 Ok(rows) => weighted_rms(&rows, ctx.weights),
410 Err(_) => f64::INFINITY,
411 }
412}
413
414fn worst_multi_residual(
415 ctx: ModelContext,
416 epochs: &[FloatEpoch],
417 state: &FloatState,
418) -> Result<Option<(usize, String)>, FloatSolveError> {
419 let rows =
420 residual_rows(ctx, epochs, &state.ambiguities_m, state).map_err(PppRowError::into_float)?;
421 let candidate = rows
422 .iter()
423 .flat_map(|r| {
424 [
425 (
426 normalized_residual(
427 ResidualNormRecipe::PppInverseSigmaMagnitude,
428 r.code_m,
429 r.code_weight,
430 ),
431 r.epoch_index,
432 r.satellite_id.clone(),
433 ),
434 (
435 normalized_residual(
436 ResidualNormRecipe::PppInverseSigmaMagnitude,
437 r.phase_m,
438 r.phase_weight,
439 ),
440 r.epoch_index,
441 r.satellite_id.clone(),
442 ),
443 ]
444 })
445 .max_by(|a, b| a.0.total_cmp(&b.0));
446 Ok(match candidate {
447 Some((normalized, epoch_idx, sat)) if normalized > RESIDUAL_SCREEN_THRESHOLD => {
448 Some((epoch_idx, sat))
449 }
450 _ => None,
451 })
452}
453
454fn exclude_observation(
455 epochs: &[FloatEpoch],
456 drop_epoch_idx: usize,
457 drop_sat: &str,
458) -> Vec<FloatEpoch> {
459 epochs
460 .iter()
461 .enumerate()
462 .filter_map(|(epoch_idx, epoch)| {
463 let mut epoch = epoch.clone();
464 if epoch_idx == drop_epoch_idx {
465 epoch
466 .observations
467 .retain(|obs| obs.satellite_id != drop_sat);
468 }
469 if epoch.observations.is_empty() {
470 None
471 } else {
472 Some(epoch)
473 }
474 })
475 .collect()
476}
477
478fn multi_enough_after_prune(epochs: &[FloatEpoch], tropo: TroposphereOptions) -> bool {
479 if epochs.len() < 2 {
480 return false;
481 }
482 let n_sats = multi_ambiguity_ids(epochs).len();
483 let n_obs: usize = epochs.iter().map(|e| e.observations.len()).sum();
484 let equations = 2 * n_obs;
485 let unknowns = ParameterLayout::ppp(epochs.len(), ztd_unknown_count(tropo), n_sats).dim();
486 n_sats >= 4 && equations >= unknowns
487}
488
489fn reseed_state(state: &FloatState, epochs: &[FloatEpoch]) -> FloatState {
490 FloatState {
491 position_m: state.position_m,
492 clocks_m: vec![state.clocks_m[0]; epochs.len()],
493 ambiguities_m: initial_ambiguities(epochs),
494 ztd_m: state.ztd_m,
495 }
496}
497
498pub(super) fn initial_ambiguities(epochs: &[FloatEpoch]) -> BTreeMap<String, f64> {
499 let mut out = BTreeMap::new();
500 for obs in epochs.iter().flat_map(|e| e.observations.iter()) {
501 out.entry(obs.ambiguity_id.clone())
502 .or_insert(obs.phase_m - obs.code_m);
503 }
504 out
505}
506
507fn multi_ambiguity_ids(epochs: &[FloatEpoch]) -> Vec<AmbiguityId> {
508 epochs
509 .iter()
510 .flat_map(|e| {
511 e.observations
512 .iter()
513 .map(|o| AmbiguityId::new(o.ambiguity_id.clone()))
514 })
515 .collect::<BTreeSet<_>>()
516 .into_iter()
517 .collect()
518}