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