use super::recipe::{
AmbiguityIdPolicy, EstimationRecipe, ReferenceTarget, ScreenKind, StrategyId, Technique,
};
use crate::observables::ObservableEphemerisSource;
use crate::precise_positioning::{
FixedSolution, FixedSolveConfig, FixedSolveError, FloatEpoch, FloatSolution, FloatSolveConfig,
FloatSolveError as PppFloatSolveError, FloatState,
};
use crate::rtk_filter::{
AmbiguitySet, Epoch, FloatBaselineSolution, FloatSolveError as RtkFloatSolveError,
FloatSolveOpts, MeasModel, ReceiverAntennaCorrections, ValidatedFixedBaselineSolution,
ValidatedFixedSolveError, ValidatedFixedSolveOpts,
};
use crate::spp::{EphemerisSource, ReceiverSolution, SolveInputs, SolvePolicy, SolvePolicyError};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct EstimateOptions {
pub strategy: StrategyId,
}
impl EstimateOptions {
pub const fn new(strategy: StrategyId) -> Self {
Self { strategy }
}
}
pub enum EstimateInput<'a> {
Spp {
eph: &'a dyn EphemerisSource,
inputs: &'a SolveInputs,
with_geodetic: bool,
policy: SolvePolicy,
},
RtkFloat {
epochs: &'a [Epoch],
base: [f64; 3],
ambiguity_ids: &'a [String],
initial_baseline_m: [f64; 3],
model: &'a MeasModel,
opts: FloatSolveOpts,
receiver_antenna_corrections: Option<&'a ReceiverAntennaCorrections>,
},
RtkFixed {
epochs: &'a [Epoch],
base: [f64; 3],
initial_ambiguities: AmbiguitySet<'a>,
initial_baseline_m: [f64; 3],
model: &'a MeasModel,
opts: ValidatedFixedSolveOpts,
receiver_antenna_corrections: Option<&'a ReceiverAntennaCorrections>,
},
PppFloat {
source: &'a dyn ObservableEphemerisSource,
epochs: &'a [FloatEpoch],
initial_state: FloatState,
config: FloatSolveConfig,
},
PppFixed {
source: &'a dyn ObservableEphemerisSource,
epochs: &'a [FloatEpoch],
float_solution: FloatSolution,
config: FixedSolveConfig,
},
}
impl EstimateInput<'_> {
pub fn technique(&self) -> Technique {
match self {
Self::Spp { .. } => Technique::Spp,
Self::RtkFloat { .. } | Self::RtkFixed { .. } => Technique::Rtk,
Self::PppFloat { .. } | Self::PppFixed { .. } => Technique::Ppp,
}
}
}
#[derive(Debug, Clone)]
pub enum EstimateOutput {
Spp(Box<ReceiverSolution>),
RtkFloat(Box<FloatBaselineSolution>),
RtkFixed(Box<ValidatedFixedBaselineSolution>),
PppFloat(Box<FloatSolution>),
PppFixed(Box<FixedSolution>),
}
#[derive(Debug)]
pub enum EstimateError {
TechniqueMismatch {
strategy: Technique,
input: Technique,
},
IncompatibleTarget {
technique: Technique,
target: ReferenceTarget,
},
CanonicalUnavailable {
technique: Technique,
},
Spp(SolvePolicyError),
RtkFloat(RtkFloatSolveError),
RtkFixed(ValidatedFixedSolveError),
PppFloat(PppFloatSolveError),
PppFixed(FixedSolveError),
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ResolvedStrategy {
pub id: StrategyId,
pub technique: Technique,
pub recipe: EstimationRecipe,
pub screens: &'static [ScreenKind],
}
impl ResolvedStrategy {
pub fn resolve(id: StrategyId) -> Result<Self, EstimateError> {
match id {
StrategyId::Reference { technique, target } => {
let recipe = EstimationRecipe::for_reference(technique, target)
.ok_or(EstimateError::IncompatibleTarget { technique, target })?;
Ok(Self {
id,
technique,
recipe,
screens: screens_for(technique),
})
}
StrategyId::Canonical { technique } => {
let recipe = EstimationRecipe::for_canonical(technique)
.ok_or(EstimateError::CanonicalUnavailable { technique })?;
Ok(Self {
id,
technique,
recipe,
screens: screens_for(technique),
})
}
}
}
pub fn ambiguity_id_policy(
&self,
ratio_threshold: f64,
partial_min_ambiguities: usize,
) -> Option<AmbiguityIdPolicy> {
match self.technique {
Technique::Spp => None,
Technique::Rtk => Some(AmbiguityIdPolicy::rtk_static(
ratio_threshold,
partial_min_ambiguities,
)),
Technique::Ppp => Some(AmbiguityIdPolicy::ppp(ratio_threshold)),
}
}
}
const fn screens_for(technique: Technique) -> &'static [ScreenKind] {
match technique {
Technique::Spp => &[ScreenKind::RaimChiSquare],
Technique::Rtk => &[
ScreenKind::RtkFixedResidualValidation,
ScreenKind::RtkSequentialInnovation,
],
Technique::Ppp => &[ScreenKind::PppFloatLeaveOneOut],
}
}
pub fn estimate(
input: EstimateInput<'_>,
options: EstimateOptions,
) -> Result<EstimateOutput, EstimateError> {
let resolved = ResolvedStrategy::resolve(options.strategy)?;
let input_technique = input.technique();
if resolved.technique != input_technique {
return Err(EstimateError::TechniqueMismatch {
strategy: resolved.technique,
input: input_technique,
});
}
match input {
EstimateInput::Spp {
eph,
inputs,
with_geodetic,
policy,
} => crate::spp::run(&resolved.recipe, eph, inputs, with_geodetic, policy)
.map(|s| EstimateOutput::Spp(Box::new(s)))
.map_err(EstimateError::Spp),
EstimateInput::RtkFloat {
epochs,
base,
ambiguity_ids,
initial_baseline_m,
model,
opts,
receiver_antenna_corrections,
} => crate::rtk_filter::run_float(
&resolved.recipe,
crate::rtk_filter::MeasContext::new(base, model, receiver_antenna_corrections),
epochs,
ambiguity_ids,
initial_baseline_m,
opts,
)
.map(|s| EstimateOutput::RtkFloat(Box::new(s)))
.map_err(EstimateError::RtkFloat),
EstimateInput::RtkFixed {
epochs,
base,
initial_ambiguities,
initial_baseline_m,
model,
opts,
receiver_antenna_corrections,
} => crate::rtk_filter::run_fixed_validated(
&resolved.recipe,
crate::rtk_filter::MeasContext::new(base, model, receiver_antenna_corrections),
epochs,
initial_ambiguities,
initial_baseline_m,
opts,
)
.map(|s| EstimateOutput::RtkFixed(Box::new(s)))
.map_err(EstimateError::RtkFixed),
EstimateInput::PppFloat {
source,
epochs,
initial_state,
config,
} => crate::precise_positioning::run_float_epochs(
&resolved.recipe,
source,
epochs,
initial_state,
config,
)
.map(|s| EstimateOutput::PppFloat(Box::new(s)))
.map_err(EstimateError::PppFloat),
EstimateInput::PppFixed {
source,
epochs,
float_solution,
config,
} => crate::precise_positioning::run_fixed_from_float(
&resolved.recipe,
source,
epochs,
float_solution,
config,
)
.map(|s| EstimateOutput::PppFixed(Box::new(s)))
.map_err(EstimateError::PppFixed),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::estimation::recipe::{ReferenceTarget, ResidualNormRecipe};
#[test]
fn input_technique_matches_each_variant() {
assert_eq!(
screens_for(Technique::Rtk),
&[
ScreenKind::RtkFixedResidualValidation,
ScreenKind::RtkSequentialInnovation,
]
);
assert_eq!(screens_for(Technique::Spp), &[ScreenKind::RaimChiSquare]);
assert_eq!(
screens_for(Technique::Ppp),
&[ScreenKind::PppFloatLeaveOneOut]
);
}
#[test]
fn resolve_reference_strategies_to_their_recipe_and_screens() {
let spp = ResolvedStrategy::resolve(StrategyId::spp_reference()).unwrap();
assert_eq!(spp.technique, Technique::Spp);
assert_eq!(spp.recipe, EstimationRecipe::spp());
assert_eq!(spp.screens, &[ScreenKind::RaimChiSquare]);
assert!(spp.ambiguity_id_policy(3.0, 1).is_none());
let rtk = ResolvedStrategy::resolve(StrategyId::rtk_reference()).unwrap();
assert_eq!(rtk.technique, Technique::Rtk);
assert_eq!(rtk.recipe, EstimationRecipe::rtk());
let rtk_policy = rtk.ambiguity_id_policy(3.0, 4).unwrap();
assert_eq!(rtk_policy, AmbiguityIdPolicy::rtk_static(3.0, 4));
let ppp = ResolvedStrategy::resolve(StrategyId::ppp_reference()).unwrap();
assert_eq!(ppp.technique, Technique::Ppp);
assert_eq!(ppp.recipe, EstimationRecipe::ppp());
assert_eq!(ppp.screens, &[ScreenKind::PppFloatLeaveOneOut]);
let ppp_policy = ppp.ambiguity_id_policy(2.5, 0).unwrap();
assert_eq!(ppp_policy, AmbiguityIdPolicy::ppp(2.5));
}
#[test]
fn each_resolved_strategy_screen_uses_its_own_residual_norm() {
let rtk = ResolvedStrategy::resolve(StrategyId::rtk_reference()).unwrap();
assert_eq!(
rtk.screens
.iter()
.map(|screen| screen.residual_norm())
.collect::<Vec<_>>(),
vec![
Some(ResidualNormRecipe::RtkInverseSigmaResidual),
Some(ResidualNormRecipe::RtkInverseVarianceInnovation),
]
);
let ppp = ResolvedStrategy::resolve(StrategyId::ppp_reference()).unwrap();
assert_eq!(
ppp.screens[0].residual_norm(),
Some(ResidualNormRecipe::PppInverseSigmaMagnitude)
);
let spp = ResolvedStrategy::resolve(StrategyId::spp_reference()).unwrap();
assert_eq!(spp.screens[0].residual_norm(), None);
}
#[test]
fn resolve_owned_deterministic_spp_selects_the_owned_solver() {
use crate::estimation::recipe::SolverRecipe;
let owned = ResolvedStrategy::resolve(StrategyId::spp_owned_deterministic()).unwrap();
assert_eq!(owned.technique, Technique::Spp);
assert_eq!(owned.recipe.solver, SolverRecipe::OwnedDeterministicTrf);
assert_eq!(owned.recipe, EstimationRecipe::spp_owned_deterministic());
assert_eq!(owned.screens, &[ScreenKind::RaimChiSquare]);
}
#[test]
fn resolve_rejects_incompatible_technique_target_pairs() {
for (technique, target) in [
(Technique::Spp, ReferenceTarget::Rtklib),
(Technique::Spp, ReferenceTarget::Scipy),
(Technique::Rtk, ReferenceTarget::OwnedDeterministic),
(Technique::Ppp, ReferenceTarget::Skyfield),
] {
let err =
ResolvedStrategy::resolve(StrategyId::Reference { technique, target }).unwrap_err();
match err {
EstimateError::IncompatibleTarget {
technique: t,
target: g,
} => {
assert_eq!(t, technique);
assert_eq!(g, target);
}
other => {
panic!("{technique:?} + {target:?} should be IncompatibleTarget, got {other:?}")
}
}
}
}
#[test]
fn canonical_spp_resolves_to_the_canonical_recipe() {
let resolved = ResolvedStrategy::resolve(StrategyId::Canonical {
technique: Technique::Spp,
})
.expect("canonical SPP resolves");
assert_eq!(resolved.technique, Technique::Spp);
assert_eq!(resolved.recipe, EstimationRecipe::canonical_spp());
assert_eq!(resolved.screens, &[ScreenKind::RaimChiSquare]);
assert!(resolved.ambiguity_id_policy(3.0, 1).is_none());
}
#[test]
fn canonical_rtk_resolves_to_the_canonical_recipe() {
let resolved = ResolvedStrategy::resolve(StrategyId::Canonical {
technique: Technique::Rtk,
})
.expect("canonical RTK resolves");
assert_eq!(resolved.technique, Technique::Rtk);
assert_eq!(resolved.recipe, EstimationRecipe::canonical_rtk());
assert_eq!(
resolved.recipe.normal,
crate::estimation::recipe::NormalRecipe::CanonicalSquareRoot
);
assert_eq!(
resolved.recipe.solver,
crate::estimation::recipe::SolverRecipe::OwnedDeterministicCholesky
);
}
#[test]
fn canonical_ppp_resolves_to_the_canonical_recipe() {
let resolved = ResolvedStrategy::resolve(StrategyId::Canonical {
technique: Technique::Ppp,
})
.expect("canonical PPP resolves");
assert_eq!(resolved.technique, Technique::Ppp);
assert_eq!(resolved.recipe, EstimationRecipe::canonical_ppp());
assert_eq!(
resolved.recipe.normal,
crate::estimation::recipe::NormalRecipe::CanonicalSquareRoot
);
assert_eq!(
resolved.recipe.solver,
crate::estimation::recipe::SolverRecipe::OwnedDeterministicCholesky
);
assert_eq!(resolved.screens, &[ScreenKind::PppFloatLeaveOneOut]);
let policy = resolved.ambiguity_id_policy(2.5, 0).unwrap();
assert_eq!(policy, AmbiguityIdPolicy::ppp(2.5));
}
#[test]
fn default_options_select_spp_reference() {
let resolved = ResolvedStrategy::resolve(EstimateOptions::default().strategy).unwrap();
assert_eq!(
resolved.id,
StrategyId::Reference {
technique: Technique::Spp,
target: ReferenceTarget::Skyfield,
}
);
assert_eq!(resolved.recipe, EstimationRecipe::spp());
}
}