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] {
247 match technique {
248 Technique::Spp => &[ScreenKind::RaimChiSquare],
249 Technique::Rtk => &[ScreenKind::RtkFixedResidualValidation],
250 Technique::Ppp => &[ScreenKind::PppFloatLeaveOneOut],
251 }
252}
253
254pub fn estimate(
263 input: EstimateInput<'_>,
264 options: EstimateOptions,
265) -> Result<EstimateOutput, EstimateError> {
266 let resolved = ResolvedStrategy::resolve(options.strategy)?;
267 let input_technique = input.technique();
268 if resolved.technique != input_technique {
269 return Err(EstimateError::TechniqueMismatch {
270 strategy: resolved.technique,
271 input: input_technique,
272 });
273 }
274
275 match input {
276 EstimateInput::Spp {
277 eph,
278 inputs,
279 with_geodetic,
280 policy,
281 } => crate::spp::run(&resolved.recipe, eph, inputs, with_geodetic, policy)
282 .map(|s| EstimateOutput::Spp(Box::new(s)))
283 .map_err(EstimateError::Spp),
284 EstimateInput::RtkFloat {
285 epochs,
286 base,
287 ambiguity_ids,
288 initial_baseline_m,
289 model,
290 opts,
291 receiver_antenna_corrections,
292 } => crate::rtk_filter::run_float(
293 &resolved.recipe,
294 crate::rtk_filter::MeasContext::new(base, model, receiver_antenna_corrections),
295 epochs,
296 ambiguity_ids,
297 initial_baseline_m,
298 opts,
299 )
300 .map(|s| EstimateOutput::RtkFloat(Box::new(s)))
301 .map_err(EstimateError::RtkFloat),
302 EstimateInput::RtkFixed {
303 epochs,
304 base,
305 initial_ambiguities,
306 initial_baseline_m,
307 model,
308 opts,
309 receiver_antenna_corrections,
310 } => crate::rtk_filter::run_fixed_validated(
311 &resolved.recipe,
312 crate::rtk_filter::MeasContext::new(base, model, receiver_antenna_corrections),
313 epochs,
314 initial_ambiguities,
315 initial_baseline_m,
316 opts,
317 )
318 .map(|s| EstimateOutput::RtkFixed(Box::new(s)))
319 .map_err(EstimateError::RtkFixed),
320 EstimateInput::PppFloat {
321 source,
322 epochs,
323 initial_state,
324 config,
325 } => crate::precise_positioning::run_float_epochs(
326 &resolved.recipe,
327 source,
328 epochs,
329 initial_state,
330 config,
331 )
332 .map(|s| EstimateOutput::PppFloat(Box::new(s)))
333 .map_err(EstimateError::PppFloat),
334 EstimateInput::PppFixed {
335 source,
336 epochs,
337 float_solution,
338 config,
339 } => crate::precise_positioning::run_fixed_from_float(
340 &resolved.recipe,
341 source,
342 epochs,
343 float_solution,
344 config,
345 )
346 .map(|s| EstimateOutput::PppFixed(Box::new(s)))
347 .map_err(EstimateError::PppFixed),
348 }
349}
350
351#[cfg(test)]
352mod tests {
353 use super::*;
354 use crate::estimation::recipe::{ReferenceTarget, ResidualNormRecipe};
355
356 #[test]
357 fn input_technique_matches_each_variant() {
358 assert_eq!(
360 screens_for(Technique::Rtk),
361 &[ScreenKind::RtkFixedResidualValidation]
362 );
363 assert_eq!(screens_for(Technique::Spp), &[ScreenKind::RaimChiSquare]);
364 assert_eq!(
365 screens_for(Technique::Ppp),
366 &[ScreenKind::PppFloatLeaveOneOut]
367 );
368 }
369
370 #[test]
371 fn resolve_reference_strategies_to_their_recipe_and_screens() {
372 let spp = ResolvedStrategy::resolve(StrategyId::spp_reference()).unwrap();
373 assert_eq!(spp.technique, Technique::Spp);
374 assert_eq!(spp.recipe, EstimationRecipe::spp());
375 assert_eq!(spp.screens, &[ScreenKind::RaimChiSquare]);
376 assert!(spp.ambiguity_id_policy(3.0, 1).is_none());
377
378 let rtk = ResolvedStrategy::resolve(StrategyId::rtk_reference()).unwrap();
379 assert_eq!(rtk.technique, Technique::Rtk);
380 assert_eq!(rtk.recipe, EstimationRecipe::rtk());
381 let rtk_policy = rtk.ambiguity_id_policy(3.0, 4).unwrap();
382 assert_eq!(rtk_policy, AmbiguityIdPolicy::rtk_static(3.0, 4));
383
384 let ppp = ResolvedStrategy::resolve(StrategyId::ppp_reference()).unwrap();
385 assert_eq!(ppp.technique, Technique::Ppp);
386 assert_eq!(ppp.recipe, EstimationRecipe::ppp());
387 assert_eq!(ppp.screens, &[ScreenKind::PppFloatLeaveOneOut]);
388 let ppp_policy = ppp.ambiguity_id_policy(2.5, 0).unwrap();
389 assert_eq!(ppp_policy, AmbiguityIdPolicy::ppp(2.5));
390 }
391
392 #[test]
393 fn each_resolved_strategy_screen_uses_its_own_residual_norm() {
394 let rtk = ResolvedStrategy::resolve(StrategyId::rtk_reference()).unwrap();
399 assert_eq!(
400 rtk.screens
401 .iter()
402 .map(|screen| screen.residual_norm())
403 .collect::<Vec<_>>(),
404 vec![Some(ResidualNormRecipe::RtkInverseSigmaResidual)]
405 );
406 let ppp = ResolvedStrategy::resolve(StrategyId::ppp_reference()).unwrap();
407 assert_eq!(
408 ppp.screens[0].residual_norm(),
409 Some(ResidualNormRecipe::PppInverseSigmaMagnitude)
410 );
411 let spp = ResolvedStrategy::resolve(StrategyId::spp_reference()).unwrap();
412 assert_eq!(spp.screens[0].residual_norm(), None);
413 }
414
415 #[test]
416 fn resolve_owned_deterministic_spp_selects_the_owned_solver() {
417 use crate::estimation::recipe::SolverRecipe;
418
419 let owned = ResolvedStrategy::resolve(StrategyId::spp_owned_deterministic()).unwrap();
420 assert_eq!(owned.technique, Technique::Spp);
421 assert_eq!(owned.recipe.solver, SolverRecipe::OwnedDeterministicTrf);
422 assert_eq!(owned.recipe, EstimationRecipe::spp_owned_deterministic());
423 assert_eq!(owned.screens, &[ScreenKind::RaimChiSquare]);
425 }
426
427 #[test]
428 fn resolve_rejects_incompatible_technique_target_pairs() {
429 for (technique, target) in [
430 (Technique::Spp, ReferenceTarget::Rtklib),
431 (Technique::Spp, ReferenceTarget::Scipy),
432 (Technique::Rtk, ReferenceTarget::OwnedDeterministic),
433 (Technique::Ppp, ReferenceTarget::Skyfield),
434 ] {
435 let err =
436 ResolvedStrategy::resolve(StrategyId::Reference { technique, target }).unwrap_err();
437 match err {
438 EstimateError::IncompatibleTarget {
439 technique: t,
440 target: g,
441 } => {
442 assert_eq!(t, technique);
443 assert_eq!(g, target);
444 }
445 other => {
446 panic!("{technique:?} + {target:?} should be IncompatibleTarget, got {other:?}")
447 }
448 }
449 }
450 }
451
452 #[test]
453 fn canonical_spp_resolves_to_the_canonical_recipe() {
454 let resolved = ResolvedStrategy::resolve(StrategyId::Canonical {
455 technique: Technique::Spp,
456 })
457 .expect("canonical SPP resolves");
458 assert_eq!(resolved.technique, Technique::Spp);
459 assert_eq!(resolved.recipe, EstimationRecipe::canonical_spp());
460 assert_eq!(resolved.screens, &[ScreenKind::RaimChiSquare]);
462 assert!(resolved.ambiguity_id_policy(3.0, 1).is_none());
463 }
464
465 #[test]
466 fn canonical_rtk_resolves_to_the_canonical_recipe() {
467 let resolved = ResolvedStrategy::resolve(StrategyId::Canonical {
468 technique: Technique::Rtk,
469 })
470 .expect("canonical RTK resolves");
471 assert_eq!(resolved.technique, Technique::Rtk);
472 assert_eq!(resolved.recipe, EstimationRecipe::canonical_rtk());
473 assert_eq!(
476 resolved.recipe.normal,
477 crate::estimation::recipe::NormalRecipe::CanonicalSquareRoot
478 );
479 assert_eq!(
480 resolved.recipe.solver,
481 crate::estimation::recipe::SolverRecipe::OwnedDeterministicCholesky
482 );
483 }
484
485 #[test]
486 fn canonical_ppp_resolves_to_the_canonical_recipe() {
487 let resolved = ResolvedStrategy::resolve(StrategyId::Canonical {
488 technique: Technique::Ppp,
489 })
490 .expect("canonical PPP resolves");
491 assert_eq!(resolved.technique, Technique::Ppp);
492 assert_eq!(resolved.recipe, EstimationRecipe::canonical_ppp());
493 assert_eq!(
496 resolved.recipe.normal,
497 crate::estimation::recipe::NormalRecipe::CanonicalSquareRoot
498 );
499 assert_eq!(
500 resolved.recipe.solver,
501 crate::estimation::recipe::SolverRecipe::OwnedDeterministicCholesky
502 );
503 assert_eq!(resolved.screens, &[ScreenKind::PppFloatLeaveOneOut]);
505 let policy = resolved.ambiguity_id_policy(2.5, 0).unwrap();
506 assert_eq!(policy, AmbiguityIdPolicy::ppp(2.5));
507 }
508
509 #[test]
510 fn default_options_select_spp_reference() {
511 let resolved = ResolvedStrategy::resolve(EstimateOptions::default().strategy).unwrap();
512 assert_eq!(
513 resolved.id,
514 StrategyId::Reference {
515 technique: Technique::Spp,
516 target: ReferenceTarget::Skyfield,
517 }
518 );
519 assert_eq!(resolved.recipe, EstimationRecipe::spp());
520 }
521}