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