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