1use std::collections::{BTreeMap, BTreeSet};
10
11use crate::ambiguity::AmbiguityId;
12use crate::astro::math::vec3;
13use crate::estimation::recipe::{EstimationRecipe, NormalRecipe};
14use crate::estimation::substrate::ambiguity::resolve_integer_lattice;
15use crate::observables::ObservableEphemerisSource;
16
17use super::normal::{
18 ambiguity_covariance_from_normal, clock_eliminated_normal_equations, ppp_position_covariance,
19 solve_normal_equations, PppNormalLayout,
20};
21use super::rows::{build_rows, residual_rows, AmbiguityBinding, PppRowError};
22use super::temporal::{estimate_temporal_correlation, temporal_position_covariance};
23use super::{
24 apply_elevation_cutoff, estimates_tropo_gradients, estimates_ztd, max_abs,
25 residual_ionosphere_unknown_count, rms, state_from_solution, tropo_gradient_unknown_count,
26 validate_fixed_solve_boundary, weighted_rms, ztd_unknown_count, AmbiguitySearch,
27 FixedIntegerMetadata, FixedSolution, FixedSolveConfig, FixedSolveError, FloatEpoch,
28 FloatSolution, FloatSolveError, FloatSolveOptions, FloatState, FloatStatus, IntegerStatus,
29 ModelContext, TroposphereOptions,
30};
31
32pub fn solve_fixed_from_float(
35 source: &dyn ObservableEphemerisSource,
36 epochs: &[FloatEpoch],
37 float_solution: FloatSolution,
38 config: FixedSolveConfig,
39) -> Result<FixedSolution, FixedSolveError> {
40 validate_fixed_solve_boundary(epochs, &float_solution, &config)?;
41 use crate::estimation::recipe::StrategyId;
42 use crate::estimation::strategies::{
43 estimate, EstimateError, EstimateInput, EstimateOptions, EstimateOutput,
44 };
45 match estimate(
46 EstimateInput::PppFixed {
47 source,
48 epochs,
49 float_solution,
50 config,
51 },
52 EstimateOptions::new(StrategyId::ppp_reference()),
53 ) {
54 Ok(EstimateOutput::PppFixed(solution)) => Ok(*solution),
55 Err(EstimateError::PppFixed(error)) => Err(error),
56 Ok(_) | Err(_) => {
57 unreachable!(
58 "the PPP reference strategy yields a PPP fixed solution or a PPP fixed error"
59 )
60 }
61 }
62}
63
64pub(crate) fn run_fixed_from_float(
71 recipe: &EstimationRecipe,
72 source: &dyn ObservableEphemerisSource,
73 epochs: &[FloatEpoch],
74 float_solution: FloatSolution,
75 config: FixedSolveConfig,
76) -> Result<FixedSolution, FixedSolveError> {
77 validate_fixed_solve_boundary(epochs, &float_solution, &config)?;
78 let initial_state = fixed_state_from_float(&float_solution);
79 let filtered_epochs;
80 let solve_epochs = if let Some(cutoff_deg) = config.elevation_cutoff_deg {
81 filtered_epochs = apply_elevation_cutoff(
82 source,
83 epochs,
84 &initial_state,
85 cutoff_deg,
86 config.tropo,
87 config.estimate_residual_ionosphere,
88 )
89 .map_err(FixedSolveError::Float)?;
90 filtered_epochs.as_slice()
91 } else {
92 epochs
93 };
94 let active_order;
95 let search_order = if config.elevation_cutoff_deg.is_some() {
96 active_order = active_ambiguity_ids(solve_epochs);
97 Some(active_order.as_slice())
98 } else {
99 None
100 };
101 let fixed_meta =
102 search_integer_ambiguities(source, solve_epochs, &float_solution, &config, search_order)?;
103 let fixed_m = fixed_ambiguities_m(
104 &fixed_meta.fixed_cycles,
105 &config.ambiguity.wavelengths_m,
106 &config.ambiguity.offsets_m,
107 )?;
108 let ctx = ModelContext {
109 source,
110 weights: config.weights,
111 tropo: config.tropo,
112 corrections: &config.corrections,
113 normal: recipe.normal,
114 estimate_residual_ionosphere: config.estimate_residual_ionosphere,
115 };
116 let resolve = iterate_fixed_multi(ctx, solve_epochs, &fixed_m, initial_state, config.opts, 1)?;
117 finalize_fixed_multi(
118 ctx,
119 solve_epochs,
120 fixed_meta,
121 fixed_m,
122 float_solution,
123 resolve,
124 )
125}
126
127struct FixedSearchResult {
128 order: Vec<AmbiguityId>,
129 fixed_cycles: BTreeMap<String, i64>,
130 integer: FixedIntegerMetadata,
131}
132
133struct FixedResolve {
136 state: FloatState,
137 iterations: usize,
138 converged: bool,
139 status: FloatStatus,
140}
141
142impl From<FloatSolveError> for FixedSolveError {
143 fn from(value: FloatSolveError) -> Self {
144 Self::Float(value)
145 }
146}
147
148fn search_integer_ambiguities(
149 source: &dyn ObservableEphemerisSource,
150 epochs: &[FloatEpoch],
151 float_solution: &FloatSolution,
152 config: &FixedSolveConfig,
153 active_order: Option<&[AmbiguityId]>,
154) -> Result<FixedSearchResult, FixedSolveError> {
155 let order: Vec<AmbiguityId> = active_order.map_or_else(
156 || {
157 float_solution
158 .used_sats
159 .iter()
160 .map(|sat| AmbiguityId::new(sat.clone()))
161 .collect()
162 },
163 |order| order.to_vec(),
164 );
165 let covariance_cycles =
166 ambiguity_covariance_cycles(source, epochs, &order, float_solution, config)?;
167 let float_cycles = float_ambiguities_cycles(
168 float_solution,
169 &config.ambiguity.wavelengths_m,
170 &config.ambiguity.offsets_m,
171 )?;
172 let floats: Vec<f64> = order
173 .iter()
174 .map(|id| float_cycles.get(id.as_str()).copied().unwrap())
175 .collect();
176 let result = resolve_integer_lattice(
177 &floats,
178 &covariance_cycles,
179 config.ambiguity.ratio_threshold,
180 )
181 .map_err(FixedSolveError::Integer)?;
182 let fixed_cycles = order
183 .iter()
184 .map(|id| id.as_str().to_string())
185 .zip(result.fixed.iter().copied())
186 .collect::<BTreeMap<_, _>>();
187 let search_order: Vec<String> = order.iter().map(|id| id.as_str().to_string()).collect();
188 Ok(FixedSearchResult {
189 order,
190 fixed_cycles,
191 integer: FixedIntegerMetadata {
192 integer_status: if result.fixed_status {
193 IntegerStatus::Fixed
194 } else {
195 IntegerStatus::NotFixed
196 },
197 integer_ratio: result.ratio,
198 integer_best_score: result.best_score,
199 integer_second_best_score: result.second_best_score,
200 integer_candidates: result.candidates_evaluated,
201 ambiguity_search: AmbiguitySearch {
202 order: search_order,
203 float_cycles,
204 covariance_cycles: result.covariance,
205 covariance_inverse_cycles: result.covariance_inverse,
206 },
207 },
208 })
209}
210
211fn active_ambiguity_ids(epochs: &[FloatEpoch]) -> Vec<AmbiguityId> {
212 epochs
213 .iter()
214 .flat_map(|e| {
215 e.observations
216 .iter()
217 .map(|o| AmbiguityId::new(o.ambiguity_id.clone()))
218 })
219 .collect::<BTreeSet<_>>()
220 .into_iter()
221 .collect()
222}
223
224fn iterate_fixed_multi(
225 ctx: ModelContext,
226 epochs: &[FloatEpoch],
227 fixed_m: &BTreeMap<String, f64>,
228 state: FloatState,
229 opts: FloatSolveOptions,
230 iter: usize,
231) -> Result<FixedResolve, FixedSolveError> {
232 let mut current = state;
233 let mut iteration = iter;
234 let max_iterations = opts.max_iterations;
235
236 loop {
237 let binding = AmbiguityBinding::Held { values: fixed_m };
238 let rows = build_rows(ctx, epochs, &binding, ¤t).map_err(PppRowError::into_fixed)?;
239 let layout = PppNormalLayout::new(
240 epochs.len(),
241 ztd_unknown_count(ctx.tropo),
242 tropo_gradient_unknown_count(ctx.tropo),
243 residual_ionosphere_unknown_count(ctx.estimate_residual_ionosphere, fixed_m.len()),
244 0,
245 );
246 let dx = solve_normal_equations(&rows, layout, ctx.normal)?;
247 let next = apply_fixed_multi_delta(
248 ¤t,
249 epochs.len(),
250 fixed_m,
251 &dx,
252 ctx.tropo,
253 ctx.estimate_residual_ionosphere,
254 );
255 let (pos_step, clock_step, ztd_step, gradient_step) = fixed_multi_step_norms(
256 &dx,
257 ctx.tropo,
258 ctx.estimate_residual_ionosphere,
259 fixed_m.len(),
260 );
261
262 if pos_step <= opts.position_tolerance_m
263 && clock_step <= opts.clock_tolerance_m
264 && ztd_step <= opts.ztd_tolerance_m
265 && gradient_step <= opts.ztd_tolerance_m
266 {
267 return Ok(FixedResolve {
268 state: next,
269 iterations: iteration,
270 converged: true,
271 status: FloatStatus::StateTolerance,
272 });
273 }
274
275 if iteration >= max_iterations {
276 return Ok(FixedResolve {
277 state: next,
278 iterations: iteration,
279 converged: false,
280 status: FloatStatus::MaxIterations,
281 });
282 }
283
284 current = next;
285 iteration += 1;
286 }
287}
288
289fn apply_fixed_multi_delta(
290 state: &FloatState,
291 n_epochs: usize,
292 fixed_m: &BTreeMap<String, f64>,
293 dx: &[f64],
294 tropo: TroposphereOptions,
295 estimate_residual_ionosphere: bool,
296) -> FloatState {
297 let mut idx = 3;
298 let clock_deltas = &dx[idx..idx + n_epochs];
299 idx += n_epochs;
300 let ztd_delta = if estimates_ztd(tropo) {
301 let v = dx[idx];
302 idx += 1;
303 v
304 } else {
305 0.0
306 };
307 let (tropo_gradient_north_delta, tropo_gradient_east_delta) =
308 if estimates_tropo_gradients(tropo) {
309 let north = dx[idx];
310 let east = dx[idx + 1];
311 idx += 2;
312 (north, east)
313 } else {
314 (0.0, 0.0)
315 };
316 let mut residual_ionosphere_m = BTreeMap::new();
317 if estimate_residual_ionosphere {
318 let ionosphere_deltas = &dx[idx..idx + fixed_m.len()];
319 for ((id, _), delta) in fixed_m.iter().zip(ionosphere_deltas) {
320 let prior = state.residual_ionosphere_m.get(id).copied().unwrap_or(0.0);
321 residual_ionosphere_m.insert(id.clone(), prior + delta);
322 }
323 }
324 let clocks_m = state
325 .clocks_m
326 .iter()
327 .zip(clock_deltas)
328 .map(|(clock, delta)| clock + delta)
329 .collect();
330 FloatState {
331 position_m: [
332 state.position_m[0] + dx[0],
333 state.position_m[1] + dx[1],
334 state.position_m[2] + dx[2],
335 ],
336 clocks_m,
337 ambiguities_m: BTreeMap::new(),
338 ztd_m: state.ztd_m + ztd_delta,
339 tropo_gradient_north_m: state.tropo_gradient_north_m + tropo_gradient_north_delta,
340 tropo_gradient_east_m: state.tropo_gradient_east_m + tropo_gradient_east_delta,
341 residual_ionosphere_m,
342 }
343}
344
345fn fixed_multi_step_norms(
346 dx: &[f64],
347 tropo: TroposphereOptions,
348 estimate_residual_ionosphere: bool,
349 n_residual_ionosphere: usize,
350) -> (f64, f64, f64, f64) {
351 let pos = vec3::norm3([dx[0], dx[1], dx[2]]);
352 let n_ztd = ztd_unknown_count(tropo);
353 let n_gradients = tropo_gradient_unknown_count(tropo);
354 let n_ionosphere =
355 residual_ionosphere_unknown_count(estimate_residual_ionosphere, n_residual_ionosphere);
356 let n_clocks = dx.len() - 3 - n_ztd - n_gradients - n_ionosphere;
357 let clock = max_abs(&dx[3..3 + n_clocks]);
358 let mut idx = 3 + n_clocks;
359 let ztd = if estimates_ztd(tropo) {
360 let v = dx[idx].abs();
361 idx += 1;
362 v
363 } else {
364 0.0
365 };
366 let gradient = if estimates_tropo_gradients(tropo) {
367 let v = max_abs(&dx[idx..idx + 2]);
368 idx += 2;
369 v
370 } else {
371 0.0
372 };
373 let ionosphere = if estimate_residual_ionosphere {
374 max_abs(&dx[idx..idx + n_residual_ionosphere])
375 } else {
376 0.0
377 };
378 (pos, clock, ztd, gradient.max(ionosphere))
379}
380
381fn finalize_fixed_multi(
382 ctx: ModelContext,
383 epochs: &[FloatEpoch],
384 search: FixedSearchResult,
385 fixed_m: BTreeMap<String, f64>,
386 float_solution: FloatSolution,
387 resolve: FixedResolve,
388) -> Result<FixedSolution, FixedSolveError> {
389 let FixedResolve {
390 state,
391 iterations,
392 converged,
393 status,
394 } = resolve;
395 let residuals =
396 residual_rows(ctx, epochs, &fixed_m, &state).map_err(PppRowError::into_fixed)?;
397 let binding = AmbiguityBinding::Held { values: &fixed_m };
398 let rows = build_rows(ctx, epochs, &binding, &state).map_err(PppRowError::into_fixed)?;
399 let covariance = ppp_position_covariance(
400 &rows,
401 PppNormalLayout::new(
402 epochs.len(),
403 ztd_unknown_count(ctx.tropo),
404 tropo_gradient_unknown_count(ctx.tropo),
405 residual_ionosphere_unknown_count(ctx.estimate_residual_ionosphere, fixed_m.len()),
406 0,
407 ),
408 state.position_m,
409 )?;
410 let code: Vec<f64> = residuals.iter().map(|r| r.code_m).collect();
411 let phase: Vec<f64> = residuals.iter().map(|r| r.phase_m).collect();
412 let temporal_correlation = estimate_temporal_correlation(&residuals, epochs);
413 let (temporal_position_covariance, temporal_position_covariance_scale_factor) =
414 temporal_position_covariance(
415 covariance.formal,
416 covariance.posterior_variance_factor,
417 temporal_correlation,
418 );
419 Ok(FixedSolution {
420 position_m: state.position_m,
421 position_covariance: covariance.scaled,
422 formal_position_covariance: covariance.formal,
423 posterior_variance_factor: covariance.posterior_variance_factor,
424 position_covariance_scale_factor: covariance.covariance_scale_factor,
425 temporal_position_covariance,
426 temporal_position_covariance_scale_factor,
427 temporal_correlation,
428 epoch_clocks_m: state.clocks_m,
429 fixed_ambiguities_cycles: search.fixed_cycles,
430 fixed_ambiguities_m: fixed_m,
431 residual_ionosphere_m: if ctx.estimate_residual_ionosphere {
432 state.residual_ionosphere_m
433 } else {
434 BTreeMap::new()
435 },
436 ztd_residual_m: if estimates_ztd(ctx.tropo) {
437 Some(state.ztd_m)
438 } else {
439 None
440 },
441 tropo_gradient_north_m: if estimates_tropo_gradients(ctx.tropo) {
442 Some(state.tropo_gradient_north_m)
443 } else {
444 None
445 },
446 tropo_gradient_east_m: if estimates_tropo_gradients(ctx.tropo) {
447 Some(state.tropo_gradient_east_m)
448 } else {
449 None
450 },
451 tropo_gradient_covariance_m2: covariance.tropo_gradient_scaled_m2,
452 formal_tropo_gradient_covariance_m2: covariance.tropo_gradient_formal_m2,
453 float_solution,
454 residuals_m: residuals.clone(),
455 used_sats: search
456 .order
457 .into_iter()
458 .map(AmbiguityId::into_string)
459 .collect(),
460 iterations,
461 converged,
462 status,
463 code_rms_m: rms(&code),
464 phase_rms_m: rms(&phase),
465 weighted_rms_m: weighted_rms(&residuals, ctx.weights),
466 integer: search.integer,
467 })
468}
469
470fn fixed_state_from_float(solution: &FloatSolution) -> FloatState {
471 FloatState {
472 position_m: solution.position_m,
473 clocks_m: solution.epoch_clocks_m.clone(),
474 ambiguities_m: BTreeMap::new(),
475 ztd_m: solution.ztd_residual_m.unwrap_or(0.0),
476 tropo_gradient_north_m: solution.tropo_gradient_north_m.unwrap_or(0.0),
477 tropo_gradient_east_m: solution.tropo_gradient_east_m.unwrap_or(0.0),
478 residual_ionosphere_m: solution.residual_ionosphere_m.clone(),
479 }
480}
481
482fn float_ambiguities_cycles(
483 solution: &FloatSolution,
484 wavelengths_m: &BTreeMap<String, f64>,
485 offsets_m: &BTreeMap<String, f64>,
486) -> Result<BTreeMap<String, f64>, FixedSolveError> {
487 let mut out = BTreeMap::new();
488 for sat in &solution.used_sats {
489 let wavelength = wavelengths_m
490 .get(sat)
491 .copied()
492 .ok_or_else(|| FixedSolveError::MissingWavelength(sat.clone()))?;
493 let offset = offsets_m
494 .get(sat)
495 .copied()
496 .ok_or_else(|| FixedSolveError::MissingOffset(sat.clone()))?;
497 let ambiguity_m = solution.ambiguities_m.get(sat).copied().ok_or_else(|| {
498 FixedSolveError::Float(FloatSolveError::MissingAmbiguity(sat.clone()))
499 })?;
500 out.insert(sat.clone(), (ambiguity_m - offset) / wavelength);
501 }
502 Ok(out)
503}
504
505fn fixed_ambiguities_m(
506 fixed_cycles: &BTreeMap<String, i64>,
507 wavelengths_m: &BTreeMap<String, f64>,
508 offsets_m: &BTreeMap<String, f64>,
509) -> Result<BTreeMap<String, f64>, FixedSolveError> {
510 let mut out = BTreeMap::new();
511 for (sat, cycles) in fixed_cycles {
512 let wavelength = wavelengths_m
513 .get(sat)
514 .copied()
515 .ok_or_else(|| FixedSolveError::MissingWavelength(sat.clone()))?;
516 let offset = offsets_m
517 .get(sat)
518 .copied()
519 .ok_or_else(|| FixedSolveError::MissingOffset(sat.clone()))?;
520 out.insert(sat.clone(), offset + *cycles as f64 * wavelength);
521 }
522 Ok(out)
523}
524
525fn ambiguity_covariance_cycles(
526 source: &dyn ObservableEphemerisSource,
527 epochs: &[FloatEpoch],
528 ambiguity_ids: &[AmbiguityId],
529 float_solution: &FloatSolution,
530 config: &FixedSolveConfig,
531) -> Result<Vec<Vec<f64>>, FixedSolveError> {
532 let state = state_from_solution(float_solution, &FloatState::default_for_epochs(epochs));
533 let layout = PppNormalLayout::new(
534 epochs.len(),
535 ztd_unknown_count(config.tropo),
536 tropo_gradient_unknown_count(config.tropo),
537 residual_ionosphere_unknown_count(config.estimate_residual_ionosphere, ambiguity_ids.len()),
538 ambiguity_ids.len(),
539 );
540 let start = layout.reduced_ambiguity_offset();
541 let ctx = ModelContext {
542 source,
543 weights: config.weights,
544 tropo: config.tropo,
545 corrections: &config.corrections,
546 normal: NormalRecipe::PppDenseLastTie,
549 estimate_residual_ionosphere: config.estimate_residual_ionosphere,
550 };
551 let binding = AmbiguityBinding::Estimated {
552 ids: ambiguity_ids,
553 values: &state.ambiguities_m,
554 };
555 let rows = build_rows(ctx, epochs, &binding, &state)
556 .map_err(|e| FixedSolveError::from(e.into_float()))?;
557 let (normal, _rhs) = clock_eliminated_normal_equations(&rows, layout)?;
558 let covariance_m = ambiguity_covariance_from_normal(&normal, start, ambiguity_ids.len())?;
559 let mut covariance_cycles = vec![vec![0.0; ambiguity_ids.len()]; ambiguity_ids.len()];
560 for i in 0..ambiguity_ids.len() {
561 let lambda_i = config
562 .ambiguity
563 .wavelengths_m
564 .get(ambiguity_ids[i].as_str())
565 .copied()
566 .ok_or_else(|| {
567 FixedSolveError::MissingWavelength(ambiguity_ids[i].as_str().to_string())
568 })?;
569 for j in 0..ambiguity_ids.len() {
570 let lambda_j = config
571 .ambiguity
572 .wavelengths_m
573 .get(ambiguity_ids[j].as_str())
574 .copied()
575 .ok_or_else(|| {
576 FixedSolveError::MissingWavelength(ambiguity_ids[j].as_str().to_string())
577 })?;
578 covariance_cycles[i][j] = covariance_m[i][j] / (lambda_i * lambda_j);
579 }
580 }
581 Ok(covariance_cycles)
582}