1use super::recipe::{
39 AmbiguityIdPolicy, EstimationRecipe, ReferenceTarget, ScreenKind, StrategyId, Technique,
40};
41use crate::observables::ObservableEphemerisSource;
42use crate::precise_positioning::{
43 FixedSolution, FixedSolveConfig, FixedSolveError, FloatEpoch, FloatSolution, FloatSolveConfig,
44 FloatSolveError as PppFloatSolveError, FloatState,
45};
46use crate::rtk_filter::{
47 AmbiguitySet, Epoch, FloatBaselineSolution, FloatSolveError as RtkFloatSolveError,
48 FloatSolveOpts, MeasModel, ReceiverAntennaCorrections, ValidatedFixedBaselineSolution,
49 ValidatedFixedSolveError, ValidatedFixedSolveOpts,
50};
51use crate::spp::{EphemerisSource, ReceiverSolution, SolveInputs, SolvePolicy, SolvePolicyError};
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
56pub struct EstimateOptions {
57 pub strategy: StrategyId,
58}
59
60impl EstimateOptions {
61 pub const fn new(strategy: StrategyId) -> Self {
63 Self { strategy }
64 }
65}
66
67pub enum EstimateInput<'a> {
72 Spp {
75 eph: &'a dyn EphemerisSource,
76 inputs: &'a SolveInputs,
77 with_geodetic: bool,
78 policy: SolvePolicy,
79 },
80 RtkFloat {
82 epochs: &'a [Epoch],
83 base: [f64; 3],
84 ambiguity_ids: &'a [String],
85 initial_baseline_m: [f64; 3],
86 model: &'a MeasModel,
87 opts: FloatSolveOpts,
88 receiver_antenna_corrections: Option<&'a ReceiverAntennaCorrections>,
89 },
90 RtkFixed {
93 epochs: &'a [Epoch],
94 base: [f64; 3],
95 initial_ambiguities: AmbiguitySet<'a>,
96 initial_baseline_m: [f64; 3],
97 model: &'a MeasModel,
98 opts: ValidatedFixedSolveOpts,
99 receiver_antenna_corrections: Option<&'a ReceiverAntennaCorrections>,
100 },
101 PppFloat {
104 source: &'a dyn ObservableEphemerisSource,
105 epochs: &'a [FloatEpoch],
106 initial_state: FloatState,
107 config: FloatSolveConfig,
108 },
109 PppFixed {
112 source: &'a dyn ObservableEphemerisSource,
113 epochs: &'a [FloatEpoch],
114 float_solution: FloatSolution,
115 config: FixedSolveConfig,
116 },
117}
118
119impl EstimateInput<'_> {
120 pub fn technique(&self) -> Technique {
122 match self {
123 Self::Spp { .. } => Technique::Spp,
124 Self::RtkFloat { .. } | Self::RtkFixed { .. } => Technique::Rtk,
125 Self::PppFloat { .. } | Self::PppFixed { .. } => Technique::Ppp,
126 }
127 }
128}
129
130#[derive(Debug, Clone)]
135pub enum EstimateOutput {
136 Spp(Box<ReceiverSolution>),
137 RtkFloat(Box<FloatBaselineSolution>),
138 RtkFixed(Box<ValidatedFixedBaselineSolution>),
139 PppFloat(Box<FloatSolution>),
140 PppFixed(Box<FixedSolution>),
141}
142
143#[derive(Debug)]
146pub enum EstimateError {
147 TechniqueMismatch {
150 strategy: Technique,
151 input: Technique,
152 },
153 IncompatibleTarget {
158 technique: Technique,
159 target: ReferenceTarget,
160 },
161 CanonicalUnavailable {
166 technique: Technique,
167 },
168 Spp(SolvePolicyError),
169 RtkFloat(RtkFloatSolveError),
170 RtkFixed(ValidatedFixedSolveError),
171 PppFloat(PppFloatSolveError),
172 PppFixed(FixedSolveError),
173}
174
175#[derive(Debug, Clone, Copy, PartialEq)]
180pub struct ResolvedStrategy {
181 pub id: StrategyId,
182 pub technique: Technique,
183 pub recipe: EstimationRecipe,
184 pub screens: &'static [ScreenKind],
186}
187
188impl ResolvedStrategy {
189 pub fn resolve(id: StrategyId) -> Result<Self, EstimateError> {
197 match id {
198 StrategyId::Reference { technique, target } => {
199 let recipe = EstimationRecipe::for_reference(technique, target)
200 .ok_or(EstimateError::IncompatibleTarget { technique, target })?;
201 Ok(Self {
202 id,
203 technique,
204 recipe,
205 screens: screens_for(technique),
206 })
207 }
208 StrategyId::Canonical { technique } => {
209 let recipe = EstimationRecipe::for_canonical(technique)
210 .ok_or(EstimateError::CanonicalUnavailable { technique })?;
211 Ok(Self {
212 id,
213 technique,
214 recipe,
215 screens: screens_for(technique),
216 })
217 }
218 }
219 }
220
221 pub fn ambiguity_id_policy(
225 &self,
226 ratio_threshold: f64,
227 partial_min_ambiguities: usize,
228 ) -> Option<AmbiguityIdPolicy> {
229 match self.technique {
230 Technique::Spp => None,
231 Technique::Rtk => Some(AmbiguityIdPolicy::rtk_static(
232 ratio_threshold,
233 partial_min_ambiguities,
234 )),
235 Technique::Ppp => Some(AmbiguityIdPolicy::ppp(ratio_threshold)),
236 }
237 }
238}
239
240const fn screens_for(technique: Technique) -> &'static [ScreenKind] {
244 match technique {
245 Technique::Spp => &[ScreenKind::RaimChiSquare],
246 Technique::Rtk => &[
247 ScreenKind::RtkFixedResidualValidation,
248 ScreenKind::RtkSequentialInnovation,
249 ],
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 &[
362 ScreenKind::RtkFixedResidualValidation,
363 ScreenKind::RtkSequentialInnovation,
364 ]
365 );
366 assert_eq!(screens_for(Technique::Spp), &[ScreenKind::RaimChiSquare]);
367 assert_eq!(
368 screens_for(Technique::Ppp),
369 &[ScreenKind::PppFloatLeaveOneOut]
370 );
371 }
372
373 #[test]
374 fn resolve_reference_strategies_to_their_recipe_and_screens() {
375 let spp = ResolvedStrategy::resolve(StrategyId::spp_reference()).unwrap();
376 assert_eq!(spp.technique, Technique::Spp);
377 assert_eq!(spp.recipe, EstimationRecipe::spp());
378 assert_eq!(spp.screens, &[ScreenKind::RaimChiSquare]);
379 assert!(spp.ambiguity_id_policy(3.0, 1).is_none());
380
381 let rtk = ResolvedStrategy::resolve(StrategyId::rtk_reference()).unwrap();
382 assert_eq!(rtk.technique, Technique::Rtk);
383 assert_eq!(rtk.recipe, EstimationRecipe::rtk());
384 let rtk_policy = rtk.ambiguity_id_policy(3.0, 4).unwrap();
385 assert_eq!(rtk_policy, AmbiguityIdPolicy::rtk_static(3.0, 4));
386
387 let ppp = ResolvedStrategy::resolve(StrategyId::ppp_reference()).unwrap();
388 assert_eq!(ppp.technique, Technique::Ppp);
389 assert_eq!(ppp.recipe, EstimationRecipe::ppp());
390 assert_eq!(ppp.screens, &[ScreenKind::PppFloatLeaveOneOut]);
391 let ppp_policy = ppp.ambiguity_id_policy(2.5, 0).unwrap();
392 assert_eq!(ppp_policy, AmbiguityIdPolicy::ppp(2.5));
393 }
394
395 #[test]
396 fn each_resolved_strategy_screen_uses_its_own_residual_norm() {
397 let rtk = ResolvedStrategy::resolve(StrategyId::rtk_reference()).unwrap();
402 assert_eq!(
403 rtk.screens
404 .iter()
405 .map(|screen| screen.residual_norm())
406 .collect::<Vec<_>>(),
407 vec![
408 Some(ResidualNormRecipe::RtkInverseSigmaResidual),
409 Some(ResidualNormRecipe::RtkInverseVarianceInnovation),
410 ]
411 );
412 let ppp = ResolvedStrategy::resolve(StrategyId::ppp_reference()).unwrap();
413 assert_eq!(
414 ppp.screens[0].residual_norm(),
415 Some(ResidualNormRecipe::PppInverseSigmaMagnitude)
416 );
417 let spp = ResolvedStrategy::resolve(StrategyId::spp_reference()).unwrap();
418 assert_eq!(spp.screens[0].residual_norm(), None);
419 }
420
421 #[test]
422 fn resolve_owned_deterministic_spp_selects_the_owned_solver() {
423 use crate::estimation::recipe::SolverRecipe;
424
425 let owned = ResolvedStrategy::resolve(StrategyId::spp_owned_deterministic()).unwrap();
426 assert_eq!(owned.technique, Technique::Spp);
427 assert_eq!(owned.recipe.solver, SolverRecipe::OwnedDeterministicTrf);
428 assert_eq!(owned.recipe, EstimationRecipe::spp_owned_deterministic());
429 assert_eq!(owned.screens, &[ScreenKind::RaimChiSquare]);
431 }
432
433 #[test]
434 fn resolve_rejects_incompatible_technique_target_pairs() {
435 for (technique, target) in [
436 (Technique::Spp, ReferenceTarget::Rtklib),
437 (Technique::Spp, ReferenceTarget::Scipy),
438 (Technique::Rtk, ReferenceTarget::OwnedDeterministic),
439 (Technique::Ppp, ReferenceTarget::Skyfield),
440 ] {
441 let err =
442 ResolvedStrategy::resolve(StrategyId::Reference { technique, target }).unwrap_err();
443 match err {
444 EstimateError::IncompatibleTarget {
445 technique: t,
446 target: g,
447 } => {
448 assert_eq!(t, technique);
449 assert_eq!(g, target);
450 }
451 other => {
452 panic!("{technique:?} + {target:?} should be IncompatibleTarget, got {other:?}")
453 }
454 }
455 }
456 }
457
458 #[test]
459 fn canonical_spp_resolves_to_the_canonical_recipe() {
460 let resolved = ResolvedStrategy::resolve(StrategyId::Canonical {
461 technique: Technique::Spp,
462 })
463 .expect("canonical SPP resolves");
464 assert_eq!(resolved.technique, Technique::Spp);
465 assert_eq!(resolved.recipe, EstimationRecipe::canonical_spp());
466 assert_eq!(resolved.screens, &[ScreenKind::RaimChiSquare]);
468 assert!(resolved.ambiguity_id_policy(3.0, 1).is_none());
469 }
470
471 #[test]
472 fn canonical_rtk_resolves_to_the_canonical_recipe() {
473 let resolved = ResolvedStrategy::resolve(StrategyId::Canonical {
474 technique: Technique::Rtk,
475 })
476 .expect("canonical RTK resolves");
477 assert_eq!(resolved.technique, Technique::Rtk);
478 assert_eq!(resolved.recipe, EstimationRecipe::canonical_rtk());
479 assert_eq!(
482 resolved.recipe.normal,
483 crate::estimation::recipe::NormalRecipe::CanonicalSquareRoot
484 );
485 assert_eq!(
486 resolved.recipe.solver,
487 crate::estimation::recipe::SolverRecipe::OwnedDeterministicCholesky
488 );
489 }
490
491 #[test]
492 fn canonical_ppp_resolves_to_the_canonical_recipe() {
493 let resolved = ResolvedStrategy::resolve(StrategyId::Canonical {
494 technique: Technique::Ppp,
495 })
496 .expect("canonical PPP resolves");
497 assert_eq!(resolved.technique, Technique::Ppp);
498 assert_eq!(resolved.recipe, EstimationRecipe::canonical_ppp());
499 assert_eq!(
502 resolved.recipe.normal,
503 crate::estimation::recipe::NormalRecipe::CanonicalSquareRoot
504 );
505 assert_eq!(
506 resolved.recipe.solver,
507 crate::estimation::recipe::SolverRecipe::OwnedDeterministicCholesky
508 );
509 assert_eq!(resolved.screens, &[ScreenKind::PppFloatLeaveOneOut]);
511 let policy = resolved.ambiguity_id_policy(2.5, 0).unwrap();
512 assert_eq!(policy, AmbiguityIdPolicy::ppp(2.5));
513 }
514
515 #[test]
516 fn default_options_select_spp_reference() {
517 let resolved = ResolvedStrategy::resolve(EstimateOptions::default().strategy).unwrap();
518 assert_eq!(
519 resolved.id,
520 StrategyId::Reference {
521 technique: Technique::Spp,
522 target: ReferenceTarget::Skyfield,
523 }
524 );
525 assert_eq!(resolved.recipe, EstimationRecipe::spp());
526 }
527}