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