1use super::recipe::{
43 AmbiguityIdPolicy, EstimationRecipe, ReferenceTarget, ScreenKind, StrategyId, Technique,
44};
45use crate::observables::ObservableEphemerisSource;
46use crate::precise_positioning::{
47 FixedSolution, FixedSolveConfig, FixedSolveError, FloatEpoch, FloatSolution, FloatSolveConfig,
48 FloatSolveError as PppFloatSolveError, FloatState,
49};
50use crate::rtk_filter::{
51 AmbiguitySet, Epoch, FloatBaselineSolution, FloatSolveError as RtkFloatSolveError,
52 FloatSolveOpts, MeasModel, ReceiverAntennaCorrections, ValidatedFixedBaselineSolution,
53 ValidatedFixedSolveError, ValidatedFixedSolveOpts,
54};
55use crate::spp::{EphemerisSource, ReceiverSolution, SolveInputs, SolvePolicy, SolvePolicyError};
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
60pub struct EstimateOptions {
61 pub strategy: StrategyId,
62}
63
64impl EstimateOptions {
65 pub const fn new(strategy: StrategyId) -> Self {
67 Self { strategy }
68 }
69}
70
71#[allow(clippy::large_enum_variant)]
76pub enum EstimateInput<'a> {
77 Spp {
80 eph: &'a dyn EphemerisSource,
81 inputs: &'a SolveInputs,
82 with_geodetic: bool,
83 policy: SolvePolicy,
84 },
85 RtkFloat {
87 epochs: &'a [Epoch],
88 base: [f64; 3],
89 ambiguity_ids: &'a [String],
90 initial_baseline_m: [f64; 3],
91 model: &'a MeasModel,
92 opts: FloatSolveOpts,
93 receiver_antenna_corrections: Option<&'a ReceiverAntennaCorrections>,
94 },
95 RtkFixed {
98 epochs: &'a [Epoch],
99 base: [f64; 3],
100 initial_ambiguities: AmbiguitySet<'a>,
101 initial_baseline_m: [f64; 3],
102 model: &'a MeasModel,
103 opts: ValidatedFixedSolveOpts,
104 receiver_antenna_corrections: Option<&'a ReceiverAntennaCorrections>,
105 },
106 PppFloat {
109 source: &'a dyn ObservableEphemerisSource,
110 epochs: &'a [FloatEpoch],
111 initial_state: FloatState,
112 config: FloatSolveConfig,
113 },
114 PppFixed {
117 source: &'a dyn ObservableEphemerisSource,
118 epochs: &'a [FloatEpoch],
119 float_solution: FloatSolution,
120 config: FixedSolveConfig,
121 },
122}
123
124impl EstimateInput<'_> {
125 pub fn technique(&self) -> Technique {
127 match self {
128 Self::Spp { .. } => Technique::Spp,
129 Self::RtkFloat { .. } | Self::RtkFixed { .. } => Technique::Rtk,
130 Self::PppFloat { .. } | Self::PppFixed { .. } => Technique::Ppp,
131 }
132 }
133}
134
135#[derive(Debug, Clone)]
140pub enum EstimateOutput {
141 Spp(Box<ReceiverSolution>),
142 RtkFloat(Box<FloatBaselineSolution>),
143 RtkFixed(Box<ValidatedFixedBaselineSolution>),
144 PppFloat(Box<FloatSolution>),
145 PppFixed(Box<FixedSolution>),
146}
147
148#[derive(Debug)]
151pub enum EstimateError {
152 TechniqueMismatch {
155 strategy: Technique,
156 input: Technique,
157 },
158 IncompatibleTarget {
163 technique: Technique,
164 target: ReferenceTarget,
165 },
166 CanonicalUnavailable {
171 technique: Technique,
172 },
173 Spp(SolvePolicyError),
174 RtkFloat(RtkFloatSolveError),
175 RtkFixed(ValidatedFixedSolveError),
176 PppFloat(PppFloatSolveError),
177 PppFixed(FixedSolveError),
178}
179
180#[derive(Debug, Clone, Copy, PartialEq)]
185pub struct ResolvedStrategy {
186 pub id: StrategyId,
187 pub technique: Technique,
188 pub recipe: EstimationRecipe,
189 pub screens: &'static [ScreenKind],
191}
192
193impl ResolvedStrategy {
194 pub fn resolve(id: StrategyId) -> Result<Self, EstimateError> {
202 match id {
203 StrategyId::Reference { technique, target } => {
204 let recipe = EstimationRecipe::for_reference(technique, target)
205 .ok_or(EstimateError::IncompatibleTarget { technique, target })?;
206 Ok(Self {
207 id,
208 technique,
209 recipe,
210 screens: screens_for(technique),
211 })
212 }
213 StrategyId::Canonical { technique } => {
214 let recipe = EstimationRecipe::for_canonical(technique)
215 .ok_or(EstimateError::CanonicalUnavailable { technique })?;
216 Ok(Self {
217 id,
218 technique,
219 recipe,
220 screens: screens_for(technique),
221 })
222 }
223 }
224 }
225
226 pub fn ambiguity_id_policy(
230 &self,
231 ratio_threshold: f64,
232 partial_min_ambiguities: usize,
233 ) -> Option<AmbiguityIdPolicy> {
234 match self.technique {
235 Technique::Spp => None,
236 Technique::Rtk => Some(AmbiguityIdPolicy::rtk_static(
237 ratio_threshold,
238 partial_min_ambiguities,
239 )),
240 Technique::Ppp => Some(AmbiguityIdPolicy::ppp(ratio_threshold)),
241 }
242 }
243}
244
245const fn screens_for(technique: Technique) -> &'static [ScreenKind] {
249 match technique {
250 Technique::Spp => &[ScreenKind::RaimChiSquare],
251 Technique::Rtk => &[
252 ScreenKind::RtkFixedResidualValidation,
253 ScreenKind::RtkSequentialInnovation,
254 ],
255 Technique::Ppp => &[ScreenKind::PppFloatLeaveOneOut],
256 }
257}
258
259pub fn estimate(
268 input: EstimateInput<'_>,
269 options: EstimateOptions,
270) -> Result<EstimateOutput, EstimateError> {
271 let resolved = ResolvedStrategy::resolve(options.strategy)?;
272 let input_technique = input.technique();
273 if resolved.technique != input_technique {
274 return Err(EstimateError::TechniqueMismatch {
275 strategy: resolved.technique,
276 input: input_technique,
277 });
278 }
279
280 match input {
281 EstimateInput::Spp {
282 eph,
283 inputs,
284 with_geodetic,
285 policy,
286 } => crate::spp::run(&resolved.recipe, eph, inputs, with_geodetic, policy)
287 .map(|s| EstimateOutput::Spp(Box::new(s)))
288 .map_err(EstimateError::Spp),
289 EstimateInput::RtkFloat {
290 epochs,
291 base,
292 ambiguity_ids,
293 initial_baseline_m,
294 model,
295 opts,
296 receiver_antenna_corrections,
297 } => crate::rtk_filter::run_float(
298 &resolved.recipe,
299 crate::rtk_filter::MeasContext::new(base, model, receiver_antenna_corrections),
300 epochs,
301 ambiguity_ids,
302 initial_baseline_m,
303 opts,
304 )
305 .map(|s| EstimateOutput::RtkFloat(Box::new(s)))
306 .map_err(EstimateError::RtkFloat),
307 EstimateInput::RtkFixed {
308 epochs,
309 base,
310 initial_ambiguities,
311 initial_baseline_m,
312 model,
313 opts,
314 receiver_antenna_corrections,
315 } => crate::rtk_filter::run_fixed_validated(
316 &resolved.recipe,
317 crate::rtk_filter::MeasContext::new(base, model, receiver_antenna_corrections),
318 epochs,
319 initial_ambiguities,
320 initial_baseline_m,
321 opts,
322 )
323 .map(|s| EstimateOutput::RtkFixed(Box::new(s)))
324 .map_err(EstimateError::RtkFixed),
325 EstimateInput::PppFloat {
326 source,
327 epochs,
328 initial_state,
329 config,
330 } => crate::precise_positioning::run_float_epochs(
331 &resolved.recipe,
332 source,
333 epochs,
334 initial_state,
335 config,
336 )
337 .map(|s| EstimateOutput::PppFloat(Box::new(s)))
338 .map_err(EstimateError::PppFloat),
339 EstimateInput::PppFixed {
340 source,
341 epochs,
342 float_solution,
343 config,
344 } => crate::precise_positioning::run_fixed_from_float(
345 &resolved.recipe,
346 source,
347 epochs,
348 float_solution,
349 config,
350 )
351 .map(|s| EstimateOutput::PppFixed(Box::new(s)))
352 .map_err(EstimateError::PppFixed),
353 }
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359 use crate::estimation::recipe::{ReferenceTarget, ResidualNormRecipe};
360
361 #[test]
362 fn input_technique_matches_each_variant() {
363 assert_eq!(
365 screens_for(Technique::Rtk),
366 &[
367 ScreenKind::RtkFixedResidualValidation,
368 ScreenKind::RtkSequentialInnovation,
369 ]
370 );
371 assert_eq!(screens_for(Technique::Spp), &[ScreenKind::RaimChiSquare]);
372 assert_eq!(
373 screens_for(Technique::Ppp),
374 &[ScreenKind::PppFloatLeaveOneOut]
375 );
376 }
377
378 #[test]
379 fn resolve_reference_strategies_to_their_recipe_and_screens() {
380 let spp = ResolvedStrategy::resolve(StrategyId::spp_reference()).unwrap();
381 assert_eq!(spp.technique, Technique::Spp);
382 assert_eq!(spp.recipe, EstimationRecipe::spp());
383 assert_eq!(spp.screens, &[ScreenKind::RaimChiSquare]);
384 assert!(spp.ambiguity_id_policy(3.0, 1).is_none());
385
386 let rtk = ResolvedStrategy::resolve(StrategyId::rtk_reference()).unwrap();
387 assert_eq!(rtk.technique, Technique::Rtk);
388 assert_eq!(rtk.recipe, EstimationRecipe::rtk());
389 let rtk_policy = rtk.ambiguity_id_policy(3.0, 4).unwrap();
390 assert_eq!(rtk_policy, AmbiguityIdPolicy::rtk_static(3.0, 4));
391
392 let ppp = ResolvedStrategy::resolve(StrategyId::ppp_reference()).unwrap();
393 assert_eq!(ppp.technique, Technique::Ppp);
394 assert_eq!(ppp.recipe, EstimationRecipe::ppp());
395 assert_eq!(ppp.screens, &[ScreenKind::PppFloatLeaveOneOut]);
396 let ppp_policy = ppp.ambiguity_id_policy(2.5, 0).unwrap();
397 assert_eq!(ppp_policy, AmbiguityIdPolicy::ppp(2.5));
398 }
399
400 #[test]
401 fn each_resolved_strategy_screen_uses_its_own_residual_norm() {
402 let rtk = ResolvedStrategy::resolve(StrategyId::rtk_reference()).unwrap();
407 assert_eq!(
408 rtk.screens
409 .iter()
410 .map(|screen| screen.residual_norm())
411 .collect::<Vec<_>>(),
412 vec![
413 Some(ResidualNormRecipe::RtkInverseSigmaResidual),
414 Some(ResidualNormRecipe::RtkInverseVarianceInnovation),
415 ]
416 );
417 let ppp = ResolvedStrategy::resolve(StrategyId::ppp_reference()).unwrap();
418 assert_eq!(
419 ppp.screens[0].residual_norm(),
420 Some(ResidualNormRecipe::PppInverseSigmaMagnitude)
421 );
422 let spp = ResolvedStrategy::resolve(StrategyId::spp_reference()).unwrap();
423 assert_eq!(spp.screens[0].residual_norm(), None);
424 }
425
426 #[test]
427 fn resolve_owned_deterministic_spp_selects_the_owned_solver() {
428 use crate::estimation::recipe::SolverRecipe;
429
430 let owned = ResolvedStrategy::resolve(StrategyId::spp_owned_deterministic()).unwrap();
431 assert_eq!(owned.technique, Technique::Spp);
432 assert_eq!(owned.recipe.solver, SolverRecipe::OwnedDeterministicTrf);
433 assert_eq!(owned.recipe, EstimationRecipe::spp_owned_deterministic());
434 assert_eq!(owned.screens, &[ScreenKind::RaimChiSquare]);
436 }
437
438 #[test]
439 fn resolve_rejects_incompatible_technique_target_pairs() {
440 for (technique, target) in [
441 (Technique::Spp, ReferenceTarget::Rtklib),
442 (Technique::Spp, ReferenceTarget::Scipy),
443 (Technique::Rtk, ReferenceTarget::OwnedDeterministic),
444 (Technique::Ppp, ReferenceTarget::Skyfield),
445 ] {
446 let err =
447 ResolvedStrategy::resolve(StrategyId::Reference { technique, target }).unwrap_err();
448 match err {
449 EstimateError::IncompatibleTarget {
450 technique: t,
451 target: g,
452 } => {
453 assert_eq!(t, technique);
454 assert_eq!(g, target);
455 }
456 other => {
457 panic!("{technique:?} + {target:?} should be IncompatibleTarget, got {other:?}")
458 }
459 }
460 }
461 }
462
463 #[test]
464 fn canonical_spp_resolves_to_the_canonical_recipe() {
465 let resolved = ResolvedStrategy::resolve(StrategyId::Canonical {
466 technique: Technique::Spp,
467 })
468 .expect("canonical SPP resolves");
469 assert_eq!(resolved.technique, Technique::Spp);
470 assert_eq!(resolved.recipe, EstimationRecipe::canonical_spp());
471 assert_eq!(resolved.screens, &[ScreenKind::RaimChiSquare]);
473 assert!(resolved.ambiguity_id_policy(3.0, 1).is_none());
474 }
475
476 #[test]
477 fn canonical_rtk_resolves_to_the_canonical_recipe() {
478 let resolved = ResolvedStrategy::resolve(StrategyId::Canonical {
479 technique: Technique::Rtk,
480 })
481 .expect("canonical RTK resolves");
482 assert_eq!(resolved.technique, Technique::Rtk);
483 assert_eq!(resolved.recipe, EstimationRecipe::canonical_rtk());
484 assert_eq!(
487 resolved.recipe.normal,
488 crate::estimation::recipe::NormalRecipe::CanonicalSquareRoot
489 );
490 assert_eq!(
491 resolved.recipe.solver,
492 crate::estimation::recipe::SolverRecipe::OwnedDeterministicCholesky
493 );
494 }
495
496 #[test]
497 fn canonical_ppp_resolves_to_the_canonical_recipe() {
498 let resolved = ResolvedStrategy::resolve(StrategyId::Canonical {
499 technique: Technique::Ppp,
500 })
501 .expect("canonical PPP resolves");
502 assert_eq!(resolved.technique, Technique::Ppp);
503 assert_eq!(resolved.recipe, EstimationRecipe::canonical_ppp());
504 assert_eq!(
507 resolved.recipe.normal,
508 crate::estimation::recipe::NormalRecipe::CanonicalSquareRoot
509 );
510 assert_eq!(
511 resolved.recipe.solver,
512 crate::estimation::recipe::SolverRecipe::OwnedDeterministicCholesky
513 );
514 assert_eq!(resolved.screens, &[ScreenKind::PppFloatLeaveOneOut]);
516 let policy = resolved.ambiguity_id_policy(2.5, 0).unwrap();
517 assert_eq!(policy, AmbiguityIdPolicy::ppp(2.5));
518 }
519
520 #[test]
521 fn default_options_select_spp_reference() {
522 let resolved = ResolvedStrategy::resolve(EstimateOptions::default().strategy).unwrap();
523 assert_eq!(
524 resolved.id,
525 StrategyId::Reference {
526 technique: Technique::Spp,
527 target: ReferenceTarget::Skyfield,
528 }
529 );
530 assert_eq!(resolved.recipe, EstimationRecipe::spp());
531 }
532}