pub mod auto_init;
pub mod cycle_slip;
mod fixed;
mod float;
mod kinematic;
mod model;
mod normal;
mod prep;
pub mod raim;
mod rows;
pub mod tec;
mod types;
pub mod velocity;
pub use auto_init::{
solve_ppp_auto_init_fixed, solve_ppp_auto_init_fixed_with_strategy, solve_ppp_auto_init_float,
solve_ppp_auto_init_float_with_strategy, PppAutoInitError, PppAutoInitOptions,
PppAutoInitStrategy, PppInitialGuess,
};
pub use cycle_slip::{
detect_cycle_slips, geometry_free_m, melbourne_wubbena_cycles, update_geometry_free,
update_melbourne_wubbena, CycleSlipConfig, CycleSlipConfigError, CycleSlipDetectorState,
CycleSlipError, CycleSlipFlagEpoch, CycleSlipFlagObservation, CycleSlipStateKey,
GeometryFreeUpdate, MelbourneWubbenaUpdate, RunningMeanVariance, SatelliteCycleSlipState,
DEFAULT_MINIMUM_ARC_LENGTH, DEFAULT_RUNNING_STATISTIC_K_FACTOR,
};
pub(crate) use fixed::run_fixed_from_float;
pub use fixed::solve_fixed_from_float;
#[cfg(test)]
use float::initial_ambiguities;
pub(crate) use float::run_float_epochs;
pub use float::{solve_float_epoch, solve_float_epochs};
pub use kinematic::{
correct_kinematic_state, predict_kinematic_state, solve_kinematic_ppp, KinematicConfig,
KinematicEpochSolution, KinematicEpochStatus, KinematicMotionModel,
KinematicPositionProcessNoise, KinematicProcessNoise, KinematicSolveError, KinematicState,
KinematicUpdateSummary,
};
pub use prep::{
prepare_widelane_fixed_epochs, split_float_cycle_slip_epochs, DualFrequencyEpoch,
DualFrequencyObservation, FloatCycleSlipEpoch, FloatCycleSlipObservation,
FloatCycleSlipTaggedEpoch, FloatCycleSlipTaggedObservation, PppSplitArc, PreparedFloatEpoch,
PreparedFloatObservation, WideLanePrepError, WideLanePrepOptions, WideLanePrepResult,
};
pub use raim::{
solve_float_epoch_with_raim, ProtectionLevels, RaimConfig, RaimError, RaimFdeError,
RaimFdeResult, RaimFdeStatus, RaimGeometryRow, RaimIdentification, RaimResult, RaimStatus,
SatelliteTestStatistic,
};
pub use tec::{
code_geometry_free_m, estimate_code_slant_tec, estimate_phase_slant_tec, estimate_tec,
ionospheric_pierce_point, level_slant_tec_arc, phase_geometry_free_m,
slant_tec_from_code_geometry_free_m, slant_tec_from_phase_geometry_free_m,
thin_shell_mapping_function, vertical_tec_from_slant_tec, CodeSlantTecEstimate,
IonosphericPiercePoint, LeveledTecSample, PhaseSlantTecEstimate, TecConfig, TecEpoch, TecError,
TecEstimate, TecEstimateSample, TecLevelingResult, TecLevelingSample, TecObservation,
TecSatelliteArc, DEFAULT_IONOSPHERIC_SHELL_HEIGHT_M, ELECTRONS_PER_TECU_M2,
TEC_GROUP_DELAY_COEFFICIENT,
};
pub use types::*;
pub use velocity::{
predict_range_rate_m_s, solve_velocity, RangeRatePrediction, ReceiverVelocityState,
VelocityConfig, VelocityObservation, VelocityRobustConfig, VelocitySolution,
VelocitySolveError,
};
pub use crate::ambiguity::CycleSlipPolicy;
pub mod defaults {
pub const POSITION_TOLERANCE_M: f64 = 1.0e-4;
pub const CLOCK_TOLERANCE_M: f64 = 1.0e-4;
pub const AMBIGUITY_TOLERANCE_M: f64 = 1.0e-4;
pub const ZTD_TOLERANCE_M: f64 = 1.0e-4;
pub const MAX_ITERATIONS: usize = 8;
pub const RATIO_THRESHOLD: f64 = 3.0;
}
use std::collections::BTreeMap;
use crate::constants::F_L1_HZ;
use crate::estimation::recipe::NormalRecipe;
use crate::observables::{ObservableEphemerisSource, ObservablesError, PredictOptions};
use crate::ppp_corrections::{
self, PppCorrectionEpoch, PppCorrectionObservation, PppCorrectionsError, PppCorrectionsOptions,
};
use crate::sp3::Sp3;
use crate::validate::{self, FieldError};
const MAX_PPP_ITERATIONS: usize = 10_000;
pub fn build_ppp_lookup(
sp3: &Sp3,
epochs: &[FloatEpoch],
receiver_ecef_m: [f64; 3],
options: &PppCorrectionsOptions,
) -> Result<PppCorrectionLookup, PppCorrectionsError> {
let ppp_epochs: Vec<PppCorrectionEpoch> = epochs
.iter()
.map(|epoch| PppCorrectionEpoch {
epoch: epoch.epoch,
t_rx_j2000_s: epoch.t_rx_j2000_s,
observations: epoch
.observations
.iter()
.map(|obs| PppCorrectionObservation {
sat: obs.sat,
freq1_hz: obs.freq1_hz,
freq2_hz: obs.freq2_hz,
})
.collect(),
})
.collect();
let corrections = ppp_corrections::build(sp3, &ppp_epochs, receiver_ecef_m, options)?;
Ok(PppCorrectionLookup::from_options(corrections, options))
}
impl FloatState {
fn default_for_epochs(epochs: &[FloatEpoch]) -> Self {
Self {
position_m: [0.0; 3],
clocks_m: vec![0.0; epochs.len()],
ambiguities_m: BTreeMap::new(),
ztd_m: 0.0,
}
}
}
#[derive(Clone, Copy)]
struct ModelContext<'a> {
source: &'a dyn ObservableEphemerisSource,
weights: MeasurementWeights,
tropo: TroposphereOptions,
corrections: &'a RangeCorrections,
normal: NormalRecipe,
}
fn predict_default(
_source: &dyn ObservableEphemerisSource,
_obs: &FloatObservation,
) -> Result<PredictOptions, FloatSolveError> {
Ok(PredictOptions {
carrier_hz: F_L1_HZ,
light_time: true,
sagnac: true,
})
}
fn no_ephemeris(obs: &FloatObservation, error: ObservablesError) -> FloatSolveError {
FloatSolveError::NoEphemeris {
satellite_id: obs.satellite_id.clone(),
reason: match error {
ObservablesError::NoEphemeris => NoEphemerisReason::NoEphemeris,
ObservablesError::InvalidInput { .. } => NoEphemerisReason::Reason(error.to_string()),
ObservablesError::Ephemeris(err) => NoEphemerisReason::Reason(err.to_string()),
},
}
}
fn missing_satellite_clock(obs: &FloatObservation) -> FloatSolveError {
FloatSolveError::NoEphemeris {
satellite_id: obs.satellite_id.clone(),
reason: NoEphemerisReason::MissingSatelliteClock,
}
}
fn missing_correction(obs: &FloatObservation, correction: MissingCorrection) -> FloatSolveError {
FloatSolveError::MissingCorrection {
satellite_id: obs.satellite_id.clone(),
correction,
}
}
fn invalid_clock_count(expected: usize, actual: usize) -> FloatSolveError {
FloatSolveError::InvalidClockCount { expected, actual }
}
fn invalid_solve_option(field: &'static str, reason: &'static str) -> FloatSolveError {
FloatSolveError::InvalidSolveOption { field, reason }
}
pub(super) fn invalid_input(error: FieldError) -> FloatSolveError {
invalid_input_field(error.field(), error.reason())
}
fn invalid_input_field(field: &'static str, reason: &'static str) -> FloatSolveError {
FloatSolveError::InvalidInput { field, reason }
}
fn invalid_fixed_input(error: FieldError) -> FixedSolveError {
FixedSolveError::Float(invalid_input(error))
}
pub(super) fn validate_float_solve_boundary(
epochs: &[FloatEpoch],
state: &FloatState,
config: &FloatSolveConfig,
) -> Result<(), FloatSolveError> {
validate_epochs(epochs)?;
validate_float_state(state, epochs.len())?;
validate_float_config(config)
}
pub(super) fn validate_fixed_solve_boundary(
epochs: &[FloatEpoch],
solution: &FloatSolution,
config: &FixedSolveConfig,
) -> Result<(), FixedSolveError> {
validate_epochs(epochs).map_err(FixedSolveError::Float)?;
validate_float_solution(solution, epochs.len())?;
validate_fixed_config(config)
}
fn validate_epochs(epochs: &[FloatEpoch]) -> Result<(), FloatSolveError> {
for epoch in epochs {
validate_epoch(epoch)?;
}
Ok(())
}
fn validate_epoch(epoch: &FloatEpoch) -> Result<(), FloatSolveError> {
validate::civil_datetime_with_second_policy(
epoch.epoch.year as i64,
epoch.epoch.month as i64,
epoch.epoch.day as i64,
epoch.epoch.hour as i64,
epoch.epoch.minute as i64,
epoch.epoch.second,
validate::CivilSecondPolicy::Continuous,
)
.map_err(invalid_input)?;
validate::finite(epoch.jd_whole, "ppp epoch jd_whole").map_err(invalid_input)?;
validate::finite(epoch.jd_fraction, "ppp epoch jd_fraction").map_err(invalid_input)?;
validate::finite(epoch.t_rx_j2000_s, "ppp epoch t_rx_j2000_s").map_err(invalid_input)?;
for obs in &epoch.observations {
validate_observation(obs)?;
}
Ok(())
}
fn validate_observation(obs: &FloatObservation) -> Result<(), FloatSolveError> {
validate::finite(obs.code_m, "ppp observation code_m").map_err(invalid_input)?;
validate::finite(obs.phase_m, "ppp observation phase_m").map_err(invalid_input)?;
validate::finite(obs.freq1_hz, "ppp observation freq1_hz").map_err(invalid_input)?;
validate::finite(obs.freq2_hz, "ppp observation freq2_hz").map_err(invalid_input)?;
Ok(())
}
fn validate_float_state(state: &FloatState, n_epochs: usize) -> Result<(), FloatSolveError> {
validate_state_clock_count(state, n_epochs)?;
validate::finite_vec3(state.position_m, "ppp state position_m").map_err(invalid_input)?;
validate::finite_slice(&state.clocks_m, "ppp state clocks_m").map_err(invalid_input)?;
for value in state.ambiguities_m.values() {
validate::finite(*value, "ppp state ambiguities_m").map_err(invalid_input)?;
}
validate::finite(state.ztd_m, "ppp state ztd_m").map_err(invalid_input)?;
Ok(())
}
fn validate_float_solution(
solution: &FloatSolution,
n_epochs: usize,
) -> Result<(), FixedSolveError> {
validate_solution_clock_count(solution, n_epochs)?;
validate::finite_vec3(solution.position_m, "ppp float_solution position_m")
.map_err(invalid_fixed_input)?;
validate::finite_slice(
&solution.epoch_clocks_m,
"ppp float_solution epoch_clocks_m",
)
.map_err(invalid_fixed_input)?;
for value in solution.ambiguities_m.values() {
validate::finite(*value, "ppp float_solution ambiguities_m")
.map_err(invalid_fixed_input)?;
}
if let Some(ztd_m) = solution.ztd_residual_m {
validate::finite(ztd_m, "ppp float_solution ztd_residual_m")
.map_err(invalid_fixed_input)?;
}
for residual in &solution.residuals_m {
validate::finite(residual.code_m, "ppp float_solution residual code_m")
.map_err(invalid_fixed_input)?;
validate::finite(residual.phase_m, "ppp float_solution residual phase_m")
.map_err(invalid_fixed_input)?;
validate::finite(
residual.code_weight,
"ppp float_solution residual code_weight",
)
.map_err(invalid_fixed_input)?;
validate::finite(
residual.phase_weight,
"ppp float_solution residual phase_weight",
)
.map_err(invalid_fixed_input)?;
}
validate::finite_nonneg(solution.code_rms_m, "ppp float_solution code_rms_m")
.map_err(invalid_fixed_input)?;
validate::finite_nonneg(solution.phase_rms_m, "ppp float_solution phase_rms_m")
.map_err(invalid_fixed_input)?;
validate::finite_nonneg(solution.weighted_rms_m, "ppp float_solution weighted_rms_m")
.map_err(invalid_fixed_input)?;
Ok(())
}
pub(super) fn validate_float_solution_output(
solution: &FloatSolution,
n_epochs: usize,
) -> Result<(), FloatSolveError> {
validate_float_solution_clock_count(solution, n_epochs)?;
validate::finite_vec3(solution.position_m, "ppp float_solution position_m")
.map_err(invalid_input)?;
validate::finite_slice(
&solution.epoch_clocks_m,
"ppp float_solution epoch_clocks_m",
)
.map_err(invalid_input)?;
for value in solution.ambiguities_m.values() {
validate::finite(*value, "ppp float_solution ambiguities_m").map_err(invalid_input)?;
}
if let Some(ztd_m) = solution.ztd_residual_m {
validate::finite(ztd_m, "ppp float_solution ztd_residual_m").map_err(invalid_input)?;
}
for residual in &solution.residuals_m {
validate::finite(residual.code_m, "ppp float_solution residual code_m")
.map_err(invalid_input)?;
validate::finite(residual.phase_m, "ppp float_solution residual phase_m")
.map_err(invalid_input)?;
validate::finite(
residual.code_weight,
"ppp float_solution residual code_weight",
)
.map_err(invalid_input)?;
validate::finite(
residual.phase_weight,
"ppp float_solution residual phase_weight",
)
.map_err(invalid_input)?;
}
validate::finite_nonneg(solution.code_rms_m, "ppp float_solution code_rms_m")
.map_err(invalid_input)?;
validate::finite_nonneg(solution.phase_rms_m, "ppp float_solution phase_rms_m")
.map_err(invalid_input)?;
validate::finite_nonneg(solution.weighted_rms_m, "ppp float_solution weighted_rms_m")
.map_err(invalid_input)?;
Ok(())
}
fn validate_float_config(config: &FloatSolveConfig) -> Result<(), FloatSolveError> {
validate_common_config(
config.weights,
config.tropo,
&config.corrections,
config.opts,
)
}
fn validate_fixed_config(config: &FixedSolveConfig) -> Result<(), FixedSolveError> {
validate_common_config(
config.weights,
config.tropo,
&config.corrections,
config.opts,
)
.map_err(FixedSolveError::Float)?;
validate_fixed_ambiguity_options(&config.ambiguity)
}
fn validate_common_config(
weights: MeasurementWeights,
tropo: TroposphereOptions,
corrections: &RangeCorrections,
opts: FloatSolveOptions,
) -> Result<(), FloatSolveError> {
validate_measurement_weights(weights)?;
validate_troposphere_options(tropo)?;
validate_range_corrections(corrections)?;
validate_float_solve_options(opts)
}
fn validate_measurement_weights(weights: MeasurementWeights) -> Result<(), FloatSolveError> {
validate::finite_positive(weights.code, "ppp measurement weight code")
.map_err(invalid_input)?;
validate::finite_positive(weights.phase, "ppp measurement weight phase")
.map_err(invalid_input)?;
Ok(())
}
fn validate_troposphere_options(tropo: TroposphereOptions) -> Result<(), FloatSolveError> {
if !tropo.enabled {
return Ok(());
}
validate::finite_positive(tropo.met.pressure_hpa, "ppp tropo pressure_hpa")
.map_err(invalid_input)?;
validate::finite_positive(tropo.met.temperature_k, "ppp tropo temperature_k")
.map_err(invalid_input)?;
validate::fraction(tropo.met.relative_humidity, "ppp tropo relative_humidity")
.map_err(invalid_input)?;
Ok(())
}
fn validate_range_corrections(corrections: &RangeCorrections) -> Result<(), FloatSolveError> {
if let Some(receiver) = &corrections.receiver_antenna {
validate::finite_positive(receiver.freq1_hz, "ppp receiver antenna freq1_hz")
.map_err(invalid_input)?;
validate::finite_positive(receiver.freq2_hz, "ppp receiver antenna freq2_hz")
.map_err(invalid_input)?;
if receiver.freq1_hz == receiver.freq2_hz {
return Err(invalid_input_field(
"ppp receiver antenna frequency pair",
"must differ",
));
}
for frequency in &receiver.frequencies {
validate_receiver_antenna_frequency(frequency)?;
}
}
if let Some(clock) = &corrections.satellite_clock {
for records in clock.series.values() {
validate::require_strictly_increasing(
records.iter().map(|&(t_gps_s, _)| t_gps_s),
"ppp satellite clock epoch_s",
)
.map_err(invalid_input)?;
for &(t_gps_s, bias_s) in records {
validate::finite(t_gps_s, "ppp satellite clock epoch_s").map_err(invalid_input)?;
validate::finite(bias_s, "ppp satellite clock bias_s").map_err(invalid_input)?;
}
}
}
for vector in corrections.ppp.tide.values() {
validate::finite_vec3(*vector, "ppp correction tide vector_m").map_err(invalid_input)?;
}
for vector in corrections.ppp.pole_tide.values() {
validate::finite_vec3(*vector, "ppp correction pole_tide vector_m")
.map_err(invalid_input)?;
}
for vector in corrections.ppp.ocean_loading.values() {
validate::finite_vec3(*vector, "ppp correction ocean_loading vector_m")
.map_err(invalid_input)?;
}
for value in corrections.ppp.windup_m.values() {
validate::finite(*value, "ppp correction windup_m").map_err(invalid_input)?;
}
for vector in corrections.ppp.sat_pco_ecef.values() {
validate::finite_vec3(*vector, "ppp correction sat_pco_ecef").map_err(invalid_input)?;
}
for value in corrections.ppp.sat_pcv_m.values() {
validate::finite(*value, "ppp correction sat_pcv_m").map_err(invalid_input)?;
}
Ok(())
}
fn validate_receiver_antenna_frequency(
frequency: &ReceiverAntennaFrequency,
) -> Result<(), FloatSolveError> {
validate::finite_vec3(frequency.pco_m, "ppp receiver antenna pco_m").map_err(invalid_input)?;
for sample in &frequency.pcv_samples {
validate_pcv_sample(sample)?;
}
Ok(())
}
fn validate_pcv_sample(sample: &PcvSample) -> Result<(), FloatSolveError> {
if let Some(azimuth_deg) = sample.azimuth_deg {
validate::finite(azimuth_deg, "ppp receiver antenna pcv azimuth_deg")
.map_err(invalid_input)?;
}
validate::finite_in_range(
sample.zenith_deg,
0.0,
180.0,
"ppp receiver antenna pcv zenith_deg",
)
.map_err(invalid_input)?;
validate::finite(sample.value_m, "ppp receiver antenna pcv value_m").map_err(invalid_input)?;
Ok(())
}
fn validate_fixed_ambiguity_options(
ambiguity: &FixedAmbiguityOptions,
) -> Result<(), FixedSolveError> {
validate::finite_nonneg(
ambiguity.ratio_threshold,
"ppp fixed ambiguity ratio_threshold",
)
.map_err(invalid_fixed_input)?;
for value in ambiguity.wavelengths_m.values() {
validate::finite_positive(*value, "ppp fixed ambiguity wavelength_m")
.map_err(invalid_fixed_input)?;
}
for value in ambiguity.offsets_m.values() {
validate::finite(*value, "ppp fixed ambiguity offset_m").map_err(invalid_fixed_input)?;
}
Ok(())
}
fn validate_float_solve_options(opts: FloatSolveOptions) -> Result<(), FloatSolveError> {
if opts.max_iterations == 0 {
return Err(invalid_solve_option("max_iterations", "must be positive"));
}
if opts.max_iterations > MAX_PPP_ITERATIONS {
return Err(invalid_solve_option(
"max_iterations",
"exceeds the PPP iteration cap",
));
}
validate_tolerance("position_tolerance_m", opts.position_tolerance_m)?;
validate_tolerance("clock_tolerance_m", opts.clock_tolerance_m)?;
validate_tolerance("ambiguity_tolerance_m", opts.ambiguity_tolerance_m)?;
validate_tolerance("ztd_tolerance_m", opts.ztd_tolerance_m)
}
fn validate_tolerance(field: &'static str, value: f64) -> Result<(), FloatSolveError> {
if validate::finite(value, field).is_err() {
return Err(invalid_solve_option(field, "must be finite"));
}
if value <= 0.0 {
return Err(invalid_solve_option(field, "must be positive"));
}
Ok(())
}
fn validate_state_clock_count(state: &FloatState, n_epochs: usize) -> Result<(), FloatSolveError> {
if state.clocks_m.len() == n_epochs {
Ok(())
} else {
Err(invalid_clock_count(n_epochs, state.clocks_m.len()))
}
}
fn validate_solution_clock_count(
solution: &FloatSolution,
n_epochs: usize,
) -> Result<(), FixedSolveError> {
if solution.epoch_clocks_m.len() == n_epochs {
Ok(())
} else {
Err(FixedSolveError::Float(invalid_clock_count(
n_epochs,
solution.epoch_clocks_m.len(),
)))
}
}
fn validate_float_solution_clock_count(
solution: &FloatSolution,
n_epochs: usize,
) -> Result<(), FloatSolveError> {
if solution.epoch_clocks_m.len() == n_epochs {
Ok(())
} else {
Err(invalid_clock_count(n_epochs, solution.epoch_clocks_m.len()))
}
}
fn state_from_solution(solution: &FloatSolution, prior: &FloatState) -> FloatState {
FloatState {
position_m: solution.position_m,
clocks_m: solution.epoch_clocks_m.clone(),
ambiguities_m: solution.ambiguities_m.clone(),
ztd_m: solution.ztd_residual_m.unwrap_or(prior.ztd_m),
}
}
fn estimates_ztd(tropo: TroposphereOptions) -> bool {
tropo.enabled && tropo.estimate_ztd
}
fn ztd_unknown_count(tropo: TroposphereOptions) -> usize {
usize::from(estimates_ztd(tropo))
}
fn rms(values: &[f64]) -> f64 {
if values.is_empty() {
return 0.0;
}
(values.iter().map(|v| v * v).sum::<f64>() / values.len() as f64).sqrt()
}
fn weighted_rms(rows: &[FloatResidual], weights: MeasurementWeights) -> f64 {
let mut values = Vec::with_capacity(rows.len() * 2);
for row in rows {
values.push(row.code_m * row.code_weight);
values.push(row.phase_m * row.phase_weight);
}
if values.is_empty() {
rms(&[0.0 * weights.code, 0.0 * weights.phase])
} else {
rms(&values)
}
}
fn max_abs(xs: &[f64]) -> f64 {
xs.iter().map(|x| x.abs()).fold(0.0, f64::max)
}
#[cfg(test)]
mod tests;