#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum Technique {
#[default]
Spp,
Rtk,
Ppp,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum ReferenceTarget {
#[default]
Skyfield,
Rtklib,
Scipy,
PppOracle,
OwnedDeterministic,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum StrategyId {
Reference {
technique: Technique,
target: ReferenceTarget,
},
Canonical { technique: Technique },
}
impl Default for StrategyId {
fn default() -> Self {
Self::Reference {
technique: Technique::Spp,
target: ReferenceTarget::Skyfield,
}
}
}
impl StrategyId {
pub const fn spp_reference() -> Self {
Self::Reference {
technique: Technique::Spp,
target: ReferenceTarget::Skyfield,
}
}
pub const fn rtk_reference() -> Self {
Self::Reference {
technique: Technique::Rtk,
target: ReferenceTarget::Rtklib,
}
}
pub const fn ppp_reference() -> Self {
Self::Reference {
technique: Technique::Ppp,
target: ReferenceTarget::PppOracle,
}
}
pub const fn spp_owned_deterministic() -> Self {
Self::Reference {
technique: Technique::Spp,
target: ReferenceTarget::OwnedDeterministic,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum RangeRecipe {
#[default]
SppMeasuredPseudorangeFixedIter,
ObservableRoundedMicrosecondFixedIter,
RtkProvidedTxFirstOrderSagnac,
CanonicalLightTimeClosedFormSagnac,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum SagnacRecipe {
#[default]
ClosedFormZRotation,
RtklibFirstOrderScalar,
Off,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum FrameRecipe {
#[default]
SppSkyfieldAuThreeIter,
GeocentricUpRtkReference,
GeodeticNeuCrossProduct,
DopEnuRotation,
CanonicalWgs84,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum NormalRecipe {
#[default]
SppWeightedResidualFiniteDifference,
RtkDoubleDifferenceBlockFirstTie,
PppDenseLastTie,
CanonicalSquareRoot,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum SolverRecipe {
#[default]
NalgebraTrfLegacy,
FlatGaussianFirstTie,
DenseGaussianLastTie,
ScipyHostLapackReference,
OwnedDeterministicCholesky,
OwnedDeterministicTrf,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct EstimationRecipe {
pub range: RangeRecipe,
pub sagnac: SagnacRecipe,
pub frame: FrameRecipe,
pub normal: NormalRecipe,
pub solver: SolverRecipe,
}
impl EstimationRecipe {
pub const fn spp() -> Self {
Self {
range: RangeRecipe::SppMeasuredPseudorangeFixedIter,
sagnac: SagnacRecipe::ClosedFormZRotation,
frame: FrameRecipe::SppSkyfieldAuThreeIter,
normal: NormalRecipe::SppWeightedResidualFiniteDifference,
solver: SolverRecipe::NalgebraTrfLegacy,
}
}
pub const fn rtk() -> Self {
Self {
range: RangeRecipe::RtkProvidedTxFirstOrderSagnac,
sagnac: SagnacRecipe::RtklibFirstOrderScalar,
frame: FrameRecipe::GeocentricUpRtkReference,
normal: NormalRecipe::RtkDoubleDifferenceBlockFirstTie,
solver: SolverRecipe::FlatGaussianFirstTie,
}
}
pub const fn ppp() -> Self {
Self {
range: RangeRecipe::ObservableRoundedMicrosecondFixedIter,
sagnac: SagnacRecipe::ClosedFormZRotation,
frame: FrameRecipe::GeodeticNeuCrossProduct,
normal: NormalRecipe::PppDenseLastTie,
solver: SolverRecipe::DenseGaussianLastTie,
}
}
pub const fn spp_owned_deterministic() -> Self {
let mut recipe = Self::spp();
recipe.solver = SolverRecipe::OwnedDeterministicTrf;
recipe
}
pub const fn canonical_spp() -> Self {
Self {
range: RangeRecipe::CanonicalLightTimeClosedFormSagnac,
sagnac: SagnacRecipe::ClosedFormZRotation,
frame: FrameRecipe::CanonicalWgs84,
normal: NormalRecipe::SppWeightedResidualFiniteDifference,
solver: SolverRecipe::OwnedDeterministicTrf,
}
}
pub const fn canonical_rtk() -> Self {
Self {
range: RangeRecipe::RtkProvidedTxFirstOrderSagnac,
sagnac: SagnacRecipe::RtklibFirstOrderScalar,
frame: FrameRecipe::GeocentricUpRtkReference,
normal: NormalRecipe::CanonicalSquareRoot,
solver: SolverRecipe::OwnedDeterministicCholesky,
}
}
pub const fn canonical_ppp() -> Self {
Self {
range: RangeRecipe::ObservableRoundedMicrosecondFixedIter,
sagnac: SagnacRecipe::ClosedFormZRotation,
frame: FrameRecipe::GeodeticNeuCrossProduct,
normal: NormalRecipe::CanonicalSquareRoot,
solver: SolverRecipe::OwnedDeterministicCholesky,
}
}
pub const fn for_canonical(technique: Technique) -> Option<Self> {
match technique {
Technique::Spp => Some(Self::canonical_spp()),
Technique::Rtk => Some(Self::canonical_rtk()),
Technique::Ppp => Some(Self::canonical_ppp()),
}
}
pub const fn for_reference(technique: Technique, target: ReferenceTarget) -> Option<Self> {
match (technique, target) {
(Technique::Spp, ReferenceTarget::Skyfield) => Some(Self::spp()),
(Technique::Spp, ReferenceTarget::OwnedDeterministic) => {
Some(Self::spp_owned_deterministic())
}
(Technique::Rtk, ReferenceTarget::Rtklib) => Some(Self::rtk()),
(Technique::Ppp, ReferenceTarget::PppOracle) => Some(Self::ppp()),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DifferencingMode {
DoubleDifferencePerSystemReference,
Undifferenced,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PartialResolution {
Disabled,
Exhaustive { min_ambiguities: usize },
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct AmbiguityIdPolicy {
pub differencing: DifferencingMode,
pub float_only_gating: bool,
pub partial: PartialResolution,
pub ratio_threshold: f64,
}
impl AmbiguityIdPolicy {
pub const fn rtk_static(ratio_threshold: f64, partial_min_ambiguities: usize) -> Self {
Self {
differencing: DifferencingMode::DoubleDifferencePerSystemReference,
float_only_gating: true,
partial: PartialResolution::Exhaustive {
min_ambiguities: partial_min_ambiguities,
},
ratio_threshold,
}
}
pub const fn rtk_sequential(ratio_threshold: f64) -> Self {
Self {
differencing: DifferencingMode::DoubleDifferencePerSystemReference,
float_only_gating: true,
partial: PartialResolution::Disabled,
ratio_threshold,
}
}
pub const fn ppp(ratio_threshold: f64) -> Self {
Self {
differencing: DifferencingMode::Undifferenced,
float_only_gating: false,
partial: PartialResolution::Disabled,
ratio_threshold,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ResidualNormRecipe {
RtkInverseVarianceInnovation,
RtkInverseSigmaResidual,
PppInverseSigmaMagnitude,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ScreenKind {
RaimChiSquare,
RtkFixedResidualValidation,
RtkSequentialInnovation,
PppFloatLeaveOneOut,
}
impl ScreenKind {
pub const fn residual_norm(self) -> Option<ResidualNormRecipe> {
match self {
Self::RaimChiSquare => None,
Self::RtkFixedResidualValidation => Some(ResidualNormRecipe::RtkInverseSigmaResidual),
Self::RtkSequentialInnovation => Some(ResidualNormRecipe::RtkInverseVarianceInnovation),
Self::PppFloatLeaveOneOut => Some(ResidualNormRecipe::PppInverseSigmaMagnitude),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults_name_current_spp_behavior() {
assert_eq!(EstimationRecipe::default(), EstimationRecipe::spp());
assert_eq!(
RangeRecipe::default(),
RangeRecipe::SppMeasuredPseudorangeFixedIter
);
assert_eq!(SagnacRecipe::default(), SagnacRecipe::ClosedFormZRotation);
assert_eq!(FrameRecipe::default(), FrameRecipe::SppSkyfieldAuThreeIter);
assert_eq!(
NormalRecipe::default(),
NormalRecipe::SppWeightedResidualFiniteDifference
);
assert_eq!(SolverRecipe::default(), SolverRecipe::NalgebraTrfLegacy);
assert_eq!(StrategyId::default(), StrategyId::spp_reference());
}
#[test]
fn strategy_constructors_match_reference_targets() {
assert_eq!(
StrategyId::spp_reference(),
StrategyId::Reference {
technique: Technique::Spp,
target: ReferenceTarget::Skyfield,
}
);
assert_eq!(
StrategyId::rtk_reference(),
StrategyId::Reference {
technique: Technique::Rtk,
target: ReferenceTarget::Rtklib,
}
);
assert_eq!(
StrategyId::ppp_reference(),
StrategyId::Reference {
technique: Technique::Ppp,
target: ReferenceTarget::PppOracle,
}
);
}
#[test]
fn for_reference_selects_each_supported_pairs_recipe() {
assert_eq!(
EstimationRecipe::for_reference(Technique::Spp, ReferenceTarget::Skyfield),
Some(EstimationRecipe::spp())
);
assert_eq!(
EstimationRecipe::for_reference(Technique::Rtk, ReferenceTarget::Rtklib),
Some(EstimationRecipe::rtk())
);
assert_eq!(
EstimationRecipe::for_reference(Technique::Ppp, ReferenceTarget::PppOracle),
Some(EstimationRecipe::ppp())
);
}
#[test]
fn owned_deterministic_recipe_swaps_only_the_solver() {
let owned = EstimationRecipe::spp_owned_deterministic();
assert_eq!(owned.solver, SolverRecipe::OwnedDeterministicTrf);
assert_eq!(
EstimationRecipe {
solver: SolverRecipe::NalgebraTrfLegacy,
..owned
},
EstimationRecipe::spp()
);
assert_eq!(
EstimationRecipe::for_reference(Technique::Spp, ReferenceTarget::OwnedDeterministic),
Some(owned)
);
}
#[test]
fn canonical_spp_recipe_uses_the_rigorous_op_orders() {
let canonical = EstimationRecipe::canonical_spp();
assert_eq!(
canonical.range,
RangeRecipe::CanonicalLightTimeClosedFormSagnac
);
assert_ne!(canonical.range, EstimationRecipe::spp().range);
assert_eq!(canonical.frame, FrameRecipe::CanonicalWgs84);
assert_ne!(canonical.frame, EstimationRecipe::spp().frame);
assert_eq!(canonical.sagnac, SagnacRecipe::ClosedFormZRotation);
assert_ne!(canonical.sagnac, SagnacRecipe::RtklibFirstOrderScalar);
assert_eq!(canonical.solver, SolverRecipe::OwnedDeterministicTrf);
assert_eq!(
EstimationRecipe::for_canonical(Technique::Spp),
Some(canonical)
);
}
#[test]
fn canonical_rtk_recipe_uses_the_square_root_solve() {
let canonical = EstimationRecipe::canonical_rtk();
assert_eq!(canonical.normal, NormalRecipe::CanonicalSquareRoot);
assert_eq!(canonical.solver, SolverRecipe::OwnedDeterministicCholesky);
assert_ne!(canonical.normal, EstimationRecipe::rtk().normal);
assert_ne!(canonical.solver, EstimationRecipe::rtk().solver);
assert_eq!(canonical.range, EstimationRecipe::rtk().range);
assert_eq!(canonical.sagnac, EstimationRecipe::rtk().sagnac);
assert_eq!(canonical.frame, EstimationRecipe::rtk().frame);
assert_eq!(
EstimationRecipe::for_canonical(Technique::Rtk),
Some(canonical)
);
}
#[test]
fn canonical_ppp_recipe_uses_the_square_root_solve() {
let canonical = EstimationRecipe::canonical_ppp();
assert_eq!(canonical.normal, NormalRecipe::CanonicalSquareRoot);
assert_eq!(canonical.solver, SolverRecipe::OwnedDeterministicCholesky);
assert_ne!(canonical.normal, EstimationRecipe::ppp().normal);
assert_ne!(canonical.solver, EstimationRecipe::ppp().solver);
assert_eq!(canonical.range, EstimationRecipe::ppp().range);
assert_eq!(canonical.sagnac, EstimationRecipe::ppp().sagnac);
assert_eq!(canonical.frame, EstimationRecipe::ppp().frame);
assert_eq!(canonical.normal, EstimationRecipe::canonical_rtk().normal);
assert_eq!(canonical.solver, EstimationRecipe::canonical_rtk().solver);
assert_eq!(
EstimationRecipe::for_canonical(Technique::Ppp),
Some(canonical)
);
}
#[test]
fn for_canonical_wires_all_three_techniques() {
assert_eq!(
EstimationRecipe::for_canonical(Technique::Spp),
Some(EstimationRecipe::canonical_spp())
);
assert_eq!(
EstimationRecipe::for_canonical(Technique::Rtk),
Some(EstimationRecipe::canonical_rtk())
);
assert_eq!(
EstimationRecipe::for_canonical(Technique::Ppp),
Some(EstimationRecipe::canonical_ppp())
);
}
#[test]
fn for_reference_rejects_impossible_pairs() {
for (technique, target) in [
(Technique::Spp, ReferenceTarget::Rtklib),
(Technique::Spp, ReferenceTarget::PppOracle),
(Technique::Spp, ReferenceTarget::Scipy),
(Technique::Rtk, ReferenceTarget::Skyfield),
(Technique::Rtk, ReferenceTarget::OwnedDeterministic),
(Technique::Rtk, ReferenceTarget::PppOracle),
(Technique::Ppp, ReferenceTarget::Skyfield),
(Technique::Ppp, ReferenceTarget::OwnedDeterministic),
] {
assert_eq!(
EstimationRecipe::for_reference(technique, target),
None,
"{technique:?} + {target:?} must be rejected"
);
}
}
#[test]
fn reference_ambiguity_policies_name_current_behavior() {
let rtk_static = AmbiguityIdPolicy::rtk_static(3.0, 4);
assert_eq!(
rtk_static.differencing,
DifferencingMode::DoubleDifferencePerSystemReference
);
assert!(rtk_static.float_only_gating);
assert_eq!(
rtk_static.partial,
PartialResolution::Exhaustive { min_ambiguities: 4 }
);
let rtk_seq = AmbiguityIdPolicy::rtk_sequential(3.0);
assert_eq!(
rtk_seq.differencing,
DifferencingMode::DoubleDifferencePerSystemReference
);
assert!(rtk_seq.float_only_gating);
assert_eq!(rtk_seq.partial, PartialResolution::Disabled);
let ppp = AmbiguityIdPolicy::ppp(2.5);
assert_eq!(ppp.differencing, DifferencingMode::Undifferenced);
assert!(!ppp.float_only_gating);
assert_eq!(ppp.partial, PartialResolution::Disabled);
}
#[test]
fn rtk_and_ppp_id_policies_differ_only_in_data() {
let rtk = AmbiguityIdPolicy::rtk_static(3.0, 1);
let ppp = AmbiguityIdPolicy::ppp(3.0);
assert_ne!(rtk.differencing, ppp.differencing);
assert_ne!(rtk.float_only_gating, ppp.float_only_gating);
assert_ne!(rtk.partial, ppp.partial);
}
#[test]
fn screen_kinds_select_their_normalization_order() {
assert_eq!(ScreenKind::RaimChiSquare.residual_norm(), None);
assert_eq!(
ScreenKind::RtkFixedResidualValidation.residual_norm(),
Some(ResidualNormRecipe::RtkInverseSigmaResidual)
);
assert_eq!(
ScreenKind::RtkSequentialInnovation.residual_norm(),
Some(ResidualNormRecipe::RtkInverseVarianceInnovation)
);
assert_eq!(
ScreenKind::PppFloatLeaveOneOut.residual_norm(),
Some(ResidualNormRecipe::PppInverseSigmaMagnitude)
);
}
#[test]
fn each_strategy_selects_a_distinct_solver_order() {
assert_ne!(
EstimationRecipe::spp().solver,
EstimationRecipe::rtk().solver
);
assert_ne!(
EstimationRecipe::rtk().solver,
EstimationRecipe::ppp().solver
);
assert_ne!(
EstimationRecipe::spp().solver,
EstimationRecipe::ppp().solver
);
}
}