use core::fmt;
use flate2::read::GzDecoder;
use std::io::Read;
use std::path::Path;
pub use sidereon_core::astro::forces::{
EarthRadiationPressure, SchwarzschildRelativity, SolarRadiationPressure,
SphericalHarmonicCoefficient, SphericalHarmonicGravity, SphericalHarmonicGravityConfig,
ThirdBodyBodies, ThirdBodyGravity, ZonalCoefficients, ZonalDegrees, ZonalGravity,
EGM96_DEGREE_ORDER_36, EGM96_EMBEDDED_MAX_DEGREE, EGM96_EMBEDDED_MAX_ORDER, EGM96_MU_KM3_S2,
EGM96_REFERENCE_RADIUS_KM,
};
pub use sidereon_core::astro::frames::{
EarthOrientation, EarthOrientationProvider, TdbEarthOrientationProvider,
};
pub use sidereon_core::astro::propagator::{ForceModelComponents, ForceModelKind};
pub use sidereon_core::geometry_quality::{
classify, GeometryQuality, GeometryQualityThresholds, ObservabilityTier,
};
pub use sidereon_core::positioning::{
solve_spp_from_rinex_obs, spp_inputs_from_rinex_obs, spp_inputs_from_rtcm_msm,
RinexSppAssemblySource, RinexSppBroadcastCorrections, RinexSppEpochInputs,
RinexSppEpochSolution, RinexSppError, RinexSppOptions, RinexSppSource, RtcmSppEpochInputs,
};
pub use sidereon_core::quality::{
reliability_araim, reliability_design, spp_robust_fde_driver, wtest_noncentrality,
wtest_noncentrality_components, ObservationReliability, RangeReliabilityRow,
ReliabilityOptions, ReliabilityReport, ReliabilitySummary, WtestNoncentralityComponents,
};
pub use sidereon_core::static_positioning::{
solve_static, StaticClockBias, StaticCovariance, StaticEpoch, StaticEpochInfluence,
StaticInfluenceStatus, StaticResidual, StaticSatelliteBatchInfluence, StaticSatelliteInfluence,
StaticSolution, StaticSolutionMetadata, StaticSolveError, StaticSolveOptions,
};
pub use sidereon_core::{
antex, araim, astro, atmosphere, bias, broadcast_comparison, carrier_phase, clock_stability,
combinations, constants, constellation, data, dgnss, dop, ephemeris, frame_catalog,
frequencies, fusion, geodesic, geodetic_time_series, geofence, geometry, geometry_quality, ils,
inertial, navigation, nmea, observables, orbit, positioning, ppp_corrections, qc_obs, quality,
rinex, rtcm, rtk, sbas, sbas_pl, sidereal, signal, source_localization, ssr, staleness,
static_positioning, terrain, terrain_store, tides, velocity,
};
pub use sidereon_core::{
catalog, catalog_entry, propagate_position, transform, transform_from_epoch, FrameCatalogError,
HelmertParameters, HelmertRates, HelmertTransform, TerrestrialFrame, TerrestrialPositionM,
TerrestrialState, TerrestrialVelocityMPerYear, TERRESTRIAL_FRAME_CATALOG,
};
pub use sidereon_core::{
containment, containment_probability, containment_probability_with_options, crossing,
crossing_probability, crossing_probability_with_options, distance_to_boundary, CrossingEvent,
CrossingKind, Fence, GeofenceError, GeofencePositionEstimate, PositionUncertainty,
ProbabilityHysteresis, ProbabilityMethod, ProbabilityOptions, GEOFENCE_BOUNDARY_TOLERANCE_M,
PLANAR_FAST_PATH_MAX_RADIUS_M,
};
pub use sidereon_core::{
emission_media_batch_at_j2000_s, observable_media_corrections, predict_batch_with_media,
predict_batch_with_media_parallel, predict_ranges_with_media, predict_with_media,
AppliedMediaCorrections, EmissionMediaBatch, EmissionMediaBatchOptions, EmissionMediaStatus,
MediaPredictOptions, MediaPredictedObservables, MediaRangePrediction,
ObservableIonosphereCorrection, ObservableMediaOptions, ObservableTroposphereCorrection,
};
pub use sidereon_core::{
error_ellipse_from_enu_m2, horizontal_radius_at, metrics_from_ecef_covariance_m2,
metrics_from_enu_covariance_m2, metrics_from_kinematic_solution,
metrics_from_position_covariance, spherical_radius_at, vertical_radius_at, ErrorEllipse,
ErrorMetricsError, PercentileRadius, PositionErrorMetrics,
};
pub use sidereon_core::{
gauss_markov_bias_decay, gauss_markov_bias_variance_increment, gravity_ecef_mps2,
mechanize_ecef, normal_gravity_mps2, rodrigues_delta_dcm, simulate_imu_samples,
simulate_imu_samples_from_increments, true_imu_increment_between, AttitudeQuaternion,
ConingCorrection, CorrectedImuIncrement, ImuBias, ImuCalibration, ImuErrorModel, ImuGrade,
ImuRateRandomWalk, ImuSample, ImuSampleKind, ImuSimulationOptions, ImuSimulationOutput,
ImuSimulator, ImuSpec, InertialError, MechanizationConfig, NavState, SimulatedImuSequence,
StrapdownMechanizer, DEFAULT_IMU_SIM_SEED, WGS84_NORMAL_GRAVITY_EQUATOR_MPS2,
WGS84_NORMAL_GRAVITY_POLE_MPS2, WGS84_SOMIGLIANA_K,
};
pub use sidereon_core::{
geodesic_direct, geodesic_inverse, geodetic_to_itrf, itrf_to_geodetic, FrameValueError,
GeodesicError, GnssSatelliteId, GnssSystem, ItrfPositionM, ItrfVelocityMS, ProtectionModel,
SatelliteIdError, Wgs84Geodetic,
};
pub use sidereon_core::{
orbit_repeat_lag, periodicity_strength, periodicity_strength_with_sample_interval,
repeat_period, sidereal_filter, solar_day_period, SiderealFilterError, SiderealFilterOptions,
SiderealFilterOutput, SiderealTemplateMethod, SIDEREAL_DAY_NANOS, SIDEREAL_DAY_SECONDS,
};
pub use sidereon_core::{
sbas_protection_levels, AirborneModel, DegradationParams, ProtectionGeometry, ProtectionRow,
SbasErrorModel, SbasKMultipliers, SbasPlError, SbasProtection, SbasSisError,
};
pub use sidereon_core::{
ukf_correct_closed_loop, F64Bits, FusionFilterKind, FusionStateCodecError,
SerializableErrorStateLayout, SerializableFusionSnapshot, SerializableFusionState,
SerializableImuSample, SerializableImuSampleKind, SerializableInsFilterState,
SerializableLooseMeasurement, SerializableNavState, SerializableRateEndpoint,
SerializableSatelliteId, SerializableStoredCheckpoint, SerializableStoredGnssMeasurement,
SerializableStoredImuSample, SerializableTightCarrierPhaseObservation,
SerializableTightFilterState, SerializableTightGnssEpoch, SerializableTightGnssObservation,
SerializableTightRangeRateObservation, SerializableTimeSyncHistory,
SerializableTimeSyncHistoryConfig, UkfUpdateOptions, UnscentedTransformOptions,
FUSION_STATE_CODEC_VERSION,
};
pub use sidereon_core::{RejectedSat, RejectionReason};
pub mod rtk_filter {
pub use sidereon_core::rtk_filter::{
fix_wide_lane_rtk_arc, prepare_ionosphere_free_rtk_arc, solve_moving_baseline,
solve_moving_baseline_epoch, solve_rtk_arc, solve_static_rtk_arc,
solve_wide_lane_fixed_rtk_arc, AmbiguityScale, AmbiguitySearch, AmbiguitySet,
CycleSlipOptions, CycleSlipPolicy, CycleSlipSplitArc, Epoch, FixedBaselineSolution,
FixedSolveError, FixedSolveOpts, FloatBaselineSolution, FloatResidual, FloatSolveError,
FloatSolveOpts, FloatSolveStatus, FullSetIntegerSummary, InnovationScreen,
IntegerSearchMeta, IntegerStatus, IonosphereFreeBaselineError, MeasModel,
MovingBaselineEpoch, MovingBaselineEpochSolution, MovingBaselineError, MovingBaselineOpts,
MovingBaselineSequenceError, MovingBaselineStatus, PartialSearchMeta,
ReceiverAntennaCalibration, ReceiverAntennaCorrections, ReceiverAntennaError,
ResidualComponentKind, ResidualValidationMeta, ResidualValidationOpts,
ResidualValidationOutlier, RtkArcConfig, RtkArcEpoch, RtkArcEpochSolution, RtkArcError,
RtkArcObservation, RtkArcPreprocessing, RtkArcSolution, RtkDualCycleSlipConfig,
RtkDualFrequencyArcEpoch, RtkDualFrequencyObservation,
RtkDualFrequencySatelliteObservation, RtkIonosphereFreeArcConfig,
RtkIonosphereFreeArcError, RtkIonosphereFreeArcSolution, RtkStaticArcConfig,
RtkStaticArcError, RtkStaticArcSolution, RtkWideLaneArcConfig, RtkWideLaneArcError,
RtkWideLaneArcSolution, RtkWideLaneFixedArcConfig, RtkWideLaneFixedArcError,
RtkWideLaneFixedArcIntegerMethod, RtkWideLaneFixedArcMetadata, RtkWideLaneFixedArcSolution,
RtkWideLaneFixedArcSolveConfig, RtkWideLaneFixedSequentialArcSolution,
RtkWideLaneFixedStaticArcSolution, SatMeas, StochasticModel,
ValidatedFixedBaselineSolution, ValidatedFixedSolveError, ValidatedFixedSolveOpts,
WideLaneError, WideLaneOptions,
};
}
pub mod geoid {
pub use sidereon_core::geoid::{
egm96_ellipsoidal_height_m, egm96_grid, egm96_orthometric_height_m, egm96_undulation,
egm96_undulations_deg, egm96_undulations_rad, ellipsoidal_height_m, geoid_undulation,
geoid_undulations_deg, geoid_undulations_rad, orthometric_height_m, Egm2008GridSpacing,
Egm2008RasterWindow, GeoidError, GeoidGrid,
};
}
pub mod almanac {
pub use sidereon_core::astro::almanac::{
geocentric_ecliptic, lunar_solar_eclipses, meridian_transits, moon_phase_deg, moon_phases,
planetary_events, seasons, AlmanacError, CulminationEvent, CulminationKind, EclipseEvent,
EclipseKind, EclipticLonLat, EphemerisSource, MoonPhaseEvent, MoonPhaseKind, Planet,
PlanetaryEvent, PlanetaryEventKind, SeasonEvent, SeasonKind, TransitBody,
};
}
pub mod precise_positioning {
pub use sidereon_core::precise_positioning::{
solve_ppp_auto_init_fixed, solve_ppp_auto_init_fixed_with_strategy,
solve_ppp_auto_init_float, solve_ppp_auto_init_float_with_strategy, AmbiguitySearch,
FixedAmbiguityOptions, FixedIntegerMetadata, FixedSolution, FixedSolveConfig,
FixedSolveError, FloatEpoch, FloatObservation, FloatResidual, FloatSolution,
FloatSolveConfig, FloatSolveError, FloatSolveOptions, FloatState, FloatStatus,
IntegerStatus, MeasurementWeights, MissingCorrection, NoEphemerisReason, PcvSample,
PositionCovariance, PppAutoInitError, PppAutoInitOptions, PppAutoInitStrategy,
PppCorrectionLookup, PppInitialGuess, RangeCorrections, ReceiverAntennaFrequency,
ReceiverAntennaOptions, SatelliteClockCorrections, TemporalCorrelationSummary,
TroposphereOptions,
};
}
pub mod raw {
pub use sidereon_core::{precise_positioning, rtk_filter};
}
pub use sidereon_core::astro::anomaly::{
eccentric_to_mean, eccentric_to_true, mean_to_eccentric, mean_to_true, propagate_kepler,
solve_kepler, true_to_eccentric, true_to_mean, AnomalyError, KeplerSolution,
};
pub use sidereon_core::astro::bodies::{
observe, observe_spk_body, Ecliptic, Equatorial, Horizontal, Observation, ObserveOptions,
Refraction, Target,
};
pub mod covariance {
pub use sidereon_core::astro::covariance::{
covariance6_km_to_m, covariance6_m_to_km, eci_to_rtn_covariance6,
interpolate_covariance_psd, rtn_to_eci, rtn_to_eci_covariance6, rtn_to_eci_rotation,
symmetric, Covariance6, Covariance6Error, Mat6, RtnFrameError,
};
}
pub use sidereon_core::astro::forces::{
DragForce, SourcedDragForce, SpaceWeather, SpaceWeatherSource,
};
pub use sidereon_core::astro::frames::transforms::{
gcrs_to_teme_compute, gcrs_to_true_of_date_matrix,
};
pub use sidereon_core::astro::sgp4::{DecayLatch, DecayLatchedError, Loss, XScale};
pub use sidereon_core::astro::space_weather::{
ObservationClass, SpaceWeatherPolicy, SpaceWeatherSample, SpaceWeatherTable,
};
pub use sidereon_core::astro::{
omm, passes, propagator, sgp4, space_weather, state, tca, tdm, tle,
};
pub use sidereon_core::ephemeris::{
fit_precise_ephemeris_state_sample_orbit, fit_precise_ephemeris_state_sample_orbits,
precise_interpolant_store_checksum64, sp3_ecef_state_to_eci, MmapPreciseEphemerisInterpolant,
OrientedPreciseEphemerisStateSample, PreciseEphemerisInterpolant, PreciseEphemerisStateSample,
PreciseInterpolantStoreError,
};
pub use sidereon_core::astro::relative;
pub mod least_squares {
pub use sidereon_core::astro::math::least_squares::{
covariance_from_jacobian, covariance_from_report, normal_covariance,
};
}
pub mod rinex_qc {
pub use sidereon_core::rinex::qc::{
AppliedEdit, Finding, FindingRef, HeaderEditError, LintReport, NavRepair, ObsHeaderEdit,
ObsRepair, RepairAction, RepairOptions, Severity,
};
}
use sidereon_core::antex::{Antex, AntexError};
use sidereon_core::bias::{BiasError, BiasSet, CodeDcbOptions, Parsed as BiasParsed};
use sidereon_core::ephemeris::{BroadcastEphemeris, Sp3};
use sidereon_core::observables::ObservableEphemerisSource;
use sidereon_core::positioning::{
EphemerisSource, ReceiverSolution, SolveInputs, SolvePolicy, SolvePolicyError,
};
use sidereon_core::precise_positioning::{
FixedSolution, FixedSolveConfig, FixedSolveError as PppFixedSolveError, FloatEpoch,
FloatSolution, FloatSolveConfig, FloatSolveError as PppFloatSolveError, FloatState,
};
use sidereon_core::rinex::clock::{RinexClock, RinexClockError};
use sidereon_core::rinex::nav::NavParseError;
use sidereon_core::rinex::observations::ObservationFile;
use sidereon_core::rinex::qc::{LintReport, NavRepair, ObsRepair, RepairOptions};
use sidereon_core::rtk_filter::{
AmbiguitySet, Epoch, FloatBaselineSolution, FloatSolveError as RtkFloatSolveError,
FloatSolveOpts, MeasModel, ReceiverAntennaCorrections, ValidatedFixedBaselineSolution,
ValidatedFixedSolveError, ValidatedFixedSolveOpts,
};
use sidereon_core::velocity::{
VelocityError, VelocityObservation, VelocitySolution, VelocitySolveOptions,
};
#[derive(Debug)]
pub enum Error {
Sp3(sidereon_core::Error),
Antex(AntexError),
RinexNav(NavParseError),
RinexObs(sidereon_core::Error),
RinexClock(RinexClockError),
Bias(BiasError),
Ssr(sidereon_core::Error),
Crinex(sidereon_core::Error),
Io(std::io::Error),
Spp(SolvePolicyError),
Velocity(VelocityError),
RtkFloat(RtkFloatSolveError),
RtkFixed(ValidatedFixedSolveError),
PppFloat(PppFloatSolveError),
PppFixed(PppFixedSolveError),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Sp3(e) => write!(f, "SP3 parse failed: {e}"),
Error::Antex(e) => write!(f, "ANTEX parse failed: {e}"),
Error::RinexNav(e) => write!(f, "RINEX NAV parse failed: {e}"),
Error::RinexObs(e) => write!(f, "RINEX OBS parse failed: {e}"),
Error::RinexClock(e) => write!(f, "RINEX clock parse failed: {e}"),
Error::Bias(e) => write!(f, "bias product parse failed: {e}"),
Error::Ssr(e) => write!(f, "SSR ingest failed: {e}"),
Error::Crinex(e) => write!(f, "CRINEX decode failed: {e}"),
Error::Io(e) => write!(f, "product file read failed: {e}"),
Error::Spp(e) => write!(f, "{e}"),
Error::Velocity(e) => write!(f, "velocity solve failed: {e}"),
Error::RtkFloat(e) => write!(f, "{e}"),
Error::RtkFixed(e) => write!(f, "{e}"),
Error::PppFloat(e) => write!(f, "{e}"),
Error::PppFixed(e) => write!(f, "{e}"),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::Sp3(e) => Some(e),
Error::Antex(e) => Some(e),
Error::RinexNav(e) => Some(e),
Error::RinexObs(e) => Some(e),
Error::RinexClock(e) => Some(e),
Error::Bias(e) => Some(e),
Error::Ssr(e) => Some(e),
Error::Crinex(e) => Some(e),
Error::Io(e) => Some(e),
Error::Spp(e) => Some(e),
Error::Velocity(e) => Some(e),
Error::RtkFloat(e) => Some(e),
Error::RtkFixed(e) => Some(e),
Error::PppFloat(e) => Some(e),
Error::PppFixed(e) => Some(e),
}
}
}
impl From<AntexError> for Error {
fn from(e: AntexError) -> Self {
Error::Antex(e)
}
}
impl From<NavParseError> for Error {
fn from(e: NavParseError) -> Self {
Error::RinexNav(e)
}
}
impl From<RinexClockError> for Error {
fn from(e: RinexClockError) -> Self {
Error::RinexClock(e)
}
}
impl From<BiasError> for Error {
fn from(e: BiasError) -> Self {
Error::Bias(e)
}
}
impl From<std::io::Error> for Error {
fn from(e: std::io::Error) -> Self {
Error::Io(e)
}
}
impl From<SolvePolicyError> for Error {
fn from(e: SolvePolicyError) -> Self {
Error::Spp(e)
}
}
impl From<VelocityError> for Error {
fn from(e: VelocityError) -> Self {
Error::Velocity(e)
}
}
impl From<RtkFloatSolveError> for Error {
fn from(e: RtkFloatSolveError) -> Self {
Error::RtkFloat(e)
}
}
impl From<ValidatedFixedSolveError> for Error {
fn from(e: ValidatedFixedSolveError) -> Self {
Error::RtkFixed(e)
}
}
impl From<PppFloatSolveError> for Error {
fn from(e: PppFloatSolveError) -> Self {
Error::PppFloat(e)
}
}
impl From<PppFixedSolveError> for Error {
fn from(e: PppFixedSolveError) -> Self {
Error::PppFixed(e)
}
}
pub type Result<T> = core::result::Result<T, Error>;
#[derive(Clone)]
pub struct RtkFloatConfig<'a> {
pub epochs: &'a [Epoch],
pub base_ecef_m: [f64; 3],
pub ambiguity_ids: &'a [String],
pub initial_baseline_m: [f64; 3],
pub model: &'a MeasModel,
pub options: FloatSolveOpts,
pub receiver_antenna_corrections: Option<&'a ReceiverAntennaCorrections>,
}
impl<'a> RtkFloatConfig<'a> {
#[must_use]
pub fn new(
epochs: &'a [Epoch],
base_ecef_m: [f64; 3],
ambiguity_ids: &'a [String],
model: &'a MeasModel,
options: FloatSolveOpts,
) -> Self {
Self {
epochs,
base_ecef_m,
ambiguity_ids,
initial_baseline_m: [0.0; 3],
model,
options,
receiver_antenna_corrections: None,
}
}
#[must_use = "this builder consumes and returns the updated RTK float config"]
pub fn with_initial_baseline_m(mut self, initial_baseline_m: [f64; 3]) -> Self {
self.initial_baseline_m = initial_baseline_m;
self
}
#[must_use = "this builder consumes and returns the updated RTK float config"]
pub fn with_receiver_antenna_corrections(
mut self,
receiver_antenna_corrections: Option<&'a ReceiverAntennaCorrections>,
) -> Self {
self.receiver_antenna_corrections = receiver_antenna_corrections;
self
}
}
#[derive(Clone)]
pub struct RtkFixedConfig<'a> {
pub epochs: &'a [Epoch],
pub base_ecef_m: [f64; 3],
pub initial_ambiguities: AmbiguitySet<'a>,
pub initial_baseline_m: [f64; 3],
pub model: &'a MeasModel,
pub options: ValidatedFixedSolveOpts,
pub receiver_antenna_corrections: Option<&'a ReceiverAntennaCorrections>,
}
impl<'a> RtkFixedConfig<'a> {
#[must_use]
pub fn new(
epochs: &'a [Epoch],
base_ecef_m: [f64; 3],
initial_ambiguities: AmbiguitySet<'a>,
model: &'a MeasModel,
options: ValidatedFixedSolveOpts,
) -> Self {
Self {
epochs,
base_ecef_m,
initial_ambiguities,
initial_baseline_m: [0.0; 3],
model,
options,
receiver_antenna_corrections: None,
}
}
#[must_use = "this builder consumes and returns the updated RTK fixed config"]
pub fn with_initial_baseline_m(mut self, initial_baseline_m: [f64; 3]) -> Self {
self.initial_baseline_m = initial_baseline_m;
self
}
#[must_use = "this builder consumes and returns the updated RTK fixed config"]
pub fn with_receiver_antenna_corrections(
mut self,
receiver_antenna_corrections: Option<&'a ReceiverAntennaCorrections>,
) -> Self {
self.receiver_antenna_corrections = receiver_antenna_corrections;
self
}
}
#[derive(Clone)]
pub struct PppFloatConfig<'a> {
pub source: &'a dyn ObservableEphemerisSource,
pub epochs: &'a [FloatEpoch],
pub initial_state: FloatState,
pub solve: FloatSolveConfig,
}
impl<'a> PppFloatConfig<'a> {
pub fn new(
source: &'a dyn ObservableEphemerisSource,
epochs: &'a [FloatEpoch],
initial_state: FloatState,
solve: FloatSolveConfig,
) -> Self {
Self {
source,
epochs,
initial_state,
solve,
}
}
}
#[derive(Clone)]
pub struct PppFixedConfig<'a> {
pub source: &'a dyn ObservableEphemerisSource,
pub epochs: &'a [FloatEpoch],
pub float_solution: FloatSolution,
pub solve: FixedSolveConfig,
}
impl<'a> PppFixedConfig<'a> {
pub fn new(
source: &'a dyn ObservableEphemerisSource,
epochs: &'a [FloatEpoch],
float_solution: FloatSolution,
solve: FixedSolveConfig,
) -> Self {
Self {
source,
epochs,
float_solution,
solve,
}
}
}
pub fn load_sp3(bytes: &[u8]) -> Result<Sp3> {
Sp3::parse(bytes).map_err(Error::Sp3)
}
pub fn parse_nmea(input: &[u8]) -> nmea::Parsed<nmea::NmeaLog> {
nmea::parse_nmea(input)
}
pub fn nmea_epochs(input: &[u8]) -> (Vec<nmea::EpochSnapshot>, nmea::Diagnostics) {
let parsed = nmea::parse_nmea(input);
let epochs = nmea::group_epochs(&parsed.value);
(epochs, parsed.diagnostics)
}
pub fn write_gga(
talker: nmea::NmeaTalker,
gga: &nmea::Gga,
) -> std::result::Result<String, nmea::NmeaError> {
nmea::write_gga(talker, gga)
}
pub fn parse_antex(text: &str) -> Result<Antex> {
Antex::parse(text).map_err(Error::Antex)
}
pub fn load_antex(path: impl AsRef<Path>) -> Result<Antex> {
let text = std::fs::read_to_string(path)?;
parse_antex(&text)
}
pub fn parse_rinex_nav(text: &str) -> Result<BroadcastEphemeris> {
BroadcastEphemeris::from_nav(text).map_err(Error::RinexNav)
}
pub fn load_rinex_nav(path: impl AsRef<Path>) -> Result<BroadcastEphemeris> {
let text = std::fs::read_to_string(path)?;
parse_rinex_nav(&text)
}
pub fn ssr_store_from_rtcm(
bytes: &[u8],
week: sidereon_core::astro::time::GnssWeekTow,
) -> Result<sidereon_core::ssr::SsrCorrectionStore> {
let mut store = sidereon_core::ssr::SsrCorrectionStore::new();
let mut assembler = sidereon_core::rtcm::SsrStreamAssembler::new();
for decoded in assembler.push(bytes) {
let message = decoded.map_err(Error::Ssr)?;
store.ingest(&message, week).map_err(Error::Ssr)?;
}
Ok(store)
}
pub fn parse_rinex_obs(text: &str) -> Result<ObservationFile> {
ObservationFile::parse(text).map_err(Error::RinexObs)
}
pub fn load_rinex_obs(path: impl AsRef<Path>) -> Result<ObservationFile> {
let text = std::fs::read_to_string(path)?;
parse_rinex_obs(&text)
}
pub fn lint_rinex_obs(text: &str) -> LintReport {
sidereon_core::rinex::qc::lint_obs_text(text)
}
pub fn lint_rinex_nav(text: &str) -> LintReport {
sidereon_core::rinex::qc::lint_nav_text(text)
}
pub fn repair_rinex_obs(text: &str, options: &RepairOptions) -> Result<ObsRepair> {
sidereon_core::rinex::qc::repair_obs_text(text, options).map_err(Error::RinexObs)
}
pub fn repair_rinex_nav(text: &str, options: &RepairOptions) -> Result<NavRepair> {
sidereon_core::rinex::qc::repair_nav_text(text, options).map_err(Error::RinexNav)
}
pub fn parse_rinex_clock(text: &str) -> Result<RinexClock> {
RinexClock::parse(text).map_err(Error::RinexClock)
}
pub fn load_rinex_clock(path: impl AsRef<Path>) -> Result<RinexClock> {
let text = std::fs::read_to_string(path)?;
parse_rinex_clock(&text)
}
pub fn parse_rinex_clock_lossy(text: &str) -> RinexClock {
RinexClock::parse_lossy(text)
}
pub fn load_rinex_clock_lossy(path: impl AsRef<Path>) -> Result<RinexClock> {
let text = std::fs::read_to_string(path)?;
Ok(parse_rinex_clock_lossy(&text))
}
pub fn parse_bias_sinex(bytes: &[u8]) -> Result<BiasSet> {
Ok(parse_bias_sinex_lossy(bytes)?.value)
}
pub fn parse_bias_sinex_lossy(bytes: &[u8]) -> Result<BiasParsed<BiasSet>> {
BiasSet::parse_bias_sinex(bytes).map_err(Error::Bias)
}
pub fn load_bias_sinex(path: impl AsRef<Path>) -> Result<BiasSet> {
let bytes = read_maybe_gzip(path)?;
parse_bias_sinex(&bytes)
}
pub fn load_bias_sinex_lossy(path: impl AsRef<Path>) -> Result<BiasParsed<BiasSet>> {
let bytes = read_maybe_gzip(path)?;
parse_bias_sinex_lossy(&bytes)
}
pub fn parse_code_dcb(bytes: &[u8], options: Option<CodeDcbOptions>) -> Result<BiasSet> {
Ok(parse_code_dcb_lossy(bytes, options)?.value)
}
pub fn parse_code_dcb_lossy(
bytes: &[u8],
options: Option<CodeDcbOptions>,
) -> Result<BiasParsed<BiasSet>> {
BiasSet::parse_code_dcb(bytes, options).map_err(Error::Bias)
}
pub fn load_code_dcb(path: impl AsRef<Path>, options: Option<CodeDcbOptions>) -> Result<BiasSet> {
let bytes = read_maybe_gzip(path)?;
parse_code_dcb(&bytes, options)
}
pub fn load_code_dcb_lossy(
path: impl AsRef<Path>,
options: Option<CodeDcbOptions>,
) -> Result<BiasParsed<BiasSet>> {
let bytes = read_maybe_gzip(path)?;
parse_code_dcb_lossy(&bytes, options)
}
pub fn decode_crinex(text: &str) -> Result<String> {
rinex::decode_crinex(text).map_err(Error::Crinex)
}
pub fn load_crinex(path: impl AsRef<Path>) -> Result<String> {
let text = std::fs::read_to_string(path)?;
decode_crinex(&text)
}
fn read_maybe_gzip(path: impl AsRef<Path>) -> Result<Vec<u8>> {
let path = path.as_ref();
let bytes = std::fs::read(path)?;
if path.extension().and_then(|ext| ext.to_str()) != Some("gz") {
return Ok(bytes);
}
let mut decoder = GzDecoder::new(&bytes[..]);
let mut decoded = Vec::new();
decoder.read_to_end(&mut decoded)?;
Ok(decoded)
}
pub fn solve_spp(
eph: &dyn EphemerisSource,
inputs: &SolveInputs,
with_geodetic: bool,
policy: SolvePolicy,
) -> Result<ReceiverSolution> {
sidereon_core::positioning::solve_with_policy(eph, inputs, with_geodetic, policy)
.map_err(Error::Spp)
}
pub fn solve_spp_batch_serial(
eph: &dyn EphemerisSource,
epochs: &[SolveInputs],
with_geodetic: bool,
policy: SolvePolicy,
) -> Vec<Result<ReceiverSolution>> {
sidereon_core::positioning::solve_spp_batch_serial(eph, epochs, with_geodetic, policy)
.into_iter()
.map(|r| r.map_err(Error::Spp))
.collect()
}
pub fn solve_spp_batch(
eph: &(dyn EphemerisSource + Sync),
epochs: &[SolveInputs],
with_geodetic: bool,
policy: SolvePolicy,
) -> Vec<Result<ReceiverSolution>> {
sidereon_core::positioning::solve_spp_batch_parallel(eph, epochs, with_geodetic, policy)
.into_iter()
.map(|r| r.map_err(Error::Spp))
.collect()
}
pub fn solve_velocity(
source: &dyn ObservableEphemerisSource,
observations: &[VelocityObservation],
receiver_ecef_m: [f64; 3],
t_rx_j2000_s: f64,
options: VelocitySolveOptions,
) -> Result<VelocitySolution> {
sidereon_core::velocity::solve(source, observations, receiver_ecef_m, t_rx_j2000_s, options)
.map_err(Error::Velocity)
}
pub fn solve_rtk_float_with(config: RtkFloatConfig<'_>) -> Result<FloatBaselineSolution> {
solve_rtk_float(
config.epochs,
config.base_ecef_m,
config.ambiguity_ids,
config.initial_baseline_m,
config.model,
config.options,
config.receiver_antenna_corrections,
)
}
pub fn solve_rtk_float(
epochs: &[Epoch],
base: [f64; 3],
ambiguity_ids: &[String],
initial_baseline_m: [f64; 3],
model: &MeasModel,
opts: FloatSolveOpts,
receiver_antenna_corrections: Option<&ReceiverAntennaCorrections>,
) -> Result<FloatBaselineSolution> {
sidereon_core::rtk_filter::solve_float_baseline(
epochs,
base,
ambiguity_ids,
initial_baseline_m,
model,
opts,
receiver_antenna_corrections,
)
.map_err(Error::RtkFloat)
}
pub fn solve_rtk_fixed_with(config: RtkFixedConfig<'_>) -> Result<ValidatedFixedBaselineSolution> {
solve_rtk_fixed(
config.epochs,
config.base_ecef_m,
config.initial_ambiguities,
config.initial_baseline_m,
config.model,
config.options,
config.receiver_antenna_corrections,
)
}
pub fn solve_rtk_fixed(
epochs: &[Epoch],
base: [f64; 3],
initial_ambiguities: AmbiguitySet,
initial_baseline_m: [f64; 3],
model: &MeasModel,
opts: ValidatedFixedSolveOpts,
receiver_antenna_corrections: Option<&ReceiverAntennaCorrections>,
) -> Result<ValidatedFixedBaselineSolution> {
sidereon_core::rtk_filter::solve_fixed_baseline_validated(
epochs,
base,
initial_ambiguities,
initial_baseline_m,
model,
opts,
receiver_antenna_corrections,
)
.map_err(Error::RtkFixed)
}
pub fn solve_ppp_float_with(config: PppFloatConfig<'_>) -> Result<FloatSolution> {
solve_ppp_float(
config.source,
config.epochs,
config.initial_state,
config.solve,
)
}
pub fn solve_ppp_float(
source: &dyn ObservableEphemerisSource,
epochs: &[FloatEpoch],
initial_state: FloatState,
config: FloatSolveConfig,
) -> Result<FloatSolution> {
sidereon_core::precise_positioning::solve_float_epochs(source, epochs, initial_state, config)
.map_err(Error::PppFloat)
}
pub fn solve_ppp_fixed_with(config: PppFixedConfig<'_>) -> Result<FixedSolution> {
solve_ppp_fixed(
config.source,
config.epochs,
config.float_solution,
config.solve,
)
}
pub fn solve_ppp_fixed(
source: &dyn ObservableEphemerisSource,
epochs: &[FloatEpoch],
float_solution: FloatSolution,
config: FixedSolveConfig,
) -> Result<FixedSolution> {
sidereon_core::precise_positioning::solve_fixed_from_float(
source,
epochs,
float_solution,
config,
)
.map_err(Error::PppFixed)
}
#[cfg(all(test, sidereon_repo_tests))]
mod tests {
use super::*;
use sidereon_core::positioning::{Corrections, KlobucharCoeffs, Observation, SurfaceMet};
use std::collections::BTreeMap;
use std::path::PathBuf;
const DEGENERATE_SP3: &[u8] =
include_bytes!("../../sidereon-core/tests/fixtures/sp3/degenerate_coincident_5sat.sp3");
const ANTEX_TEXT: &str =
include_str!("../../sidereon-core/tests/fixtures/antex/igs20_wettzell_trim.atx");
const RINEX_NAV_TEXT: &str =
include_str!("../../sidereon-core/tests/fixtures/nav/ESBC00DNK_R_20201770000_01D_MN.rnx");
const RINEX_OBS_TEXT: &str = include_str!(
"../../sidereon-core/tests/fixtures/obs/ESBC00DNK_R_20201770000_01D_30S_MO_trim.rnx"
);
const RINEX_CLOCK_TEXT: &str =
include_str!("../../sidereon-core/tests/fixtures/clk/synthetic_rinex_clock.clk");
const BIAS_BYTES: &[u8] = include_bytes!("../../sidereon-core/tests/fixtures/bias/CODE.BIA");
const DCB_BYTES: &[u8] =
include_bytes!("../../sidereon-core/tests/fixtures/bias/P1C1_RINEX.DCB");
const CRINEX_TEXT: &str = include_str!(
"../../sidereon-core/tests/fixtures/obs/ESBC00DNK_R_20201770000_01D_30S_MO_trim.crx"
);
const STATIONS_TLE: &str =
include_str!("../../sidereon-core/tests/fixtures/celestrak/stations.tle");
const REAL_SSRA02IGS0_1060_FRAME_HEX: &str =
include_str!("../../sidereon-core/tests/fixtures/ssr/SSRA02IGS0_2026181234930_1060.hex");
fn fixture_path(parts: &[&str]) -> PathBuf {
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("../sidereon-core/tests/fixtures");
for part in parts {
path.push(part);
}
path
}
fn station_tle(name: &str) -> tca::TcaTle<'static> {
let mut lines = STATIONS_TLE.lines();
while let Some(object_name) = lines.next() {
let Some(line1) = lines.next() else {
break;
};
let Some(line2) = lines.next() else {
break;
};
if object_name.trim() == name {
return tca::TcaTle::new(line1, line2);
}
}
panic!("missing station TLE {name}");
}
fn norm3(v: [f64; 3]) -> f64 {
(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt()
}
fn hex_bytes(hex: &str) -> Vec<u8> {
let compact: String = hex.chars().filter(|c| c.is_ascii_hexdigit()).collect();
assert_eq!(compact.len() % 2, 0);
compact
.as_bytes()
.chunks_exact(2)
.map(|chunk| {
let hi = (chunk[0] as char).to_digit(16).unwrap();
let lo = (chunk[1] as char).to_digit(16).unwrap();
((hi << 4) | lo) as u8
})
.collect()
}
fn rtk_model() -> MeasModel {
MeasModel {
code_sigma_m: 0.3,
phase_sigma_m: 0.003,
sagnac: true,
stochastic: rtk_filter::StochasticModel::Rtklib,
}
}
fn rtk_float_options() -> FloatSolveOpts {
FloatSolveOpts {
position_tol_m: 1.0e-4,
ambiguity_tol_m: 1.0e-4,
max_iterations: 1,
}
}
fn rtk_fixed_options() -> rtk_filter::ValidatedFixedSolveOpts {
rtk_filter::ValidatedFixedSolveOpts {
float: rtk_float_options(),
fixed: rtk_filter::FixedSolveOpts {
position_tol_m: 1.0e-4,
ambiguity_tol_m: 1.0e-4,
max_iterations: 1,
ratio_threshold: 3.0,
partial_ambiguity_resolution: false,
partial_min_ambiguities: 4,
},
residual: rtk_filter::ResidualValidationOpts {
threshold_sigma: None,
max_exclusions: 0,
},
}
}
fn ppp_float_solve_config() -> FloatSolveConfig {
FloatSolveConfig {
weights: precise_positioning::MeasurementWeights {
code: 1.0,
phase: 100.0,
elevation_weighting: false,
},
tropo: precise_positioning::TroposphereOptions::disabled(),
corrections: precise_positioning::RangeCorrections::disabled(),
opts: precise_positioning::FloatSolveOptions {
max_iterations: 1,
position_tolerance_m: 1.0e-4,
clock_tolerance_m: 1.0e-4,
ambiguity_tolerance_m: 1.0e-4,
ztd_tolerance_m: 1.0e-4,
},
elevation_cutoff_deg: None,
residual_screen: false,
estimate_residual_ionosphere: false,
}
}
fn ppp_fixed_solve_config() -> FixedSolveConfig {
let float = ppp_float_solve_config();
FixedSolveConfig {
weights: float.weights,
tropo: float.tropo,
corrections: float.corrections,
opts: float.opts,
elevation_cutoff_deg: None,
ambiguity: precise_positioning::FixedAmbiguityOptions {
wavelengths_m: BTreeMap::new(),
offsets_m: BTreeMap::new(),
ratio_threshold: 3.0,
},
estimate_residual_ionosphere: false,
}
}
fn empty_ppp_state() -> FloatState {
FloatState {
position_m: [0.0; 3],
clocks_m: Vec::new(),
ambiguities_m: BTreeMap::new(),
ztd_m: 0.0,
tropo_gradient_north_m: 0.0,
tropo_gradient_east_m: 0.0,
residual_ionosphere_m: BTreeMap::new(),
}
}
fn empty_ppp_float_solution() -> FloatSolution {
FloatSolution {
position_m: [0.0; 3],
position_covariance: sidereon_core::dop::PositionCovariance {
ecef_m2: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
enu_m2: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
},
formal_position_covariance: sidereon_core::dop::PositionCovariance {
ecef_m2: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
enu_m2: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
},
posterior_variance_factor: 1.0,
position_covariance_scale_factor: 1.0,
temporal_position_covariance: sidereon_core::dop::PositionCovariance {
ecef_m2: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
enu_m2: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
},
temporal_position_covariance_scale_factor: 1.0,
temporal_correlation: sidereon_core::precise_positioning::TemporalCorrelationSummary {
lag1_autocorrelation: 0.0,
decorrelation_time_epochs: 0.0,
decorrelation_time_s: None,
nominal_sample_count: 0,
effective_sample_count: 0.0,
variance_inflation_factor: 1.0,
arcs_used: 0,
},
epoch_clocks_m: Vec::new(),
ambiguities_m: BTreeMap::new(),
residual_ionosphere_m: BTreeMap::new(),
ztd_residual_m: None,
tropo_gradient_north_m: None,
tropo_gradient_east_m: None,
tropo_gradient_covariance_m2: None,
formal_tropo_gradient_covariance_m2: None,
residuals_m: Vec::new(),
used_sats: Vec::new(),
iterations: 0,
converged: false,
status: precise_positioning::FloatStatus::MaxIterations,
code_rms_m: 0.0,
phase_rms_m: 0.0,
weighted_rms_m: 0.0,
}
}
#[test]
fn load_sp3_parses_a_precise_product() {
let sp3 = load_sp3(DEGENERATE_SP3).expect("the fixture parses");
assert_eq!(sp3.epoch_count(), 2);
assert_eq!(sp3.satellites().len(), 5);
}
#[test]
fn load_sp3_surfaces_parse_errors() {
let err = load_sp3(b"not an sp3 file").unwrap_err();
assert!(matches!(err, Error::Sp3(_)));
assert!(err.to_string().contains("SP3 parse failed"));
assert!(std::error::Error::source(&err).is_some());
}
#[test]
fn product_ingestion_wrappers_parse_fixture_products() {
let antex = parse_antex(ANTEX_TEXT).expect("parse ANTEX fixture");
assert!(!antex.antennas.is_empty());
let loaded_antex =
load_antex(fixture_path(&["antex", "igs20_wettzell_trim.atx"])).expect("load ANTEX");
assert_eq!(loaded_antex.antennas.len(), antex.antennas.len());
let nav = parse_rinex_nav(RINEX_NAV_TEXT).expect("parse RINEX NAV fixture");
assert!(!nav.records().is_empty() || !nav.glonass_records().is_empty());
let loaded_nav =
load_rinex_nav(fixture_path(&["nav", "ESBC00DNK_R_20201770000_01D_MN.rnx"]))
.expect("load RINEX NAV");
assert_eq!(loaded_nav.records().len(), nav.records().len());
assert_eq!(
loaded_nav.glonass_records().len(),
nav.glonass_records().len()
);
let obs = parse_rinex_obs(RINEX_OBS_TEXT).expect("parse RINEX OBS fixture");
assert!(!obs.epochs().is_empty());
let loaded_obs = load_rinex_obs(fixture_path(&[
"obs",
"ESBC00DNK_R_20201770000_01D_30S_MO_trim.rnx",
]))
.expect("load RINEX OBS");
assert_eq!(loaded_obs.epochs().len(), obs.epochs().len());
let clock = parse_rinex_clock(RINEX_CLOCK_TEXT).expect("parse RINEX clock fixture");
assert!(!clock.series_rows().is_empty());
let loaded_clock = load_rinex_clock(fixture_path(&["clk", "synthetic_rinex_clock.clk"]))
.expect("load RINEX clock");
assert_eq!(loaded_clock.series_rows().len(), clock.series_rows().len());
let lossy_clock = parse_rinex_clock_lossy("AS malformed\n");
assert!(lossy_clock.series_rows().is_empty());
let loaded_lossy_clock =
load_rinex_clock_lossy(fixture_path(&["clk", "synthetic_rinex_clock.clk"]))
.expect("load lossy RINEX clock");
assert_eq!(
loaded_lossy_clock.series_rows().len(),
clock.series_rows().len()
);
let bias = parse_bias_sinex(BIAS_BYTES).expect("parse Bias-SINEX fixture");
assert_eq!(bias.records().len(), 351);
let loaded_bias = load_bias_sinex(fixture_path(&[
"bias",
"COD0OPSFIN_20261330000_01D_01D_OSB.BIA.gz",
]))
.expect("load gzip Bias-SINEX");
assert!(!loaded_bias.records().is_empty());
let lossy_bias = parse_bias_sinex_lossy(BIAS_BYTES).expect("lossy Bias-SINEX parse");
assert_eq!(lossy_bias.value.records().len(), bias.records().len());
let dcb = parse_code_dcb(DCB_BYTES, None).expect("parse CODE DCB fixture");
assert_eq!(dcb.records().len(), 496);
let loaded_dcb =
load_code_dcb(fixture_path(&["bias", "P1C1_RINEX.DCB"]), None).expect("load CODE DCB");
assert_eq!(loaded_dcb.records().len(), dcb.records().len());
let lossy_dcb = load_code_dcb_lossy(fixture_path(&["bias", "P1C1_RINEX.DCB"]), None)
.expect("load lossy CODE DCB");
assert_eq!(lossy_dcb.value.records().len(), dcb.records().len());
let decoded = decode_crinex(CRINEX_TEXT).expect("decode CRINEX fixture");
assert!(decoded.contains("RINEX VERSION / TYPE"));
let loaded_decoded = load_crinex(fixture_path(&[
"obs",
"ESBC00DNK_R_20201770000_01D_30S_MO_trim.crx",
]))
.expect("load CRINEX");
assert_eq!(loaded_decoded, decoded);
}
#[test]
fn ssr_store_from_rtcm_ingests_real_combined_orbit_clock_frame() {
let week = sidereon_core::astro::time::model::GnssWeekTow::new(
sidereon_core::astro::time::model::TimeScale::Gpst,
2425,
344_970.0,
)
.expect("valid week");
let store = ssr_store_from_rtcm(&hex_bytes(REAL_SSRA02IGS0_1060_FRAME_HEX), week)
.expect("ingest real SSR frame");
let sat = GnssSatelliteId::new(GnssSystem::Gps, 30).expect("valid satellite");
let orbit = store.orbit(sat).expect("G30 orbit correction");
let clock = store.clock(sat).expect("G30 clock correction");
assert_eq!(orbit.iode, 90);
assert!((orbit.radial_m + 0.0807).abs() < 1.0e-12);
assert!((orbit.along_m + 0.2484).abs() < 1.0e-12);
assert!((orbit.cross_m - 0.1396).abs() < 1.0e-12);
assert!((clock.c0_m - 0.0166).abs() < 1.0e-12);
}
#[test]
fn product_ingestion_wrappers_map_errors() {
let err = match parse_rinex_nav("not a RINEX NAV file") {
Ok(_) => panic!("invalid RINEX NAV unexpectedly parsed"),
Err(err) => err,
};
assert!(matches!(err, Error::RinexNav(_)));
assert!(std::error::Error::source(&err).is_some());
let err = match parse_rinex_nav(
" 4.00 NAVIGATION DATA M RINEX VERSION / TYPE\n\
XXX END OF HEADER\n\
> EPH G01 LNAV\n",
) {
Ok(_) => panic!("empty v4 EPH frame unexpectedly parsed"),
Err(err) => err,
};
assert!(matches!(err, Error::RinexNav(_)));
let err = parse_rinex_obs("not a RINEX OBS file").unwrap_err();
assert!(matches!(err, Error::RinexObs(_)));
assert!(std::error::Error::source(&err).is_some());
let err = parse_rinex_clock("AS malformed").unwrap_err();
assert!(matches!(err, Error::RinexClock(_)));
assert!(std::error::Error::source(&err).is_some());
let err = parse_code_dcb(b"not a DCB file", None).unwrap_err();
assert!(matches!(err, Error::Bias(_)));
assert!(std::error::Error::source(&err).is_some());
let err = decode_crinex("not a CRINEX file\n").unwrap_err();
assert!(matches!(err, Error::Crinex(_)));
let rendered = err.to_string();
assert!(rendered.starts_with("CRINEX decode failed:"), "{rendered}");
assert!(!rendered.contains("SP3 parse failed"), "{rendered}");
assert!(std::error::Error::source(&err).is_some());
let missing = fixture_path(&["missing.nope"]);
let err = match load_rinex_nav(missing) {
Ok(_) => panic!("missing RINEX NAV path unexpectedly loaded"),
Err(err) => err,
};
assert!(matches!(err, Error::Io(_)));
assert!(std::error::Error::source(&err).is_some());
}
#[test]
fn astro_umbrella_reexports_broader_astrodynamics_surface() {
let budget = astro::rf::LinkBudget {
eirp_dbw: 0.0,
fspl_db: 165.0,
receiver_gt_dbk: -12.0,
other_losses_db: 3.0,
required_cn0_dbhz: 35.0,
};
assert_eq!(
astro::rf::link_margin(&budget)
.expect("valid RF link budget")
.to_bits(),
13.599999999999994_f64.to_bits()
);
let ts =
astro::time::TimeScales::from_utc(2000, 1, 1, 12, 0, 0.0).expect("valid UTC instant");
assert!((ts.jd_tt - 2451545.0).abs() < 1.0e-3);
let sun = [149_597_870.7, 0.0, 0.0];
assert_eq!(
astro::events::eclipse::status([-7000.0, 0.0, 0.0], sun)
.expect("valid eclipse geometry"),
astro::events::eclipse::EclipseStatus::Umbra
);
assert!(
(astro::angles::beta_angle([1.0, 0.0, 0.0], sun).expect("valid beta geometry") - 90.0)
.abs()
<= 1.0e-9
);
let sep = astro::angles::angular_separation_coords(
(101.287155333, -16.716115861),
(114.825493028, 5.224993306),
)
.expect("valid angular separation");
assert!((sep - 25.7013646403623).abs() <= 1.0e-9);
let pa = astro::angles::position_angle(
(101.287155333, -16.716115861),
(114.825493028, 5.224993306),
)
.expect("valid position angle");
let pa_diff = {
let diff = (pa - 32.51673660099302).abs();
diff.min(360.0 - diff)
};
assert!(pa_diff <= 1.0e-9);
assert!(astro::covariance::symmetric(&[
[1.0, 0.1, 0.2],
[0.1, 2.0, 0.3],
[0.2, 0.3, 3.0]
]));
assert!(core::mem::size_of::<astro::bodies::SunMoon>() > 0);
assert!(core::mem::size_of::<astro::cdm::CdmKvn>() > 0);
assert!(core::mem::size_of::<astro::conjunction::ConjunctionState>() > 0);
assert!(core::mem::size_of::<astro::frames::transforms::TemeStateKm>() > 0);
assert!(core::mem::size_of::<astro::omm::Omm>() > 0);
assert!(core::mem::size_of::<astro::tdm::Tdm>() > 0);
}
#[test]
fn ground_site_sun_moon_helpers_reachable_through_facade() {
use astro::bodies::{
find_moon_elevation_crossings, find_moon_transits, moon_az_el, moon_illumination,
sun_az_el, MoonElevationOptions,
};
use astro::frames::transforms::{itrs_to_topocentric, GeodeticStationKm};
use astro::passes::UtcInstant;
let station = GeodeticStationKm {
latitude_deg: 51.4769,
longitude_deg: 0.0,
altitude_km: 0.046,
};
let noon = UtcInstant::from_utc(2024, 6, 20, 12, 1, 42, 0).expect("valid UTC");
let sun = sun_az_el(&station, noon).expect("sun geometry");
assert!((sun.elevation_deg - 61.96).abs() < 0.5);
let full = UtcInstant::from_utc(2024, 4, 23, 23, 49, 0, 0).expect("valid UTC");
let illum = moon_illumination(&station, full).expect("moon illumination");
assert!(illum.illuminated_fraction > 0.95);
let moon = moon_az_el(&station, full).expect("moon geometry");
assert!((350_000.0..410_000.0).contains(&moon.range_km));
let start = UtcInstant::from_utc(2024, 4, 23, 0, 0, 0, 0).expect("valid UTC");
let end = UtcInstant::from_utc(2024, 4, 24, 0, 0, 0, 0).expect("valid UTC");
assert_eq!(
find_moon_elevation_crossings(&station, start, end, MoonElevationOptions::default())
.expect("moon crossings")
.len(),
2
);
assert_eq!(
find_moon_transits(&station, start, end, 300.0, 1.0)
.expect("moon transits")
.len(),
2
);
let (_az, el, _range) =
itrs_to_topocentric([0.0, 0.0, 7000.0], &station).expect("topocentric");
assert!(el.is_finite());
let kernel = astro::Spk::from_bytes(include_bytes!(
"../../sidereon-core/tests/fixtures/bodies/observe_de.bsp"
))
.expect("fixture SPK");
let observe_time = UtcInstant::from_utc(2024, 1, 1, 0, 0, 0, 0).expect("valid UTC");
let mars = observe_spk_body(&station, observe_time, &kernel, 4).expect("Mars observation");
assert!(mars.apparent.right_ascension_deg.is_finite());
assert!(!mars.reduced);
let target = Target::Spk {
kernel: &kernel,
naif_id: 4,
};
let same = observe(&station, observe_time, target, ObserveOptions::default())
.expect("generic observation");
assert_eq!(
same.apparent.right_ascension_deg.to_bits(),
mars.apparent.right_ascension_deg.to_bits()
);
let tod =
gcrs_to_true_of_date_matrix(&observe_time.time_scales()).expect("true-of-date matrix");
assert!(tod[0][0].is_finite());
let gcrs_state = astro::frames::transforms::TemeStateKm {
position_km: [7000.0, 100.0, -50.0],
velocity_km_s: [0.0, 7.5, 0.1],
};
let (teme_pos, _) = gcrs_to_teme_compute(&gcrs_state, &observe_time.time_scales(), false)
.expect("TEME inverse transform");
assert!(teme_pos.0.is_finite());
}
#[test]
fn tca_shortcut_screens_two_real_tles_over_one_day() {
let primary_tle = station_tle("ISS (ZARYA)");
let secondary_tle = station_tle("CSS (TIANHE)");
let primary = sgp4::Satellite::from_tle(primary_tle.line1, primary_tle.line2)
.expect("station TLE parses");
let window = tca::TcaWindow::from_start_and_duration_seconds(
primary.epoch_jd(),
sidereon_core::constants::SECONDS_PER_DAY,
)
.expect("valid one-day window");
let options = tca::TcaFinderOptions {
coarse_step_seconds: 120.0,
time_tolerance_seconds: 1.0e-2,
};
let candidates =
tca::find_tca_candidates_between_tles(primary_tle, secondary_tle, window, options)
.expect("real TLE TCA search succeeds");
assert!(!candidates.is_empty());
let best = candidates
.iter()
.min_by(|a, b| a.miss_distance_km.total_cmp(&b.miss_distance_km))
.expect("candidate set is nonempty");
assert!(best.tca_seconds_since_window_start > 0.0);
assert!(best.tca_seconds_since_window_start < sidereon_core::constants::SECONDS_PER_DAY);
assert!(best.miss_distance_km.is_finite());
assert!(best.miss_distance_km > 0.0);
assert!((norm3(best.relative_position_km) - best.miss_distance_km).abs() < 1.0e-9);
assert!(norm3(best.relative_velocity_km_s) > 0.0);
let secondaries = [secondary_tle];
let threshold_km = best.miss_distance_km + 1.0;
let serial = tca::screen_tca_candidates_from_tle_catalog_serial(
primary_tle,
&secondaries,
window,
threshold_km,
options,
)
.expect("serial real TLE screening succeeds");
let parallel = tca::screen_tca_candidates_from_tle_catalog_parallel(
primary_tle,
&secondaries,
window,
threshold_km,
options,
)
.expect("parallel real TLE screening succeeds");
assert_eq!(serial, parallel);
assert!(!serial.is_empty());
assert!(serial.iter().all(|hit| hit.secondary_index == 0));
assert!(serial
.iter()
.all(|hit| hit.candidate.miss_distance_km <= threshold_km));
let pc_options = tca::TcaPcOptions::with_default_covariance(
0.020,
astro::conjunction::PcMethod::Alfano2005,
);
let conjunctions = tca::find_tca_conjunctions_between_tles(
primary_tle,
secondary_tle,
window,
options,
pc_options,
)
.expect("real TLE TCA Pc search succeeds");
assert_eq!(conjunctions.len(), candidates.len());
assert!(conjunctions.iter().all(|conjunction| {
conjunction.collision_probability.pc.is_finite()
&& (0.0..=1.0).contains(&conjunction.collision_probability.pc)
}));
}
#[test]
fn gnss_utility_modules_are_reexported() {
assert_eq!(
frequencies::frequency_hz(GnssSystem::Gps, frequencies::CarrierBand::L1),
Some(constants::F_L1_HZ)
);
assert_eq!(GnssSystem::Gps.as_str(), "GPS");
assert_eq!(GnssSystem::Gps.to_string(), "GPS");
assert_eq!(frequencies::CarrierBand::L1.as_str(), "l1");
assert_eq!(frequencies::CarrierBand::L1.to_string(), "l1");
assert!(geometry::visible_at_elevation_mask(5.0, 5.0));
assert!(combinations::gamma(constants::F_L1_HZ, constants::F_L2_HZ)
.expect("GPS L1/L2 gamma")
.is_finite());
assert_eq!(
carrier_phase::geometry_free(100.0, 60.0)
.expect("finite geometry-free combination")
.to_bits(),
40.0_f64.to_bits()
);
assert!(quality::pseudorange_variance(
30.0,
quality::PseudorangeVarianceOptions::default()
)
.expect("positive elevation variance")
.is_finite());
assert_eq!(
signal::ca_code(1).expect("GPS PRN 1").len(),
signal::CA_CODE_LENGTH
);
assert_eq!(
velocity::doppler_to_range_rate(-1.0, constants::F_L1_HZ)
.expect("valid Doppler conversion")
.to_bits(),
(constants::C_M_S / constants::F_L1_HZ).to_bits()
);
assert_eq!(navigation::lnav::PREAMBLE, 0b1000_1011);
assert_eq!(dgnss::CodeObservation::new("G01", 1.0).satellite_id, "G01");
assert_eq!(
sbas::sat_to_sbas_prn(sbas::sbas_prn_to_sat(120).expect("valid augmentation PRN")),
Some(120)
);
assert!(core::mem::size_of::<geometry::VisibilityOptions>() > 0);
assert!(core::mem::size_of::<broadcast_comparison::EpochInputs>() > 0);
}
#[test]
fn solve_spp_delegates_to_the_core_solver_and_maps_errors() {
let sp3 = load_sp3(DEGENERATE_SP3).expect("the fixture parses");
let sat = |prn| GnssSatelliteId::new(GnssSystem::Gps, prn).expect("valid satellite id");
let inputs = SolveInputs {
observations: vec![
Observation {
satellite_id: sat(1),
pseudorange_m: 2.1e7,
},
Observation {
satellite_id: sat(2),
pseudorange_m: 2.1e7,
},
],
t_rx_j2000_s: 646_315_200.0,
t_rx_second_of_day_s: 0.0,
day_of_year: 176.0,
initial_guess: [0.0, 0.0, 0.0, 0.0],
corrections: Corrections::NONE,
klobuchar: KlobucharCoeffs {
alpha: [0.0; 4],
beta: [0.0; 4],
},
beidou_klobuchar: None,
galileo_nequick: None,
sbas_iono: None,
glonass_channels: std::collections::BTreeMap::new(),
met: SurfaceMet {
pressure_hpa: 1013.25,
temperature_k: 288.15,
relative_humidity: 0.5,
},
robust: None,
};
let result = solve_spp(&sp3, &inputs, false, SolvePolicy::default());
assert!(matches!(result, Err(Error::Spp(_))), "got {result:?}");
if let Err(err) = result {
assert!(err.to_string().contains("SPP solve failed"));
assert!(std::error::Error::source(&err).is_some());
}
}
#[test]
fn spp_robust_fde_driver_is_reexported_from_facade_root() {
let sp3 = load_sp3(DEGENERATE_SP3).expect("the fixture parses");
let sat = |prn| GnssSatelliteId::new(GnssSystem::Gps, prn).expect("valid satellite id");
let inputs = SolveInputs {
observations: vec![
Observation {
satellite_id: sat(1),
pseudorange_m: 2.1e7,
},
Observation {
satellite_id: sat(2),
pseudorange_m: 2.1e7,
},
],
t_rx_j2000_s: 646_315_200.0,
t_rx_second_of_day_s: 0.0,
day_of_year: 176.0,
initial_guess: [0.0, 0.0, 0.0, 0.0],
corrections: Corrections::NONE,
klobuchar: KlobucharCoeffs {
alpha: [0.0; 4],
beta: [0.0; 4],
},
beidou_klobuchar: None,
galileo_nequick: None,
sbas_iono: None,
glonass_channels: std::collections::BTreeMap::new(),
met: SurfaceMet {
pressure_hpa: 1013.25,
temperature_k: 288.15,
relative_humidity: 0.5,
},
robust: None,
};
let options = quality::FdeSppOptions {
fde: quality::FdeOptions {
raim: quality::RaimOptions::default(),
max_iterations: 0,
},
validation: quality::SolutionValidationOptions::default(),
};
let result = spp_robust_fde_driver(
&sp3,
&inputs,
false,
positioning::RobustConfig::default(),
&options,
);
assert!(
matches!(
result,
Err(quality::FdeError::Solve(quality::FdeSppError::Spp(_)))
),
"got {result:?}"
);
}
#[test]
fn solve_velocity_delegates_to_core_solver_and_maps_errors() {
let sp3 = load_sp3(DEGENERATE_SP3).expect("the fixture parses");
let result = solve_velocity(
&sp3,
&[],
[0.0; 3],
646_315_200.0,
VelocitySolveOptions::default(),
);
assert!(
matches!(
result,
Err(Error::Velocity(velocity::VelocityError::NoObservations))
),
"got {result:?}"
);
if let Err(err) = result {
assert!(err.to_string().contains("velocity solve failed"));
assert!(std::error::Error::source(&err).is_some());
}
}
#[test]
fn solve_rtk_float_positional_wrapper_maps_errors() {
let epochs = Vec::new();
let ambiguity_ids = Vec::<String>::new();
let model = rtk_model();
let err = solve_rtk_float(
&epochs,
[0.0; 3],
&ambiguity_ids,
[0.0; 3],
&model,
rtk_float_options(),
None,
)
.unwrap_err();
assert!(matches!(err, Error::RtkFloat(_)));
assert!(err.to_string().contains("RTK float"));
assert!(std::error::Error::source(&err).is_some());
}
#[test]
fn solve_rtk_fixed_positional_wrapper_maps_errors() {
let epochs = Vec::new();
let ambiguity_ids = Vec::<String>::new();
let ambiguity_satellites = BTreeMap::new();
let wavelengths_m = BTreeMap::new();
let offsets_m = BTreeMap::new();
let float_only_systems = Vec::new();
let model = rtk_model();
let ambiguity_set = AmbiguitySet {
ids: &ambiguity_ids,
satellites: &ambiguity_satellites,
scale: rtk_filter::AmbiguityScale {
wavelengths_m: &wavelengths_m,
offsets_m: &offsets_m,
},
float_only_systems: &float_only_systems,
};
let err = solve_rtk_fixed(
&epochs,
[0.0; 3],
ambiguity_set,
[0.0; 3],
&model,
rtk_fixed_options(),
None,
)
.unwrap_err();
assert!(matches!(err, Error::RtkFixed(_)));
assert!(err.to_string().contains("fixed RTK"));
assert!(std::error::Error::source(&err).is_some());
}
#[test]
fn solve_ppp_float_positional_wrapper_maps_errors_with_fixture_source() {
let sp3 = load_sp3(DEGENERATE_SP3).expect("the fixture parses");
let epochs = Vec::new();
let err = solve_ppp_float(&sp3, &epochs, empty_ppp_state(), ppp_float_solve_config())
.unwrap_err();
assert!(matches!(err, Error::PppFloat(_)));
assert!(err.to_string().contains("PPP float"));
assert!(std::error::Error::source(&err).is_some());
}
#[test]
fn solve_ppp_fixed_positional_wrapper_maps_errors_with_fixture_source() {
let sp3 = load_sp3(DEGENERATE_SP3).expect("the fixture parses");
let epochs = Vec::new();
let err = solve_ppp_fixed(
&sp3,
&epochs,
empty_ppp_float_solution(),
ppp_fixed_solve_config(),
)
.unwrap_err();
assert!(matches!(err, Error::PppFixed(_)));
assert!(err.to_string().contains("PPP"));
assert!(std::error::Error::source(&err).is_some());
}
#[test]
fn solve_rtk_float_with_delegates_to_positional_solver() {
let epochs = Vec::new();
let ambiguity_ids = Vec::new();
let model = rtk_model();
let config = RtkFloatConfig {
epochs: &epochs,
base_ecef_m: [0.0; 3],
ambiguity_ids: &ambiguity_ids,
initial_baseline_m: [0.0; 3],
model: &model,
options: rtk_float_options(),
receiver_antenna_corrections: None,
};
let typed = solve_rtk_float_with(config.clone()).unwrap_err();
let positional = solve_rtk_float(
config.epochs,
config.base_ecef_m,
config.ambiguity_ids,
config.initial_baseline_m,
config.model,
config.options,
config.receiver_antenna_corrections,
)
.unwrap_err();
assert!(matches!(typed, Error::RtkFloat(_)));
assert_eq!(typed.to_string(), positional.to_string());
}
#[test]
fn solve_rtk_float_with_rejects_receiver_antenna_zero_base_geometry() {
let base = [0.0; 3];
let baseline = [1.0, 0.0, 0.0];
let rover = [
base[0] + baseline[0],
base[1] + baseline[1],
base[2] + baseline[2],
];
let g01 = [15_000_000.0, 7_000_000.0, 21_000_000.0];
let g02 = [-12_000_000.0, 18_000_000.0, 19_000_000.0];
let range_m = |sat: [f64; 3], recv: [f64; 3]| {
let dx = sat[0] - recv[0];
let dy = sat[1] - recv[1];
let dz = sat[2] - recv[2];
(dx * dx + dy * dy + dz * dz).sqrt()
};
let mk = |sat: [f64; 3], id: &str| rtk_filter::SatMeas {
sat: id.into(),
sd_ambiguity_id: id.into(),
base_code_m: range_m(sat, base),
base_phase_m: range_m(sat, base),
rover_code_m: range_m(sat, rover),
rover_phase_m: range_m(sat, rover),
base_tx_pos: sat,
rover_tx_pos: sat,
pos: sat,
};
let epochs = vec![rtk_filter::Epoch {
references: vec![mk(g01, "G01")],
nonref: vec![mk(g02, "G02")],
velocity_mps: None,
dt_s: 0.0,
}];
let ambiguity_ids = vec!["G02".to_string()];
let model = rtk_model();
let cal = rtk_filter::ReceiverAntennaCalibration {
pco_neu_m: [0.0, 0.0, 0.0],
noazi_pcv_m: vec![(0.0, 0.0)],
azi_pcv_m: Vec::new(),
};
let corrections = ReceiverAntennaCorrections {
base: cal.clone(),
rover: cal,
};
let config =
RtkFloatConfig::new(&epochs, base, &ambiguity_ids, &model, rtk_float_options())
.with_initial_baseline_m(baseline)
.with_receiver_antenna_corrections(Some(&corrections));
let err = solve_rtk_float_with(config).unwrap_err();
assert!(matches!(
err,
Error::RtkFloat(rtk_filter::FloatSolveError::ReceiverAntenna(
rtk_filter::ReceiverAntennaError::InvalidGeometry
))
));
}
#[test]
fn solve_rtk_fixed_with_delegates_to_positional_solver() {
let epochs = Vec::new();
let ambiguity_ids = Vec::new();
let ambiguity_satellites = BTreeMap::new();
let wavelengths_m = BTreeMap::new();
let offsets_m = BTreeMap::new();
let float_only_systems = Vec::new();
let model = rtk_model();
let ambiguity_set = AmbiguitySet {
ids: &ambiguity_ids,
satellites: &ambiguity_satellites,
scale: rtk_filter::AmbiguityScale {
wavelengths_m: &wavelengths_m,
offsets_m: &offsets_m,
},
float_only_systems: &float_only_systems,
};
let config = RtkFixedConfig {
epochs: &epochs,
base_ecef_m: [0.0; 3],
initial_ambiguities: ambiguity_set,
initial_baseline_m: [0.0; 3],
model: &model,
options: rtk_fixed_options(),
receiver_antenna_corrections: None,
};
let typed = solve_rtk_fixed_with(config.clone()).unwrap_err();
let positional = solve_rtk_fixed(
config.epochs,
config.base_ecef_m,
config.initial_ambiguities,
config.initial_baseline_m,
config.model,
config.options,
config.receiver_antenna_corrections,
)
.unwrap_err();
assert!(matches!(typed, Error::RtkFixed(_)));
assert_eq!(typed.to_string(), positional.to_string());
}
#[test]
fn solve_ppp_float_with_delegates_to_positional_solver() {
let sp3 = load_sp3(DEGENERATE_SP3).expect("the fixture parses");
let epochs = Vec::new();
let config = PppFloatConfig {
source: &sp3,
epochs: &epochs,
initial_state: empty_ppp_state(),
solve: ppp_float_solve_config(),
};
let typed = solve_ppp_float_with(config.clone()).unwrap_err();
let positional = solve_ppp_float(
config.source,
config.epochs,
config.initial_state,
config.solve,
)
.unwrap_err();
assert!(matches!(typed, Error::PppFloat(_)));
assert_eq!(typed.to_string(), positional.to_string());
}
#[test]
fn solve_ppp_fixed_with_delegates_to_positional_solver() {
let sp3 = load_sp3(DEGENERATE_SP3).expect("the fixture parses");
let epochs = Vec::new();
let config = PppFixedConfig {
source: &sp3,
epochs: &epochs,
float_solution: empty_ppp_float_solution(),
solve: ppp_fixed_solve_config(),
};
let typed = solve_ppp_fixed_with(config.clone()).unwrap_err();
let positional = solve_ppp_fixed(
config.source,
config.epochs,
config.float_solution,
config.solve,
)
.unwrap_err();
assert!(matches!(typed, Error::PppFixed(_)));
assert_eq!(typed.to_string(), positional.to_string());
}
}