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