1use std::collections::BTreeMap;
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::{
23 estimates_ztd, max_abs, rms, state_from_solution, validate_fixed_solve_boundary, weighted_rms,
24 ztd_unknown_count, AmbiguitySearch, FixedIntegerMetadata, FixedSolution, FixedSolveConfig,
25 FixedSolveError, FloatEpoch, FloatSolution, FloatSolveError, FloatSolveOptions, FloatState,
26 FloatStatus, IntegerStatus, ModelContext, TroposphereOptions,
27};
28
29pub fn solve_fixed_from_float(
32 source: &dyn ObservableEphemerisSource,
33 epochs: &[FloatEpoch],
34 float_solution: FloatSolution,
35 config: FixedSolveConfig,
36) -> Result<FixedSolution, FixedSolveError> {
37 validate_fixed_solve_boundary(epochs, &float_solution, &config)?;
38 use crate::estimation::recipe::StrategyId;
39 use crate::estimation::strategies::{
40 estimate, EstimateError, EstimateInput, EstimateOptions, EstimateOutput,
41 };
42 match estimate(
43 EstimateInput::PppFixed {
44 source,
45 epochs,
46 float_solution,
47 config,
48 },
49 EstimateOptions::new(StrategyId::ppp_reference()),
50 ) {
51 Ok(EstimateOutput::PppFixed(solution)) => Ok(*solution),
52 Err(EstimateError::PppFixed(error)) => Err(error),
53 Ok(_) | Err(_) => {
54 unreachable!(
55 "the PPP reference strategy yields a PPP fixed solution or a PPP fixed error"
56 )
57 }
58 }
59}
60
61pub(crate) fn run_fixed_from_float(
68 recipe: &EstimationRecipe,
69 source: &dyn ObservableEphemerisSource,
70 epochs: &[FloatEpoch],
71 float_solution: FloatSolution,
72 config: FixedSolveConfig,
73) -> Result<FixedSolution, FixedSolveError> {
74 validate_fixed_solve_boundary(epochs, &float_solution, &config)?;
75 let fixed_meta = search_integer_ambiguities(source, epochs, &float_solution, &config)?;
76 let fixed_m = fixed_ambiguities_m(
77 &fixed_meta.fixed_cycles,
78 &config.ambiguity.wavelengths_m,
79 &config.ambiguity.offsets_m,
80 )?;
81 let initial_state = fixed_state_from_float(&float_solution);
82 let ctx = ModelContext {
83 source,
84 weights: config.weights,
85 tropo: config.tropo,
86 corrections: &config.corrections,
87 normal: recipe.normal,
88 };
89 let resolve = iterate_fixed_multi(ctx, epochs, &fixed_m, initial_state, config.opts, 1)?;
90 finalize_fixed_multi(ctx, epochs, fixed_meta, fixed_m, float_solution, resolve)
91}
92
93struct FixedSearchResult {
94 order: Vec<AmbiguityId>,
95 fixed_cycles: BTreeMap<String, i64>,
96 integer: FixedIntegerMetadata,
97}
98
99struct FixedResolve {
102 state: FloatState,
103 iterations: usize,
104 converged: bool,
105 status: FloatStatus,
106}
107
108impl From<FloatSolveError> for FixedSolveError {
109 fn from(value: FloatSolveError) -> Self {
110 Self::Float(value)
111 }
112}
113
114fn search_integer_ambiguities(
115 source: &dyn ObservableEphemerisSource,
116 epochs: &[FloatEpoch],
117 float_solution: &FloatSolution,
118 config: &FixedSolveConfig,
119) -> Result<FixedSearchResult, FixedSolveError> {
120 let order: Vec<AmbiguityId> = float_solution
121 .used_sats
122 .iter()
123 .map(|sat| AmbiguityId::new(sat.clone()))
124 .collect();
125 let covariance_cycles =
126 ambiguity_covariance_cycles(source, epochs, &order, float_solution, config)?;
127 let float_cycles = float_ambiguities_cycles(
128 float_solution,
129 &config.ambiguity.wavelengths_m,
130 &config.ambiguity.offsets_m,
131 )?;
132 let floats: Vec<f64> = order
133 .iter()
134 .map(|id| float_cycles.get(id.as_str()).copied().unwrap())
135 .collect();
136 let result = resolve_integer_lattice(
137 &floats,
138 &covariance_cycles,
139 config.ambiguity.ratio_threshold,
140 )
141 .map_err(FixedSolveError::Integer)?;
142 let fixed_cycles = order
143 .iter()
144 .map(|id| id.as_str().to_string())
145 .zip(result.fixed.iter().copied())
146 .collect::<BTreeMap<_, _>>();
147 let search_order: Vec<String> = order.iter().map(|id| id.as_str().to_string()).collect();
148 Ok(FixedSearchResult {
149 order,
150 fixed_cycles,
151 integer: FixedIntegerMetadata {
152 integer_status: if result.fixed_status {
153 IntegerStatus::Fixed
154 } else {
155 IntegerStatus::NotFixed
156 },
157 integer_ratio: result.ratio,
158 integer_best_score: result.best_score,
159 integer_second_best_score: result.second_best_score,
160 integer_candidates: result.candidates_evaluated,
161 ambiguity_search: AmbiguitySearch {
162 order: search_order,
163 float_cycles,
164 covariance_cycles: result.covariance,
165 covariance_inverse_cycles: result.covariance_inverse,
166 },
167 },
168 })
169}
170
171fn iterate_fixed_multi(
172 ctx: ModelContext,
173 epochs: &[FloatEpoch],
174 fixed_m: &BTreeMap<String, f64>,
175 state: FloatState,
176 opts: FloatSolveOptions,
177 iter: usize,
178) -> Result<FixedResolve, FixedSolveError> {
179 let mut current = state;
180 let mut iteration = iter;
181 let max_iterations = opts.max_iterations;
182
183 loop {
184 let binding = AmbiguityBinding::Held { values: fixed_m };
185 let rows = build_rows(ctx, epochs, &binding, ¤t).map_err(PppRowError::into_fixed)?;
186 let layout = PppNormalLayout::new(epochs.len(), ztd_unknown_count(ctx.tropo), 0);
187 let dx = solve_normal_equations(&rows, layout, ctx.normal)?;
188 let next = apply_fixed_multi_delta(¤t, epochs.len(), &dx, ctx.tropo);
189 let (pos_step, clock_step, ztd_step) = fixed_multi_step_norms(&dx, ctx.tropo);
190
191 if pos_step <= opts.position_tolerance_m
192 && clock_step <= opts.clock_tolerance_m
193 && ztd_step <= opts.ztd_tolerance_m
194 {
195 return Ok(FixedResolve {
196 state: next,
197 iterations: iteration,
198 converged: true,
199 status: FloatStatus::StateTolerance,
200 });
201 }
202
203 if iteration >= max_iterations {
204 return Ok(FixedResolve {
205 state: next,
206 iterations: iteration,
207 converged: false,
208 status: FloatStatus::MaxIterations,
209 });
210 }
211
212 current = next;
213 iteration += 1;
214 }
215}
216
217fn apply_fixed_multi_delta(
218 state: &FloatState,
219 n_epochs: usize,
220 dx: &[f64],
221 tropo: TroposphereOptions,
222) -> FloatState {
223 let mut idx = 3;
224 let clock_deltas = &dx[idx..idx + n_epochs];
225 idx += n_epochs;
226 let ztd_delta = if estimates_ztd(tropo) { dx[idx] } else { 0.0 };
227 let clocks_m = state
228 .clocks_m
229 .iter()
230 .zip(clock_deltas)
231 .map(|(clock, delta)| clock + delta)
232 .collect();
233 FloatState {
234 position_m: [
235 state.position_m[0] + dx[0],
236 state.position_m[1] + dx[1],
237 state.position_m[2] + dx[2],
238 ],
239 clocks_m,
240 ambiguities_m: BTreeMap::new(),
241 ztd_m: state.ztd_m + ztd_delta,
242 }
243}
244
245fn fixed_multi_step_norms(dx: &[f64], tropo: TroposphereOptions) -> (f64, f64, f64) {
246 let pos = vec3::norm3([dx[0], dx[1], dx[2]]);
247 let n_ztd = ztd_unknown_count(tropo);
248 let n_clocks = dx.len() - 3 - n_ztd;
249 let clock = max_abs(&dx[3..3 + n_clocks]);
250 let ztd = if estimates_ztd(tropo) {
251 dx[3 + n_clocks].abs()
252 } else {
253 0.0
254 };
255 (pos, clock, ztd)
256}
257
258fn finalize_fixed_multi(
259 ctx: ModelContext,
260 epochs: &[FloatEpoch],
261 search: FixedSearchResult,
262 fixed_m: BTreeMap<String, f64>,
263 float_solution: FloatSolution,
264 resolve: FixedResolve,
265) -> Result<FixedSolution, FixedSolveError> {
266 let FixedResolve {
267 state,
268 iterations,
269 converged,
270 status,
271 } = resolve;
272 let residuals =
273 residual_rows(ctx, epochs, &fixed_m, &state).map_err(PppRowError::into_fixed)?;
274 let binding = AmbiguityBinding::Held { values: &fixed_m };
275 let rows = build_rows(ctx, epochs, &binding, &state).map_err(PppRowError::into_fixed)?;
276 let covariance = ppp_position_covariance(
277 &rows,
278 PppNormalLayout::new(epochs.len(), ztd_unknown_count(ctx.tropo), 0),
279 state.position_m,
280 )?;
281 let code: Vec<f64> = residuals.iter().map(|r| r.code_m).collect();
282 let phase: Vec<f64> = residuals.iter().map(|r| r.phase_m).collect();
283 Ok(FixedSolution {
284 position_m: state.position_m,
285 position_covariance: covariance.scaled,
286 formal_position_covariance: covariance.formal,
287 posterior_variance_factor: covariance.posterior_variance_factor,
288 position_covariance_scale_factor: covariance.covariance_scale_factor,
289 epoch_clocks_m: state.clocks_m,
290 fixed_ambiguities_cycles: search.fixed_cycles,
291 fixed_ambiguities_m: fixed_m,
292 ztd_residual_m: if estimates_ztd(ctx.tropo) {
293 Some(state.ztd_m)
294 } else {
295 None
296 },
297 float_solution,
298 residuals_m: residuals.clone(),
299 used_sats: search
300 .order
301 .into_iter()
302 .map(AmbiguityId::into_string)
303 .collect(),
304 iterations,
305 converged,
306 status,
307 code_rms_m: rms(&code),
308 phase_rms_m: rms(&phase),
309 weighted_rms_m: weighted_rms(&residuals, ctx.weights),
310 integer: search.integer,
311 })
312}
313
314fn fixed_state_from_float(solution: &FloatSolution) -> FloatState {
315 FloatState {
316 position_m: solution.position_m,
317 clocks_m: solution.epoch_clocks_m.clone(),
318 ambiguities_m: BTreeMap::new(),
319 ztd_m: solution.ztd_residual_m.unwrap_or(0.0),
320 }
321}
322
323fn float_ambiguities_cycles(
324 solution: &FloatSolution,
325 wavelengths_m: &BTreeMap<String, f64>,
326 offsets_m: &BTreeMap<String, f64>,
327) -> Result<BTreeMap<String, f64>, FixedSolveError> {
328 let mut out = BTreeMap::new();
329 for sat in &solution.used_sats {
330 let wavelength = wavelengths_m
331 .get(sat)
332 .copied()
333 .ok_or_else(|| FixedSolveError::MissingWavelength(sat.clone()))?;
334 let offset = offsets_m
335 .get(sat)
336 .copied()
337 .ok_or_else(|| FixedSolveError::MissingOffset(sat.clone()))?;
338 let ambiguity_m = solution.ambiguities_m.get(sat).copied().ok_or_else(|| {
339 FixedSolveError::Float(FloatSolveError::MissingAmbiguity(sat.clone()))
340 })?;
341 out.insert(sat.clone(), (ambiguity_m - offset) / wavelength);
342 }
343 Ok(out)
344}
345
346fn fixed_ambiguities_m(
347 fixed_cycles: &BTreeMap<String, i64>,
348 wavelengths_m: &BTreeMap<String, f64>,
349 offsets_m: &BTreeMap<String, f64>,
350) -> Result<BTreeMap<String, f64>, FixedSolveError> {
351 let mut out = BTreeMap::new();
352 for (sat, cycles) in fixed_cycles {
353 let wavelength = wavelengths_m
354 .get(sat)
355 .copied()
356 .ok_or_else(|| FixedSolveError::MissingWavelength(sat.clone()))?;
357 let offset = offsets_m
358 .get(sat)
359 .copied()
360 .ok_or_else(|| FixedSolveError::MissingOffset(sat.clone()))?;
361 out.insert(sat.clone(), offset + *cycles as f64 * wavelength);
362 }
363 Ok(out)
364}
365
366fn ambiguity_covariance_cycles(
367 source: &dyn ObservableEphemerisSource,
368 epochs: &[FloatEpoch],
369 ambiguity_ids: &[AmbiguityId],
370 float_solution: &FloatSolution,
371 config: &FixedSolveConfig,
372) -> Result<Vec<Vec<f64>>, FixedSolveError> {
373 let state = state_from_solution(float_solution, &FloatState::default_for_epochs(epochs));
374 let layout = PppNormalLayout::new(
375 epochs.len(),
376 ztd_unknown_count(config.tropo),
377 ambiguity_ids.len(),
378 );
379 let start = layout.reduced_ambiguity_offset();
380 let ctx = ModelContext {
381 source,
382 weights: config.weights,
383 tropo: config.tropo,
384 corrections: &config.corrections,
385 normal: NormalRecipe::PppDenseLastTie,
388 };
389 let binding = AmbiguityBinding::Estimated {
390 ids: ambiguity_ids,
391 values: &state.ambiguities_m,
392 };
393 let rows = build_rows(ctx, epochs, &binding, &state)
394 .map_err(|e| FixedSolveError::from(e.into_float()))?;
395 let (normal, _rhs) = clock_eliminated_normal_equations(&rows, layout)?;
396 let covariance_m = ambiguity_covariance_from_normal(&normal, start, ambiguity_ids.len())?;
397 let mut covariance_cycles = vec![vec![0.0; ambiguity_ids.len()]; ambiguity_ids.len()];
398 for i in 0..ambiguity_ids.len() {
399 let lambda_i = config
400 .ambiguity
401 .wavelengths_m
402 .get(ambiguity_ids[i].as_str())
403 .copied()
404 .ok_or_else(|| {
405 FixedSolveError::MissingWavelength(ambiguity_ids[i].as_str().to_string())
406 })?;
407 for j in 0..ambiguity_ids.len() {
408 let lambda_j = config
409 .ambiguity
410 .wavelengths_m
411 .get(ambiguity_ids[j].as_str())
412 .copied()
413 .ok_or_else(|| {
414 FixedSolveError::MissingWavelength(ambiguity_ids[j].as_str().to_string())
415 })?;
416 covariance_cycles[i][j] = covariance_m[i][j] / (lambda_i * lambda_j);
417 }
418 }
419 Ok(covariance_cycles)
420}