1use core::fmt;
178use flate2::read::GzDecoder;
179use std::io::Read;
180use std::path::Path;
181
182pub use sidereon_core::astro::forces::{
185 EarthRadiationPressure, SchwarzschildRelativity, SolarRadiationPressure,
186 SphericalHarmonicCoefficient, SphericalHarmonicGravity, SphericalHarmonicGravityConfig,
187 ThirdBodyBodies, ThirdBodyGravity, ZonalCoefficients, ZonalDegrees, ZonalGravity,
188 EGM96_DEGREE_ORDER_36, EGM96_EMBEDDED_MAX_DEGREE, EGM96_EMBEDDED_MAX_ORDER, EGM96_MU_KM3_S2,
189 EGM96_REFERENCE_RADIUS_KM,
190};
191pub use sidereon_core::astro::frames::transforms::{
192 gcrs_to_topocentric_compute, geodetic_from_ecef_proj, geodetic_to_itrs,
193 itrs_to_geodetic_compute, itrs_to_topocentric, FrameTransformError, GeodeticStationKm,
194};
195pub use sidereon_core::astro::frames::{
196 EarthOrientation, EarthOrientationProvider, TdbEarthOrientationProvider,
197};
198pub use sidereon_core::astro::propagator::{ForceModelComponents, ForceModelKind};
199pub use sidereon_core::geometry_quality::{
200 classify, GeometryQuality, GeometryQualityThresholds, ObservabilityTier,
201};
202pub use sidereon_core::positioning::{
203 solve_spp_from_rinex_obs, spp_inputs_from_rinex_obs, spp_inputs_from_rtcm_msm,
204 RinexSppAssemblySource, RinexSppBroadcastCorrections, RinexSppEpochInputs,
205 RinexSppEpochSolution, RinexSppError, RinexSppOptions, RinexSppSource, RtcmSppEpochInputs,
206};
207pub use sidereon_core::quality::{
208 reliability_araim, reliability_design, spp_robust_fde_driver, wtest_noncentrality,
209 wtest_noncentrality_components, ObservationReliability, RangeReliabilityRow,
210 ReliabilityOptions, ReliabilityReport, ReliabilitySummary, WtestNoncentralityComponents,
211};
212pub use sidereon_core::static_positioning::{
213 solve_static, StaticClockBias, StaticCovariance, StaticEpoch, StaticEpochInfluence,
214 StaticInfluenceStatus, StaticResidual, StaticSatelliteBatchInfluence, StaticSatelliteInfluence,
215 StaticSolution, StaticSolutionMetadata, StaticSolveError, StaticSolveOptions,
216};
217pub use sidereon_core::{
218 antex, araim, astro, atmosphere, bias, broadcast_comparison, carrier_phase, clock_stability,
219 combinations, constants, constellation, data, dgnss, dop, ephemeris, frame_catalog,
220 frequencies, fusion, geodesic, geodetic_time_series, geofence, geometry, geometry_quality, ils,
221 inertial, navigation, nmea, observables, orbit, positioning, ppp_corrections, qc_obs, quality,
222 rinex, rtcm, rtk, sbas, sbas_pl, sidereal, signal, source_localization, ssr, staleness,
223 static_positioning, terrain, terrain_store, tides, velocity,
224};
225pub use sidereon_core::{
226 catalog, catalog_entry, propagate_position, transform, transform_from_epoch, FrameCatalogError,
227 HelmertParameters, HelmertRates, HelmertTransform, TerrestrialFrame, TerrestrialPositionM,
228 TerrestrialState, TerrestrialVelocityMPerYear, TERRESTRIAL_FRAME_CATALOG,
229};
230pub use sidereon_core::{
231 containment, containment_probability, containment_probability_with_options, crossing,
232 crossing_probability, crossing_probability_with_options, distance_to_boundary, CrossingEvent,
233 CrossingKind, Fence, GeofenceError, GeofencePositionEstimate, PositionUncertainty,
234 ProbabilityHysteresis, ProbabilityMethod, ProbabilityOptions, GEOFENCE_BOUNDARY_TOLERANCE_M,
235 PLANAR_FAST_PATH_MAX_RADIUS_M,
236};
237pub use sidereon_core::{
238 emission_media_batch_at_j2000_s, observable_media_corrections, predict_batch_with_media,
239 predict_batch_with_media_parallel, predict_ranges_with_media, predict_with_media,
240 AppliedMediaCorrections, EmissionMediaBatch, EmissionMediaBatchOptions, EmissionMediaStatus,
241 MediaPredictOptions, MediaPredictedObservables, MediaRangePrediction,
242 ObservableIonosphereCorrection, ObservableMediaOptions, ObservableTroposphereCorrection,
243};
244pub use sidereon_core::{
245 error_ellipse_from_enu_m2, horizontal_radius_at, metrics_from_ecef_covariance_m2,
246 metrics_from_enu_covariance_m2, metrics_from_kinematic_solution,
247 metrics_from_position_covariance, spherical_radius_at, vertical_radius_at, ErrorEllipse,
248 ErrorMetricsError, PercentileRadius, PositionErrorMetrics,
249};
250pub use sidereon_core::{
251 gauss_markov_bias_decay, gauss_markov_bias_variance_increment, gravity_ecef_mps2,
252 mechanize_ecef, normal_gravity_mps2, rodrigues_delta_dcm, simulate_imu_samples,
253 simulate_imu_samples_from_increments, true_imu_increment_between, AttitudeQuaternion,
254 ConingCorrection, CorrectedImuIncrement, ImuBias, ImuCalibration, ImuErrorModel, ImuGrade,
255 ImuRateRandomWalk, ImuSample, ImuSampleKind, ImuSimulationOptions, ImuSimulationOutput,
256 ImuSimulator, ImuSpec, InertialError, MechanizationConfig, NavState, SimulatedImuSequence,
257 StrapdownMechanizer, DEFAULT_IMU_SIM_SEED, WGS84_NORMAL_GRAVITY_EQUATOR_MPS2,
258 WGS84_NORMAL_GRAVITY_POLE_MPS2, WGS84_SOMIGLIANA_K,
259};
260pub use sidereon_core::{
261 geodesic_direct, geodesic_inverse, geodetic_to_itrf, itrf_to_geodetic, FrameValueError,
262 GeodesicError, GnssSatelliteId, GnssSystem, ItrfPositionM, ItrfVelocityMS, ProtectionModel,
263 SatelliteIdError, Wgs84Geodetic,
264};
265pub use sidereon_core::{
266 orbit_repeat_lag, periodicity_strength, periodicity_strength_with_sample_interval,
267 repeat_period, sidereal_filter, solar_day_period, SiderealFilterError, SiderealFilterOptions,
268 SiderealFilterOutput, SiderealTemplateMethod, SIDEREAL_DAY_NANOS, SIDEREAL_DAY_SECONDS,
269};
270pub use sidereon_core::{
271 sbas_protection_levels, AirborneModel, DegradationParams, ProtectionGeometry, ProtectionRow,
272 SbasErrorModel, SbasKMultipliers, SbasPlError, SbasProtection, SbasSisError,
273};
274pub use sidereon_core::{
275 ukf_correct_closed_loop, F64Bits, FusionFilterKind, FusionStateCodecError,
276 SerializableErrorStateLayout, SerializableFusionSnapshot, SerializableFusionState,
277 SerializableImuSample, SerializableImuSampleKind, SerializableInsFilterState,
278 SerializableLooseMeasurement, SerializableNavState, SerializableRateEndpoint,
279 SerializableSatelliteId, SerializableStoredCheckpoint, SerializableStoredGnssMeasurement,
280 SerializableStoredImuSample, SerializableTightCarrierPhaseObservation,
281 SerializableTightFilterState, SerializableTightGnssEpoch, SerializableTightGnssObservation,
282 SerializableTightRangeRateObservation, SerializableTimeSyncHistory,
283 SerializableTimeSyncHistoryConfig, UkfUpdateOptions, UnscentedTransformOptions,
284 FUSION_STATE_CODEC_VERSION,
285};
286pub use sidereon_core::{RejectedSat, RejectionReason};
287
288pub mod rtk_filter {
291 pub use sidereon_core::rtk_filter::{
292 fix_wide_lane_rtk_arc, prepare_ionosphere_free_rtk_arc, solve_moving_baseline,
293 solve_moving_baseline_epoch, solve_rtk_arc, solve_static_rtk_arc,
294 solve_wide_lane_fixed_rtk_arc, AmbiguityScale, AmbiguitySearch, AmbiguitySet,
295 CycleSlipOptions, CycleSlipPolicy, CycleSlipSplitArc, Epoch, FixedBaselineSolution,
296 FixedSolveError, FixedSolveOpts, FloatBaselineSolution, FloatResidual, FloatSolveError,
297 FloatSolveOpts, FloatSolveStatus, FullSetIntegerSummary, IntegerSearchMeta, IntegerStatus,
298 IonosphereFreeBaselineError, MeasModel, MovingBaselineEpoch, MovingBaselineEpochSolution,
299 MovingBaselineError, MovingBaselineOpts, MovingBaselineSequenceError, MovingBaselineStatus,
300 PartialSearchMeta, ReceiverAntennaCalibration, ReceiverAntennaCorrections,
301 ReceiverAntennaError, ResidualComponentKind, ResidualValidationMeta,
302 ResidualValidationOpts, ResidualValidationOutlier, RtkArcConfig, RtkArcEpoch,
303 RtkArcEpochSolution, RtkArcError, RtkArcObservation, RtkArcPreprocessing, RtkArcSolution,
304 RtkDualCycleSlipConfig, RtkDualFrequencyArcEpoch, RtkDualFrequencyObservation,
305 RtkDualFrequencySatelliteObservation, RtkIonosphereFreeArcConfig,
306 RtkIonosphereFreeArcError, RtkIonosphereFreeArcSolution, RtkStaticArcConfig,
307 RtkStaticArcError, RtkStaticArcSolution, RtkWideLaneArcConfig, RtkWideLaneArcError,
308 RtkWideLaneArcSolution, RtkWideLaneFixedArcConfig, RtkWideLaneFixedArcError,
309 RtkWideLaneFixedArcIntegerMethod, RtkWideLaneFixedArcMetadata, RtkWideLaneFixedArcSolution,
310 RtkWideLaneFixedArcSolveConfig, RtkWideLaneFixedSequentialArcSolution,
311 RtkWideLaneFixedStaticArcSolution, SatMeas, StochasticModel,
312 ValidatedFixedBaselineSolution, ValidatedFixedSolveError, ValidatedFixedSolveOpts,
313 WideLaneError, WideLaneOptions,
314 };
315}
316
317pub mod geoid {
320 pub use sidereon_core::geoid::{
321 egm96_ellipsoidal_height_m, egm96_grid, egm96_orthometric_height_m, egm96_undulation,
322 egm96_undulations_deg, egm96_undulations_rad, ellipsoidal_height_m, geoid_undulation,
323 geoid_undulations_deg, geoid_undulations_rad, orthometric_height_m, Egm2008GridSpacing,
324 Egm2008RasterWindow, GeoidError, GeoidGrid, ProjVgridshiftArithmetic, ProjVgridshiftError,
325 };
326}
327
328pub mod almanac {
330 pub use sidereon_core::astro::almanac::{
331 geocentric_ecliptic, lunar_solar_eclipses, meridian_transits, moon_phase_deg, moon_phases,
332 planetary_events, seasons, AlmanacError, CulminationEvent, CulminationKind, EclipseEvent,
333 EclipseKind, EclipticLonLat, EphemerisSource, MoonPhaseEvent, MoonPhaseKind, Planet,
334 PlanetaryEvent, PlanetaryEventKind, SeasonEvent, SeasonKind, TransitBody,
335 };
336}
337
338pub mod precise_positioning {
341 pub use sidereon_core::precise_positioning::{
342 solve_ppp_auto_init_fixed, solve_ppp_auto_init_fixed_with_strategy,
343 solve_ppp_auto_init_float, solve_ppp_auto_init_float_with_strategy, AmbiguitySearch,
344 FixedAmbiguityOptions, FixedIntegerMetadata, FixedSolution, FixedSolveConfig,
345 FixedSolveError, FloatEpoch, FloatObservation, FloatResidual, FloatSolution,
346 FloatSolveConfig, FloatSolveError, FloatSolveOptions, FloatState, FloatStatus,
347 IntegerStatus, MeasurementWeights, MissingCorrection, NoEphemerisReason, PcvSample,
348 PositionCovariance, PppAutoInitError, PppAutoInitOptions, PppAutoInitStrategy,
349 PppCorrectionLookup, PppInitialGuess, RangeCorrections, ReceiverAntennaFrequency,
350 ReceiverAntennaOptions, SatelliteClockCorrections, TemporalCorrelationSummary,
351 TroposphereOptions,
352 };
353}
354
355pub mod raw {
360 pub use sidereon_core::{precise_positioning, rtk_filter};
361}
362
363pub use sidereon_core::astro::anomaly::{
366 eccentric_to_mean, eccentric_to_true, mean_to_eccentric, mean_to_true, propagate_kepler,
367 solve_kepler, true_to_eccentric, true_to_mean, AnomalyError, KeplerSolution,
368};
369pub use sidereon_core::astro::bodies::{
370 find_moon_elevation_crossings, find_moon_transits, find_sun_elevation_crossings, moon_az_el,
371 moon_elevation_deg, moon_illumination, observe, observe_spk_body, sun_az_el, sun_elevation_deg,
372 BodyAzEl, BodyObservationError, Ecliptic, Equatorial, Horizontal, MoonElevationCrossing,
373 MoonElevationCrossingKind, MoonElevationOptions, MoonIllumination, MoonTransit,
374 MoonTransitKind, Observation, ObserveOptions, Refraction, SunElevationCrossing,
375 SunElevationCrossingKind, SunElevationOptions, Target,
376};
377pub use sidereon_core::astro::doppler::{
378 doppler_shift, range_rate_and_ratio, DopplerError, DopplerShift,
379};
380pub use sidereon_core::astro::passes::{
381 ground_track, look_angle, look_angle_arc, look_angle_batch_parallel, look_angle_batch_serial,
382 GroundStation, LookAngle, LookAngleError, PassError, PassPredictionOptions, PredictedPass,
383 UtcInstant, VisibleSatellite,
384};
385pub mod covariance {
386 pub use sidereon_core::astro::covariance::{
387 covariance6_km_to_m, covariance6_m_to_km, eci_to_rtn_covariance6,
388 interpolate_covariance_psd, rtn_to_eci, rtn_to_eci_covariance6, rtn_to_eci_rotation,
389 symmetric, Covariance6, Covariance6Error, Mat6, RtnFrameError,
390 };
391}
392pub use sidereon_core::astro::forces::{
393 DragForce, SourcedDragForce, SpaceWeather, SpaceWeatherSource,
394};
395pub use sidereon_core::astro::frames::transforms::{
396 gcrs_to_teme_compute, gcrs_to_true_of_date_matrix,
397};
398pub use sidereon_core::astro::sgp4::{DecayLatch, DecayLatchedError, Loss, XScale};
399pub use sidereon_core::astro::space_weather::{
400 ObservationClass, SpaceWeatherPolicy, SpaceWeatherSample, SpaceWeatherTable,
401};
402pub use sidereon_core::astro::{
403 omm, passes, propagator, sgp4, space_weather, state, tca, tdm, tle,
404};
405pub use sidereon_core::ephemeris::{
406 fit_precise_ephemeris_state_sample_orbit, fit_precise_ephemeris_state_sample_orbits,
407 precise_interpolant_store_checksum64, sp3_ecef_state_to_eci, MmapPreciseEphemerisInterpolant,
408 OrientedPreciseEphemerisStateSample, PreciseEphemerisInterpolant, PreciseEphemerisStateSample,
409 PreciseInterpolantStoreError,
410};
411
412pub use sidereon_core::astro::relative;
432
433pub mod least_squares {
441 pub use sidereon_core::astro::math::least_squares::{
442 covariance_from_jacobian, covariance_from_report, normal_covariance,
443 };
444}
445
446pub mod rinex_qc {
448 pub use sidereon_core::rinex::qc::{
449 AppliedEdit, Finding, FindingRef, HeaderEditError, LintReport, NavRepair, ObsHeaderEdit,
450 ObsRepair, RepairAction, RepairOptions, Severity,
451 };
452}
453
454use sidereon_core::antex::{Antex, AntexError};
455use sidereon_core::bias::{BiasError, BiasSet, CodeDcbOptions, Parsed as BiasParsed};
456use sidereon_core::ephemeris::{BroadcastEphemeris, Sp3};
457use sidereon_core::observables::ObservableEphemerisSource;
458use sidereon_core::positioning::{
459 EphemerisSource, ReceiverSolution, SolveInputs, SolvePolicy, SolvePolicyError,
460};
461use sidereon_core::precise_positioning::{
462 FixedSolution, FixedSolveConfig, FixedSolveError as PppFixedSolveError, FloatEpoch,
463 FloatSolution, FloatSolveConfig, FloatSolveError as PppFloatSolveError, FloatState,
464};
465use sidereon_core::rinex::clock::{RinexClock, RinexClockError};
466use sidereon_core::rinex::nav::NavParseError;
467use sidereon_core::rinex::observations::ObservationFile;
468use sidereon_core::rinex::qc::{LintReport, NavRepair, ObsRepair, RepairOptions};
469use sidereon_core::rtk_filter::{
470 AmbiguitySet, Epoch, FloatBaselineSolution, FloatSolveError as RtkFloatSolveError,
471 FloatSolveOpts, MeasModel, ReceiverAntennaCorrections, ValidatedFixedBaselineSolution,
472 ValidatedFixedSolveError, ValidatedFixedSolveOpts,
473};
474use sidereon_core::velocity::{
475 VelocityError, VelocityObservation, VelocitySolution, VelocitySolveOptions,
476};
477
478#[derive(Debug)]
486pub enum Error {
487 Sp3(sidereon_core::Error),
489 Antex(AntexError),
491 RinexNav(NavParseError),
493 RinexObs(sidereon_core::Error),
495 RinexClock(RinexClockError),
497 Bias(BiasError),
499 Ssr(sidereon_core::Error),
501 Crinex(sidereon_core::Error),
503 Io(std::io::Error),
505 Spp(SolvePolicyError),
507 Velocity(VelocityError),
509 RtkFloat(RtkFloatSolveError),
511 RtkFixed(ValidatedFixedSolveError),
513 PppFloat(PppFloatSolveError),
515 PppFixed(PppFixedSolveError),
517}
518
519impl fmt::Display for Error {
520 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
521 match self {
522 Error::Sp3(e) => write!(f, "SP3 parse failed: {e}"),
523 Error::Antex(e) => write!(f, "ANTEX parse failed: {e}"),
524 Error::RinexNav(e) => write!(f, "RINEX NAV parse failed: {e}"),
525 Error::RinexObs(e) => write!(f, "RINEX OBS parse failed: {e}"),
526 Error::RinexClock(e) => write!(f, "RINEX clock parse failed: {e}"),
527 Error::Bias(e) => write!(f, "bias product parse failed: {e}"),
528 Error::Ssr(e) => write!(f, "SSR ingest failed: {e}"),
529 Error::Crinex(e) => write!(f, "CRINEX decode failed: {e}"),
530 Error::Io(e) => write!(f, "product file read failed: {e}"),
531 Error::Spp(e) => write!(f, "{e}"),
532 Error::Velocity(e) => write!(f, "velocity solve failed: {e}"),
533 Error::RtkFloat(e) => write!(f, "{e}"),
534 Error::RtkFixed(e) => write!(f, "{e}"),
535 Error::PppFloat(e) => write!(f, "{e}"),
536 Error::PppFixed(e) => write!(f, "{e}"),
537 }
538 }
539}
540
541impl std::error::Error for Error {
542 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
543 match self {
544 Error::Sp3(e) => Some(e),
545 Error::Antex(e) => Some(e),
546 Error::RinexNav(e) => Some(e),
547 Error::RinexObs(e) => Some(e),
548 Error::RinexClock(e) => Some(e),
549 Error::Bias(e) => Some(e),
550 Error::Ssr(e) => Some(e),
551 Error::Crinex(e) => Some(e),
552 Error::Io(e) => Some(e),
553 Error::Spp(e) => Some(e),
554 Error::Velocity(e) => Some(e),
555 Error::RtkFloat(e) => Some(e),
556 Error::RtkFixed(e) => Some(e),
557 Error::PppFloat(e) => Some(e),
558 Error::PppFixed(e) => Some(e),
559 }
560 }
561}
562
563impl From<AntexError> for Error {
564 fn from(e: AntexError) -> Self {
565 Error::Antex(e)
566 }
567}
568
569impl From<NavParseError> for Error {
570 fn from(e: NavParseError) -> Self {
571 Error::RinexNav(e)
572 }
573}
574
575impl From<RinexClockError> for Error {
576 fn from(e: RinexClockError) -> Self {
577 Error::RinexClock(e)
578 }
579}
580
581impl From<BiasError> for Error {
582 fn from(e: BiasError) -> Self {
583 Error::Bias(e)
584 }
585}
586
587impl From<std::io::Error> for Error {
588 fn from(e: std::io::Error) -> Self {
589 Error::Io(e)
590 }
591}
592
593impl From<SolvePolicyError> for Error {
594 fn from(e: SolvePolicyError) -> Self {
595 Error::Spp(e)
596 }
597}
598
599impl From<VelocityError> for Error {
600 fn from(e: VelocityError) -> Self {
601 Error::Velocity(e)
602 }
603}
604
605impl From<RtkFloatSolveError> for Error {
606 fn from(e: RtkFloatSolveError) -> Self {
607 Error::RtkFloat(e)
608 }
609}
610
611impl From<ValidatedFixedSolveError> for Error {
612 fn from(e: ValidatedFixedSolveError) -> Self {
613 Error::RtkFixed(e)
614 }
615}
616
617impl From<PppFloatSolveError> for Error {
618 fn from(e: PppFloatSolveError) -> Self {
619 Error::PppFloat(e)
620 }
621}
622
623impl From<PppFixedSolveError> for Error {
624 fn from(e: PppFixedSolveError) -> Self {
625 Error::PppFixed(e)
626 }
627}
628
629pub type Result<T> = core::result::Result<T, Error>;
631
632#[derive(Clone)]
641pub struct RtkFloatConfig<'a> {
642 pub epochs: &'a [Epoch],
644 pub base_ecef_m: [f64; 3],
646 pub ambiguity_ids: &'a [String],
648 pub initial_baseline_m: [f64; 3],
650 pub model: &'a MeasModel,
652 pub options: FloatSolveOpts,
654 pub receiver_antenna_corrections: Option<&'a ReceiverAntennaCorrections>,
656}
657
658impl<'a> RtkFloatConfig<'a> {
659 #[must_use]
662 pub fn new(
663 epochs: &'a [Epoch],
664 base_ecef_m: [f64; 3],
665 ambiguity_ids: &'a [String],
666 model: &'a MeasModel,
667 options: FloatSolveOpts,
668 ) -> Self {
669 Self {
670 epochs,
671 base_ecef_m,
672 ambiguity_ids,
673 initial_baseline_m: [0.0; 3],
674 model,
675 options,
676 receiver_antenna_corrections: None,
677 }
678 }
679
680 #[must_use = "this builder consumes and returns the updated RTK float config"]
682 pub fn with_initial_baseline_m(mut self, initial_baseline_m: [f64; 3]) -> Self {
683 self.initial_baseline_m = initial_baseline_m;
684 self
685 }
686
687 #[must_use = "this builder consumes and returns the updated RTK float config"]
689 pub fn with_receiver_antenna_corrections(
690 mut self,
691 receiver_antenna_corrections: Option<&'a ReceiverAntennaCorrections>,
692 ) -> Self {
693 self.receiver_antenna_corrections = receiver_antenna_corrections;
694 self
695 }
696}
697
698#[derive(Clone)]
706pub struct RtkFixedConfig<'a> {
707 pub epochs: &'a [Epoch],
709 pub base_ecef_m: [f64; 3],
711 pub initial_ambiguities: AmbiguitySet<'a>,
714 pub initial_baseline_m: [f64; 3],
716 pub model: &'a MeasModel,
718 pub options: ValidatedFixedSolveOpts,
720 pub receiver_antenna_corrections: Option<&'a ReceiverAntennaCorrections>,
722}
723
724impl<'a> RtkFixedConfig<'a> {
725 #[must_use]
728 pub fn new(
729 epochs: &'a [Epoch],
730 base_ecef_m: [f64; 3],
731 initial_ambiguities: AmbiguitySet<'a>,
732 model: &'a MeasModel,
733 options: ValidatedFixedSolveOpts,
734 ) -> Self {
735 Self {
736 epochs,
737 base_ecef_m,
738 initial_ambiguities,
739 initial_baseline_m: [0.0; 3],
740 model,
741 options,
742 receiver_antenna_corrections: None,
743 }
744 }
745
746 #[must_use = "this builder consumes and returns the updated RTK fixed config"]
748 pub fn with_initial_baseline_m(mut self, initial_baseline_m: [f64; 3]) -> Self {
749 self.initial_baseline_m = initial_baseline_m;
750 self
751 }
752
753 #[must_use = "this builder consumes and returns the updated RTK fixed config"]
755 pub fn with_receiver_antenna_corrections(
756 mut self,
757 receiver_antenna_corrections: Option<&'a ReceiverAntennaCorrections>,
758 ) -> Self {
759 self.receiver_antenna_corrections = receiver_antenna_corrections;
760 self
761 }
762}
763
764#[derive(Clone)]
772pub struct PppFloatConfig<'a> {
773 pub source: &'a dyn ObservableEphemerisSource,
775 pub epochs: &'a [FloatEpoch],
777 pub initial_state: FloatState,
779 pub solve: FloatSolveConfig,
781}
782
783impl<'a> PppFloatConfig<'a> {
784 pub fn new(
786 source: &'a dyn ObservableEphemerisSource,
787 epochs: &'a [FloatEpoch],
788 initial_state: FloatState,
789 solve: FloatSolveConfig,
790 ) -> Self {
791 Self {
792 source,
793 epochs,
794 initial_state,
795 solve,
796 }
797 }
798}
799
800#[derive(Clone)]
807pub struct PppFixedConfig<'a> {
808 pub source: &'a dyn ObservableEphemerisSource,
810 pub epochs: &'a [FloatEpoch],
812 pub float_solution: FloatSolution,
814 pub solve: FixedSolveConfig,
816}
817
818impl<'a> PppFixedConfig<'a> {
819 pub fn new(
821 source: &'a dyn ObservableEphemerisSource,
822 epochs: &'a [FloatEpoch],
823 float_solution: FloatSolution,
824 solve: FixedSolveConfig,
825 ) -> Self {
826 Self {
827 source,
828 epochs,
829 float_solution,
830 solve,
831 }
832 }
833}
834
835pub fn load_sp3(bytes: &[u8]) -> Result<Sp3> {
845 Sp3::parse(bytes).map_err(Error::Sp3)
846}
847
848pub fn parse_nmea(input: &[u8]) -> nmea::Parsed<nmea::NmeaLog> {
850 nmea::parse_nmea(input)
851}
852
853pub fn nmea_epochs(input: &[u8]) -> (Vec<nmea::EpochSnapshot>, nmea::Diagnostics) {
855 let parsed = nmea::parse_nmea(input);
856 let epochs = nmea::group_epochs(&parsed.value);
857 (epochs, parsed.diagnostics)
858}
859
860pub fn write_gga(
862 talker: nmea::NmeaTalker,
863 gga: &nmea::Gga,
864) -> std::result::Result<String, nmea::NmeaError> {
865 nmea::write_gga(talker, gga)
866}
867
868pub fn parse_antex(text: &str) -> Result<Antex> {
874 Antex::parse(text).map_err(Error::Antex)
875}
876
877pub fn load_antex(path: impl AsRef<Path>) -> Result<Antex> {
881 let text = std::fs::read_to_string(path)?;
882 parse_antex(&text)
883}
884
885pub fn parse_rinex_nav(text: &str) -> Result<BroadcastEphemeris> {
890 BroadcastEphemeris::from_nav(text).map_err(Error::RinexNav)
891}
892
893pub fn load_rinex_nav(path: impl AsRef<Path>) -> Result<BroadcastEphemeris> {
895 let text = std::fs::read_to_string(path)?;
896 parse_rinex_nav(&text)
897}
898
899pub fn ssr_store_from_rtcm(
901 bytes: &[u8],
902 week: sidereon_core::astro::time::GnssWeekTow,
903) -> Result<sidereon_core::ssr::SsrCorrectionStore> {
904 let mut store = sidereon_core::ssr::SsrCorrectionStore::new();
905 let mut assembler = sidereon_core::rtcm::SsrStreamAssembler::new();
906 for decoded in assembler.push(bytes) {
907 let message = decoded.map_err(Error::Ssr)?;
908 store.ingest(&message, week).map_err(Error::Ssr)?;
909 }
910 Ok(store)
911}
912
913pub fn parse_rinex_obs(text: &str) -> Result<ObservationFile> {
915 ObservationFile::parse(text).map_err(Error::RinexObs)
916}
917
918pub fn load_rinex_obs(path: impl AsRef<Path>) -> Result<ObservationFile> {
920 let text = std::fs::read_to_string(path)?;
921 parse_rinex_obs(&text)
922}
923
924pub fn lint_rinex_obs(text: &str) -> LintReport {
926 sidereon_core::rinex::qc::lint_obs_text(text)
927}
928
929pub fn lint_rinex_nav(text: &str) -> LintReport {
931 sidereon_core::rinex::qc::lint_nav_text(text)
932}
933
934pub fn repair_rinex_obs(text: &str, options: &RepairOptions) -> Result<ObsRepair> {
936 sidereon_core::rinex::qc::repair_obs_text(text, options).map_err(Error::RinexObs)
937}
938
939pub fn repair_rinex_nav(text: &str, options: &RepairOptions) -> Result<NavRepair> {
941 sidereon_core::rinex::qc::repair_nav_text(text, options).map_err(Error::RinexNav)
942}
943
944pub fn parse_rinex_clock(text: &str) -> Result<RinexClock> {
946 RinexClock::parse(text).map_err(Error::RinexClock)
947}
948
949pub fn load_rinex_clock(path: impl AsRef<Path>) -> Result<RinexClock> {
951 let text = std::fs::read_to_string(path)?;
952 parse_rinex_clock(&text)
953}
954
955pub fn parse_rinex_clock_lossy(text: &str) -> RinexClock {
957 RinexClock::parse_lossy(text)
958}
959
960pub fn load_rinex_clock_lossy(path: impl AsRef<Path>) -> Result<RinexClock> {
962 let text = std::fs::read_to_string(path)?;
963 Ok(parse_rinex_clock_lossy(&text))
964}
965
966pub fn parse_bias_sinex(bytes: &[u8]) -> Result<BiasSet> {
968 Ok(parse_bias_sinex_lossy(bytes)?.value)
969}
970
971pub fn parse_bias_sinex_lossy(bytes: &[u8]) -> Result<BiasParsed<BiasSet>> {
973 BiasSet::parse_bias_sinex(bytes).map_err(Error::Bias)
974}
975
976pub fn load_bias_sinex(path: impl AsRef<Path>) -> Result<BiasSet> {
978 let bytes = read_maybe_gzip(path)?;
979 parse_bias_sinex(&bytes)
980}
981
982pub fn load_bias_sinex_lossy(path: impl AsRef<Path>) -> Result<BiasParsed<BiasSet>> {
985 let bytes = read_maybe_gzip(path)?;
986 parse_bias_sinex_lossy(&bytes)
987}
988
989pub fn parse_code_dcb(bytes: &[u8], options: Option<CodeDcbOptions>) -> Result<BiasSet> {
991 Ok(parse_code_dcb_lossy(bytes, options)?.value)
992}
993
994pub fn parse_code_dcb_lossy(
996 bytes: &[u8],
997 options: Option<CodeDcbOptions>,
998) -> Result<BiasParsed<BiasSet>> {
999 BiasSet::parse_code_dcb(bytes, options).map_err(Error::Bias)
1000}
1001
1002pub fn load_code_dcb(path: impl AsRef<Path>, options: Option<CodeDcbOptions>) -> Result<BiasSet> {
1004 let bytes = read_maybe_gzip(path)?;
1005 parse_code_dcb(&bytes, options)
1006}
1007
1008pub fn load_code_dcb_lossy(
1011 path: impl AsRef<Path>,
1012 options: Option<CodeDcbOptions>,
1013) -> Result<BiasParsed<BiasSet>> {
1014 let bytes = read_maybe_gzip(path)?;
1015 parse_code_dcb_lossy(&bytes, options)
1016}
1017
1018pub fn decode_crinex(text: &str) -> Result<String> {
1020 rinex::decode_crinex(text).map_err(Error::Crinex)
1021}
1022
1023pub fn encode_crinex(text: &str) -> Result<String> {
1029 rinex::encode_crinex(text).map_err(Error::Crinex)
1030}
1031
1032pub fn load_crinex(path: impl AsRef<Path>) -> Result<String> {
1034 let text = std::fs::read_to_string(path)?;
1035 decode_crinex(&text)
1036}
1037
1038fn read_maybe_gzip(path: impl AsRef<Path>) -> Result<Vec<u8>> {
1039 let path = path.as_ref();
1040 let bytes = std::fs::read(path)?;
1041 if path.extension().and_then(|ext| ext.to_str()) != Some("gz") {
1042 return Ok(bytes);
1043 }
1044 let mut decoder = GzDecoder::new(&bytes[..]);
1045 let mut decoded = Vec::new();
1046 decoder.read_to_end(&mut decoded)?;
1047 Ok(decoded)
1048}
1049
1050pub fn solve_spp(
1065 eph: &dyn EphemerisSource,
1066 inputs: &SolveInputs,
1067 with_geodetic: bool,
1068 policy: SolvePolicy,
1069) -> Result<ReceiverSolution> {
1070 sidereon_core::positioning::solve_with_policy(eph, inputs, with_geodetic, policy)
1071 .map_err(Error::Spp)
1072}
1073
1074pub fn solve_spp_batch_serial(
1081 eph: &dyn EphemerisSource,
1082 epochs: &[SolveInputs],
1083 with_geodetic: bool,
1084 policy: SolvePolicy,
1085) -> Vec<Result<ReceiverSolution>> {
1086 sidereon_core::positioning::solve_spp_batch_serial(eph, epochs, with_geodetic, policy)
1087 .into_iter()
1088 .map(|r| r.map_err(Error::Spp))
1089 .collect()
1090}
1091
1092pub fn solve_spp_batch(
1104 eph: &(dyn EphemerisSource + Sync),
1105 epochs: &[SolveInputs],
1106 with_geodetic: bool,
1107 policy: SolvePolicy,
1108) -> Vec<Result<ReceiverSolution>> {
1109 sidereon_core::positioning::solve_spp_batch_parallel(eph, epochs, with_geodetic, policy)
1110 .into_iter()
1111 .map(|r| r.map_err(Error::Spp))
1112 .collect()
1113}
1114
1115pub fn solve_velocity(
1129 source: &dyn ObservableEphemerisSource,
1130 observations: &[VelocityObservation],
1131 receiver_ecef_m: [f64; 3],
1132 t_rx_j2000_s: f64,
1133 options: VelocitySolveOptions,
1134) -> Result<VelocitySolution> {
1135 sidereon_core::velocity::solve(source, observations, receiver_ecef_m, t_rx_j2000_s, options)
1136 .map_err(Error::Velocity)
1137}
1138
1139pub fn solve_rtk_float_with(config: RtkFloatConfig<'_>) -> Result<FloatBaselineSolution> {
1172 solve_rtk_float(
1173 config.epochs,
1174 config.base_ecef_m,
1175 config.ambiguity_ids,
1176 config.initial_baseline_m,
1177 config.model,
1178 config.options,
1179 config.receiver_antenna_corrections,
1180 )
1181}
1182
1183pub fn solve_rtk_float(
1198 epochs: &[Epoch],
1199 base: [f64; 3],
1200 ambiguity_ids: &[String],
1201 initial_baseline_m: [f64; 3],
1202 model: &MeasModel,
1203 opts: FloatSolveOpts,
1204 receiver_antenna_corrections: Option<&ReceiverAntennaCorrections>,
1205) -> Result<FloatBaselineSolution> {
1206 sidereon_core::rtk_filter::solve_float_baseline(
1207 epochs,
1208 base,
1209 ambiguity_ids,
1210 initial_baseline_m,
1211 model,
1212 opts,
1213 receiver_antenna_corrections,
1214 )
1215 .map_err(Error::RtkFloat)
1216}
1217
1218pub fn solve_rtk_fixed_with(config: RtkFixedConfig<'_>) -> Result<ValidatedFixedBaselineSolution> {
1224 solve_rtk_fixed(
1225 config.epochs,
1226 config.base_ecef_m,
1227 config.initial_ambiguities,
1228 config.initial_baseline_m,
1229 config.model,
1230 config.options,
1231 config.receiver_antenna_corrections,
1232 )
1233}
1234
1235pub fn solve_rtk_fixed(
1253 epochs: &[Epoch],
1254 base: [f64; 3],
1255 initial_ambiguities: AmbiguitySet,
1256 initial_baseline_m: [f64; 3],
1257 model: &MeasModel,
1258 opts: ValidatedFixedSolveOpts,
1259 receiver_antenna_corrections: Option<&ReceiverAntennaCorrections>,
1260) -> Result<ValidatedFixedBaselineSolution> {
1261 sidereon_core::rtk_filter::solve_fixed_baseline_validated(
1262 epochs,
1263 base,
1264 initial_ambiguities,
1265 initial_baseline_m,
1266 model,
1267 opts,
1268 receiver_antenna_corrections,
1269 )
1270 .map_err(Error::RtkFixed)
1271}
1272
1273pub fn solve_ppp_float_with(config: PppFloatConfig<'_>) -> Result<FloatSolution> {
1279 solve_ppp_float(
1280 config.source,
1281 config.epochs,
1282 config.initial_state,
1283 config.solve,
1284 )
1285}
1286
1287pub fn solve_ppp_float(
1300 source: &dyn ObservableEphemerisSource,
1301 epochs: &[FloatEpoch],
1302 initial_state: FloatState,
1303 config: FloatSolveConfig,
1304) -> Result<FloatSolution> {
1305 sidereon_core::precise_positioning::solve_float_epochs(source, epochs, initial_state, config)
1306 .map_err(Error::PppFloat)
1307}
1308
1309pub fn solve_ppp_fixed_with(config: PppFixedConfig<'_>) -> Result<FixedSolution> {
1316 solve_ppp_fixed(
1317 config.source,
1318 config.epochs,
1319 config.float_solution,
1320 config.solve,
1321 )
1322}
1323
1324pub fn solve_ppp_fixed(
1337 source: &dyn ObservableEphemerisSource,
1338 epochs: &[FloatEpoch],
1339 float_solution: FloatSolution,
1340 config: FixedSolveConfig,
1341) -> Result<FixedSolution> {
1342 sidereon_core::precise_positioning::solve_fixed_from_float(
1343 source,
1344 epochs,
1345 float_solution,
1346 config,
1347 )
1348 .map_err(Error::PppFixed)
1349}
1350
1351#[cfg(all(test, sidereon_repo_tests))]
1352mod tests {
1353 use super::*;
1354 use sidereon_core::positioning::{Corrections, KlobucharCoeffs, Observation, SurfaceMet};
1355 use std::collections::BTreeMap;
1356 use std::path::PathBuf;
1357
1358 const DEGENERATE_SP3: &[u8] =
1362 include_bytes!("../../sidereon-core/tests/fixtures/sp3/degenerate_coincident_5sat.sp3");
1363 const ANTEX_TEXT: &str =
1364 include_str!("../../sidereon-core/tests/fixtures/antex/igs20_wettzell_trim.atx");
1365 const RINEX_NAV_TEXT: &str =
1366 include_str!("../../sidereon-core/tests/fixtures/nav/ESBC00DNK_R_20201770000_01D_MN.rnx");
1367 const RINEX_OBS_TEXT: &str = include_str!(
1368 "../../sidereon-core/tests/fixtures/obs/ESBC00DNK_R_20201770000_01D_30S_MO_trim.rnx"
1369 );
1370 const RINEX_CLOCK_TEXT: &str =
1371 include_str!("../../sidereon-core/tests/fixtures/clk/synthetic_rinex_clock.clk");
1372 const BIAS_BYTES: &[u8] = include_bytes!("../../sidereon-core/tests/fixtures/bias/CODE.BIA");
1373 const DCB_BYTES: &[u8] =
1374 include_bytes!("../../sidereon-core/tests/fixtures/bias/P1C1_RINEX.DCB");
1375 const CRINEX_TEXT: &str = include_str!(
1376 "../../sidereon-core/tests/fixtures/obs/ESBC00DNK_R_20201770000_01D_30S_MO_trim.crx"
1377 );
1378 const STATIONS_TLE: &str =
1379 include_str!("../../sidereon-core/tests/fixtures/celestrak/stations.tle");
1380 const REAL_SSRA02IGS0_1060_FRAME_HEX: &str =
1381 include_str!("../../sidereon-core/tests/fixtures/ssr/SSRA02IGS0_2026181234930_1060.hex");
1382
1383 fn fixture_path(parts: &[&str]) -> PathBuf {
1384 let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
1385 path.push("../sidereon-core/tests/fixtures");
1386 for part in parts {
1387 path.push(part);
1388 }
1389 path
1390 }
1391
1392 fn station_tle(name: &str) -> tca::TcaTle<'static> {
1393 let mut lines = STATIONS_TLE.lines();
1394 while let Some(object_name) = lines.next() {
1395 let Some(line1) = lines.next() else {
1396 break;
1397 };
1398 let Some(line2) = lines.next() else {
1399 break;
1400 };
1401 if object_name.trim() == name {
1402 return tca::TcaTle::new(line1, line2);
1403 }
1404 }
1405 panic!("missing station TLE {name}");
1406 }
1407
1408 fn norm3(v: [f64; 3]) -> f64 {
1409 (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt()
1410 }
1411
1412 fn hex_bytes(hex: &str) -> Vec<u8> {
1413 let compact: String = hex.chars().filter(|c| c.is_ascii_hexdigit()).collect();
1414 assert_eq!(compact.len() % 2, 0);
1415 compact
1416 .as_bytes()
1417 .chunks_exact(2)
1418 .map(|chunk| {
1419 let hi = (chunk[0] as char).to_digit(16).unwrap();
1420 let lo = (chunk[1] as char).to_digit(16).unwrap();
1421 ((hi << 4) | lo) as u8
1422 })
1423 .collect()
1424 }
1425
1426 fn rtk_model() -> MeasModel {
1427 MeasModel {
1428 code_sigma_m: 0.3,
1429 phase_sigma_m: 0.003,
1430 sagnac: true,
1431 stochastic: rtk_filter::StochasticModel::Rtklib,
1432 }
1433 }
1434
1435 fn rtk_float_options() -> FloatSolveOpts {
1436 FloatSolveOpts {
1437 position_tol_m: 1.0e-4,
1438 ambiguity_tol_m: 1.0e-4,
1439 max_iterations: 1,
1440 }
1441 }
1442
1443 fn rtk_fixed_options() -> rtk_filter::ValidatedFixedSolveOpts {
1444 rtk_filter::ValidatedFixedSolveOpts {
1445 float: rtk_float_options(),
1446 fixed: rtk_filter::FixedSolveOpts {
1447 position_tol_m: 1.0e-4,
1448 ambiguity_tol_m: 1.0e-4,
1449 max_iterations: 1,
1450 ratio_threshold: 3.0,
1451 partial_ambiguity_resolution: false,
1452 partial_min_ambiguities: 4,
1453 },
1454 residual: rtk_filter::ResidualValidationOpts {
1455 threshold_sigma: None,
1456 max_exclusions: 0,
1457 },
1458 }
1459 }
1460
1461 fn ppp_float_solve_config() -> FloatSolveConfig {
1462 FloatSolveConfig {
1463 weights: precise_positioning::MeasurementWeights {
1464 code: 1.0,
1465 phase: 100.0,
1466 elevation_weighting: false,
1467 },
1468 tropo: precise_positioning::TroposphereOptions::disabled(),
1469 corrections: precise_positioning::RangeCorrections::disabled(),
1470 opts: precise_positioning::FloatSolveOptions {
1471 max_iterations: 1,
1472 position_tolerance_m: 1.0e-4,
1473 clock_tolerance_m: 1.0e-4,
1474 ambiguity_tolerance_m: 1.0e-4,
1475 ztd_tolerance_m: 1.0e-4,
1476 },
1477 elevation_cutoff_deg: None,
1478 residual_screen: false,
1479 estimate_residual_ionosphere: false,
1480 }
1481 }
1482
1483 fn ppp_fixed_solve_config() -> FixedSolveConfig {
1484 let float = ppp_float_solve_config();
1485 FixedSolveConfig {
1486 weights: float.weights,
1487 tropo: float.tropo,
1488 corrections: float.corrections,
1489 opts: float.opts,
1490 elevation_cutoff_deg: None,
1491 ambiguity: precise_positioning::FixedAmbiguityOptions {
1492 wavelengths_m: BTreeMap::new(),
1493 offsets_m: BTreeMap::new(),
1494 ratio_threshold: 3.0,
1495 },
1496 estimate_residual_ionosphere: false,
1497 }
1498 }
1499
1500 fn empty_ppp_state() -> FloatState {
1501 FloatState {
1502 position_m: [0.0; 3],
1503 clocks_m: Vec::new(),
1504 ambiguities_m: BTreeMap::new(),
1505 ztd_m: 0.0,
1506 tropo_gradient_north_m: 0.0,
1507 tropo_gradient_east_m: 0.0,
1508 residual_ionosphere_m: BTreeMap::new(),
1509 }
1510 }
1511
1512 fn empty_ppp_float_solution() -> FloatSolution {
1513 FloatSolution {
1514 position_m: [0.0; 3],
1515 position_covariance: sidereon_core::dop::PositionCovariance {
1516 ecef_m2: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
1517 enu_m2: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
1518 },
1519 formal_position_covariance: sidereon_core::dop::PositionCovariance {
1520 ecef_m2: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
1521 enu_m2: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
1522 },
1523 posterior_variance_factor: 1.0,
1524 position_covariance_scale_factor: 1.0,
1525 temporal_position_covariance: sidereon_core::dop::PositionCovariance {
1526 ecef_m2: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
1527 enu_m2: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
1528 },
1529 temporal_position_covariance_scale_factor: 1.0,
1530 temporal_correlation: sidereon_core::precise_positioning::TemporalCorrelationSummary {
1531 lag1_autocorrelation: 0.0,
1532 decorrelation_time_epochs: 0.0,
1533 decorrelation_time_s: None,
1534 nominal_sample_count: 0,
1535 effective_sample_count: 0.0,
1536 variance_inflation_factor: 1.0,
1537 arcs_used: 0,
1538 },
1539 epoch_clocks_m: Vec::new(),
1540 ambiguities_m: BTreeMap::new(),
1541 residual_ionosphere_m: BTreeMap::new(),
1542 ztd_residual_m: None,
1543 tropo_gradient_north_m: None,
1544 tropo_gradient_east_m: None,
1545 tropo_gradient_covariance_m2: None,
1546 formal_tropo_gradient_covariance_m2: None,
1547 residuals_m: Vec::new(),
1548 used_sats: Vec::new(),
1549 iterations: 0,
1550 converged: false,
1551 status: precise_positioning::FloatStatus::MaxIterations,
1552 code_rms_m: 0.0,
1553 phase_rms_m: 0.0,
1554 weighted_rms_m: 0.0,
1555 }
1556 }
1557
1558 #[test]
1559 fn load_sp3_parses_a_precise_product() {
1560 let sp3 = load_sp3(DEGENERATE_SP3).expect("the fixture parses");
1561 assert_eq!(sp3.epoch_count(), 2);
1562 assert_eq!(sp3.satellites().len(), 5);
1563 }
1564
1565 #[test]
1566 fn load_sp3_surfaces_parse_errors() {
1567 let err = load_sp3(b"not an sp3 file").unwrap_err();
1568 assert!(matches!(err, Error::Sp3(_)));
1569 assert!(err.to_string().contains("SP3 parse failed"));
1570 assert!(std::error::Error::source(&err).is_some());
1572 }
1573
1574 #[test]
1575 fn product_ingestion_wrappers_parse_fixture_products() {
1576 let antex = parse_antex(ANTEX_TEXT).expect("parse ANTEX fixture");
1577 assert!(!antex.antennas.is_empty());
1578 let loaded_antex =
1579 load_antex(fixture_path(&["antex", "igs20_wettzell_trim.atx"])).expect("load ANTEX");
1580 assert_eq!(loaded_antex.antennas.len(), antex.antennas.len());
1581
1582 let nav = parse_rinex_nav(RINEX_NAV_TEXT).expect("parse RINEX NAV fixture");
1583 assert!(!nav.records().is_empty() || !nav.glonass_records().is_empty());
1584 let loaded_nav =
1585 load_rinex_nav(fixture_path(&["nav", "ESBC00DNK_R_20201770000_01D_MN.rnx"]))
1586 .expect("load RINEX NAV");
1587 assert_eq!(loaded_nav.records().len(), nav.records().len());
1588 assert_eq!(
1589 loaded_nav.glonass_records().len(),
1590 nav.glonass_records().len()
1591 );
1592
1593 let obs = parse_rinex_obs(RINEX_OBS_TEXT).expect("parse RINEX OBS fixture");
1594 assert!(!obs.epochs().is_empty());
1595 let loaded_obs = load_rinex_obs(fixture_path(&[
1596 "obs",
1597 "ESBC00DNK_R_20201770000_01D_30S_MO_trim.rnx",
1598 ]))
1599 .expect("load RINEX OBS");
1600 assert_eq!(loaded_obs.epochs().len(), obs.epochs().len());
1601
1602 let clock = parse_rinex_clock(RINEX_CLOCK_TEXT).expect("parse RINEX clock fixture");
1603 assert!(!clock.series_rows().is_empty());
1604 let loaded_clock = load_rinex_clock(fixture_path(&["clk", "synthetic_rinex_clock.clk"]))
1605 .expect("load RINEX clock");
1606 assert_eq!(loaded_clock.series_rows().len(), clock.series_rows().len());
1607 let lossy_clock = parse_rinex_clock_lossy("AS malformed\n");
1608 assert!(lossy_clock.series_rows().is_empty());
1609 let loaded_lossy_clock =
1610 load_rinex_clock_lossy(fixture_path(&["clk", "synthetic_rinex_clock.clk"]))
1611 .expect("load lossy RINEX clock");
1612 assert_eq!(
1613 loaded_lossy_clock.series_rows().len(),
1614 clock.series_rows().len()
1615 );
1616
1617 let bias = parse_bias_sinex(BIAS_BYTES).expect("parse Bias-SINEX fixture");
1618 assert_eq!(bias.records().len(), 351);
1619 let loaded_bias = load_bias_sinex(fixture_path(&[
1620 "bias",
1621 "COD0OPSFIN_20261330000_01D_01D_OSB.BIA.gz",
1622 ]))
1623 .expect("load gzip Bias-SINEX");
1624 assert!(!loaded_bias.records().is_empty());
1625 let lossy_bias = parse_bias_sinex_lossy(BIAS_BYTES).expect("lossy Bias-SINEX parse");
1626 assert_eq!(lossy_bias.value.records().len(), bias.records().len());
1627
1628 let dcb = parse_code_dcb(DCB_BYTES, None).expect("parse CODE DCB fixture");
1629 assert_eq!(dcb.records().len(), 496);
1630 let loaded_dcb =
1631 load_code_dcb(fixture_path(&["bias", "P1C1_RINEX.DCB"]), None).expect("load CODE DCB");
1632 assert_eq!(loaded_dcb.records().len(), dcb.records().len());
1633 let lossy_dcb = load_code_dcb_lossy(fixture_path(&["bias", "P1C1_RINEX.DCB"]), None)
1634 .expect("load lossy CODE DCB");
1635 assert_eq!(lossy_dcb.value.records().len(), dcb.records().len());
1636
1637 let decoded = decode_crinex(CRINEX_TEXT).expect("decode CRINEX fixture");
1638 assert!(decoded.contains("RINEX VERSION / TYPE"));
1639 let encoded = encode_crinex(&decoded).expect("encode decoded RINEX fixture");
1640 let round_tripped = decode_crinex(&encoded).expect("decode encoded CRINEX fixture");
1641 assert_eq!(round_tripped, decoded);
1642 let loaded_decoded = load_crinex(fixture_path(&[
1643 "obs",
1644 "ESBC00DNK_R_20201770000_01D_30S_MO_trim.crx",
1645 ]))
1646 .expect("load CRINEX");
1647 assert_eq!(loaded_decoded, decoded);
1648 }
1649
1650 #[test]
1651 fn ssr_store_from_rtcm_ingests_real_combined_orbit_clock_frame() {
1652 let week = sidereon_core::astro::time::model::GnssWeekTow::new(
1653 sidereon_core::astro::time::model::TimeScale::Gpst,
1654 2425,
1655 344_970.0,
1656 )
1657 .expect("valid week");
1658 let store = ssr_store_from_rtcm(&hex_bytes(REAL_SSRA02IGS0_1060_FRAME_HEX), week)
1659 .expect("ingest real SSR frame");
1660 let sat = GnssSatelliteId::new(GnssSystem::Gps, 30).expect("valid satellite");
1661 let orbit = store.orbit(sat).expect("G30 orbit correction");
1662 let clock = store.clock(sat).expect("G30 clock correction");
1663 assert_eq!(orbit.iode, 90);
1664 assert!((orbit.radial_m + 0.0807).abs() < 1.0e-12);
1665 assert!((orbit.along_m + 0.2484).abs() < 1.0e-12);
1666 assert!((orbit.cross_m - 0.1396).abs() < 1.0e-12);
1667 assert!((clock.c0_m - 0.0166).abs() < 1.0e-12);
1668 }
1669
1670 #[test]
1671 fn product_ingestion_wrappers_map_errors() {
1672 let err = match parse_rinex_nav("not a RINEX NAV file") {
1673 Ok(_) => panic!("invalid RINEX NAV unexpectedly parsed"),
1674 Err(err) => err,
1675 };
1676 assert!(matches!(err, Error::RinexNav(_)));
1677 assert!(std::error::Error::source(&err).is_some());
1678
1679 let err = match parse_rinex_nav(
1680 " 4.00 NAVIGATION DATA M RINEX VERSION / TYPE\n\
1681 XXX END OF HEADER\n\
1682 > EPH G01 LNAV\n",
1683 ) {
1684 Ok(_) => panic!("empty v4 EPH frame unexpectedly parsed"),
1685 Err(err) => err,
1686 };
1687 assert!(matches!(err, Error::RinexNav(_)));
1688
1689 let err = parse_rinex_obs("not a RINEX OBS file").unwrap_err();
1690 assert!(matches!(err, Error::RinexObs(_)));
1691 assert!(std::error::Error::source(&err).is_some());
1692
1693 let err = parse_rinex_clock("AS malformed").unwrap_err();
1694 assert!(matches!(err, Error::RinexClock(_)));
1695 assert!(std::error::Error::source(&err).is_some());
1696
1697 let err = parse_code_dcb(b"not a DCB file", None).unwrap_err();
1698 assert!(matches!(err, Error::Bias(_)));
1699 assert!(std::error::Error::source(&err).is_some());
1700
1701 let err = decode_crinex("not a CRINEX file\n").unwrap_err();
1702 assert!(matches!(err, Error::Crinex(_)));
1703 let rendered = err.to_string();
1704 assert!(rendered.starts_with("CRINEX decode failed:"), "{rendered}");
1705 assert!(!rendered.contains("SP3 parse failed"), "{rendered}");
1706 assert!(std::error::Error::source(&err).is_some());
1707
1708 let missing = fixture_path(&["missing.nope"]);
1709 let err = match load_rinex_nav(missing) {
1710 Ok(_) => panic!("missing RINEX NAV path unexpectedly loaded"),
1711 Err(err) => err,
1712 };
1713 assert!(matches!(err, Error::Io(_)));
1714 assert!(std::error::Error::source(&err).is_some());
1715 }
1716
1717 #[test]
1718 fn astro_umbrella_reexports_broader_astrodynamics_surface() {
1719 let budget = astro::rf::LinkBudget {
1720 eirp_dbw: 0.0,
1721 fspl_db: 165.0,
1722 receiver_gt_dbk: -12.0,
1723 other_losses_db: 3.0,
1724 required_cn0_dbhz: 35.0,
1725 };
1726 assert_eq!(
1727 astro::rf::link_margin(&budget)
1728 .expect("valid RF link budget")
1729 .to_bits(),
1730 13.599999999999994_f64.to_bits()
1731 );
1732
1733 let ts =
1734 astro::time::TimeScales::from_utc(2000, 1, 1, 12, 0, 0.0).expect("valid UTC instant");
1735 assert!((ts.jd_tt - 2451545.0).abs() < 1.0e-3);
1736
1737 let sun = [149_597_870.7, 0.0, 0.0];
1738 assert_eq!(
1739 astro::events::eclipse::status([-7000.0, 0.0, 0.0], sun)
1740 .expect("valid eclipse geometry"),
1741 astro::events::eclipse::EclipseStatus::Umbra
1742 );
1743 assert!(
1744 (astro::angles::beta_angle([1.0, 0.0, 0.0], sun).expect("valid beta geometry") - 90.0)
1745 .abs()
1746 <= 1.0e-9
1747 );
1748 let sep = astro::angles::angular_separation_coords(
1749 (101.287155333, -16.716115861),
1750 (114.825493028, 5.224993306),
1751 )
1752 .expect("valid angular separation");
1753 assert!((sep - 25.7013646403623).abs() <= 1.0e-9);
1754 let pa = astro::angles::position_angle(
1755 (101.287155333, -16.716115861),
1756 (114.825493028, 5.224993306),
1757 )
1758 .expect("valid position angle");
1759 let pa_diff = {
1760 let diff = (pa - 32.51673660099302).abs();
1761 diff.min(360.0 - diff)
1762 };
1763 assert!(pa_diff <= 1.0e-9);
1764 assert!(astro::covariance::symmetric(&[
1765 [1.0, 0.1, 0.2],
1766 [0.1, 2.0, 0.3],
1767 [0.2, 0.3, 3.0]
1768 ]));
1769
1770 assert!(core::mem::size_of::<astro::bodies::SunMoon>() > 0);
1771 assert!(core::mem::size_of::<astro::cdm::CdmKvn>() > 0);
1772 assert!(core::mem::size_of::<astro::conjunction::ConjunctionState>() > 0);
1773 assert!(core::mem::size_of::<astro::frames::transforms::TemeStateKm>() > 0);
1774 assert!(core::mem::size_of::<astro::omm::Omm>() > 0);
1775 assert!(core::mem::size_of::<astro::tdm::Tdm>() > 0);
1776 }
1777
1778 #[test]
1779 fn ground_site_sun_moon_helpers_reachable_through_facade() {
1780 let station = GeodeticStationKm {
1781 latitude_deg: 51.4769,
1782 longitude_deg: 0.0,
1783 altitude_km: 0.046,
1784 };
1785 let noon = UtcInstant::from_utc(2024, 6, 20, 12, 1, 42, 0).expect("valid UTC");
1788 let sun = sun_az_el(&station, noon).expect("sun geometry");
1789 assert!((sun.elevation_deg - 61.96).abs() < 0.5);
1790
1791 let full = UtcInstant::from_utc(2024, 4, 23, 23, 49, 0, 0).expect("valid UTC");
1793 let illum = moon_illumination(&station, full).expect("moon illumination");
1794 assert!(illum.illuminated_fraction > 0.95);
1795 let moon = moon_az_el(&station, full).expect("moon geometry");
1796 assert!((350_000.0..410_000.0).contains(&moon.range_km));
1797
1798 let start = UtcInstant::from_utc(2024, 4, 23, 0, 0, 0, 0).expect("valid UTC");
1799 let end = UtcInstant::from_utc(2024, 4, 24, 0, 0, 0, 0).expect("valid UTC");
1800 assert_eq!(
1801 find_moon_elevation_crossings(&station, start, end, MoonElevationOptions::default())
1802 .expect("moon crossings")
1803 .len(),
1804 2
1805 );
1806 assert_eq!(
1807 find_moon_transits(&station, start, end, 300.0, 1.0)
1808 .expect("moon transits")
1809 .len(),
1810 2
1811 );
1812
1813 let (_az, el, _range) =
1815 itrs_to_topocentric([0.0, 0.0, 7000.0], &station).expect("topocentric");
1816 assert!(el.is_finite());
1817
1818 let kernel = astro::Spk::from_bytes(include_bytes!(
1819 "../../sidereon-core/tests/fixtures/bodies/observe_de.bsp"
1820 ))
1821 .expect("fixture SPK");
1822 let observe_time = UtcInstant::from_utc(2024, 1, 1, 0, 0, 0, 0).expect("valid UTC");
1823 let mars = observe_spk_body(&station, observe_time, &kernel, 4).expect("Mars observation");
1824 assert!(mars.apparent.right_ascension_deg.is_finite());
1825 assert!(!mars.reduced);
1826 let target = Target::Spk {
1827 kernel: &kernel,
1828 naif_id: 4,
1829 };
1830 let same = observe(&station, observe_time, target, ObserveOptions::default())
1831 .expect("generic observation");
1832 assert_eq!(
1833 same.apparent.right_ascension_deg.to_bits(),
1834 mars.apparent.right_ascension_deg.to_bits()
1835 );
1836 let tod =
1837 gcrs_to_true_of_date_matrix(&observe_time.time_scales()).expect("true-of-date matrix");
1838 assert!(tod[0][0].is_finite());
1839 let gcrs_state = astro::frames::transforms::TemeStateKm {
1840 position_km: [7000.0, 100.0, -50.0],
1841 velocity_km_s: [0.0, 7.5, 0.1],
1842 };
1843 let (teme_pos, _) = gcrs_to_teme_compute(&gcrs_state, &observe_time.time_scales(), false)
1844 .expect("TEME inverse transform");
1845 assert!(teme_pos.0.is_finite());
1846 }
1847
1848 #[test]
1849 fn root_geodetic_look_angle_and_doppler_helpers_reachable_through_facade() {
1850 let station = GroundStation {
1851 latitude_deg: 51.5,
1852 longitude_deg: -0.1,
1853 altitude_m: 11.0,
1854 };
1855 let datetime = UtcInstant::from_utc(2024, 1, 1, 12, 0, 0, 0).expect("valid UTC");
1856 let tle = station_tle("ISS (ZARYA)");
1857 let elements = tle::parse(tle.line1, tle.line2)
1858 .expect("ISS TLE parses")
1859 .elements
1860 .to_element_set()
1861 .expect("ISS TLE converts to SGP4 elements");
1862 let look = look_angle(&elements, station, datetime).expect("ISS look angle");
1863 assert!(look.azimuth_deg.is_finite());
1864 assert!((-90.0..=90.0).contains(&look.elevation_deg));
1865 assert!(look.range_km > 100.0);
1866
1867 let satellite = sgp4::Satellite::from_tle(tle.line1, tle.line2).expect("ISS TLE parses");
1868 let track = ground_track(&satellite, &[datetime]).expect("ISS ground track");
1869 assert_eq!(track.len(), 1);
1870 assert!(track[0].lat_rad.is_finite());
1871 assert!(track[0].lon_rad.is_finite());
1872
1873 let ecef = geodetic_to_itrs(51.5, -0.1, 0.011).expect("geodetic to ITRS");
1874 let geodetic = itrs_to_geodetic_compute(ecef.0, ecef.1, ecef.2).expect("ITRS to geodetic");
1875 assert!((geodetic.0 - 51.5).abs() < 1.0e-6);
1876 assert!((geodetic.1 + 0.1).abs() < 1.0e-6);
1877 let projected = geodetic_from_ecef_proj(ecef.0 * 1000.0, ecef.1 * 1000.0, ecef.2 * 1000.0)
1878 .expect("projected geodetic");
1879 assert!((projected[0] + 0.1).abs() < 1.0e-6);
1880 assert!((projected[1] - 51.5).abs() < 1.0e-6);
1881
1882 let ts = datetime.time_scales();
1883 let shifted = doppler_shift(
1884 [3700.2112112039954, 2015.9122181206055, 5309.513078070448],
1885 [-3.398428894395407, 6.869656830559572, -0.239850181126689],
1886 40.0,
1887 -74.0,
1888 0.0,
1889 &ts,
1890 437.0e6,
1891 )
1892 .expect("Doppler shift");
1893 assert!(shifted.range_rate_km_s.is_finite());
1894 assert!(shifted.doppler_hz.is_finite());
1895 }
1896
1897 #[test]
1898 fn tca_shortcut_screens_two_real_tles_over_one_day() {
1899 let primary_tle = station_tle("ISS (ZARYA)");
1900 let secondary_tle = station_tle("CSS (TIANHE)");
1901 let primary = sgp4::Satellite::from_tle(primary_tle.line1, primary_tle.line2)
1902 .expect("station TLE parses");
1903 let window = tca::TcaWindow::from_start_and_duration_seconds(
1904 primary.epoch_jd(),
1905 sidereon_core::constants::SECONDS_PER_DAY,
1906 )
1907 .expect("valid one-day window");
1908 let options = tca::TcaFinderOptions {
1909 coarse_step_seconds: 120.0,
1910 time_tolerance_seconds: 1.0e-2,
1911 };
1912
1913 let candidates =
1914 tca::find_tca_candidates_between_tles(primary_tle, secondary_tle, window, options)
1915 .expect("real TLE TCA search succeeds");
1916 assert!(!candidates.is_empty());
1917
1918 let best = candidates
1919 .iter()
1920 .min_by(|a, b| a.miss_distance_km.total_cmp(&b.miss_distance_km))
1921 .expect("candidate set is nonempty");
1922 assert!(best.tca_seconds_since_window_start > 0.0);
1923 assert!(best.tca_seconds_since_window_start < sidereon_core::constants::SECONDS_PER_DAY);
1924 assert!(best.miss_distance_km.is_finite());
1925 assert!(best.miss_distance_km > 0.0);
1926 assert!((norm3(best.relative_position_km) - best.miss_distance_km).abs() < 1.0e-9);
1927 assert!(norm3(best.relative_velocity_km_s) > 0.0);
1928
1929 let secondaries = [secondary_tle];
1930 let threshold_km = best.miss_distance_km + 1.0;
1931 let serial = tca::screen_tca_candidates_from_tle_catalog_serial(
1932 primary_tle,
1933 &secondaries,
1934 window,
1935 threshold_km,
1936 options,
1937 )
1938 .expect("serial real TLE screening succeeds");
1939 let parallel = tca::screen_tca_candidates_from_tle_catalog_parallel(
1940 primary_tle,
1941 &secondaries,
1942 window,
1943 threshold_km,
1944 options,
1945 )
1946 .expect("parallel real TLE screening succeeds");
1947
1948 assert_eq!(serial, parallel);
1949 assert!(!serial.is_empty());
1950 assert!(serial.iter().all(|hit| hit.secondary_index == 0));
1951 assert!(serial
1952 .iter()
1953 .all(|hit| hit.candidate.miss_distance_km <= threshold_km));
1954
1955 let pc_options = tca::TcaPcOptions::with_default_covariance(
1956 0.020,
1957 astro::conjunction::PcMethod::Alfano2005,
1958 );
1959 let conjunctions = tca::find_tca_conjunctions_between_tles(
1960 primary_tle,
1961 secondary_tle,
1962 window,
1963 options,
1964 pc_options,
1965 )
1966 .expect("real TLE TCA Pc search succeeds");
1967 assert_eq!(conjunctions.len(), candidates.len());
1968 assert!(conjunctions.iter().all(|conjunction| {
1969 conjunction.collision_probability.pc.is_finite()
1970 && (0.0..=1.0).contains(&conjunction.collision_probability.pc)
1971 }));
1972 }
1973
1974 #[test]
1975 fn gnss_utility_modules_are_reexported() {
1976 assert_eq!(
1977 frequencies::frequency_hz(GnssSystem::Gps, frequencies::CarrierBand::L1),
1978 Some(constants::F_L1_HZ)
1979 );
1980 assert_eq!(GnssSystem::Gps.as_str(), "GPS");
1981 assert_eq!(GnssSystem::Gps.to_string(), "GPS");
1982 assert_eq!(frequencies::CarrierBand::L1.as_str(), "l1");
1983 assert_eq!(frequencies::CarrierBand::L1.to_string(), "l1");
1984 assert!(geometry::visible_at_elevation_mask(5.0, 5.0));
1985 assert!(combinations::gamma(constants::F_L1_HZ, constants::F_L2_HZ)
1986 .expect("GPS L1/L2 gamma")
1987 .is_finite());
1988 assert_eq!(
1989 carrier_phase::geometry_free(100.0, 60.0)
1990 .expect("finite geometry-free combination")
1991 .to_bits(),
1992 40.0_f64.to_bits()
1993 );
1994 assert!(quality::pseudorange_variance(
1995 30.0,
1996 quality::PseudorangeVarianceOptions::default()
1997 )
1998 .expect("positive elevation variance")
1999 .is_finite());
2000 assert_eq!(
2001 signal::ca_code(1).expect("GPS PRN 1").len(),
2002 signal::CA_CODE_LENGTH
2003 );
2004 assert_eq!(
2005 velocity::doppler_to_range_rate(-1.0, constants::F_L1_HZ)
2006 .expect("valid Doppler conversion")
2007 .to_bits(),
2008 (constants::C_M_S / constants::F_L1_HZ).to_bits()
2009 );
2010 assert_eq!(navigation::lnav::PREAMBLE, 0b1000_1011);
2011 assert_eq!(dgnss::CodeObservation::new("G01", 1.0).satellite_id, "G01");
2012 assert_eq!(
2013 sbas::sat_to_sbas_prn(sbas::sbas_prn_to_sat(120).expect("valid augmentation PRN")),
2014 Some(120)
2015 );
2016 assert!(core::mem::size_of::<geometry::VisibilityOptions>() > 0);
2017 assert!(core::mem::size_of::<broadcast_comparison::EpochInputs>() > 0);
2018 }
2019
2020 #[test]
2021 fn solve_spp_delegates_to_the_core_solver_and_maps_errors() {
2022 let sp3 = load_sp3(DEGENERATE_SP3).expect("the fixture parses");
2026 let sat = |prn| GnssSatelliteId::new(GnssSystem::Gps, prn).expect("valid satellite id");
2027 let inputs = SolveInputs {
2028 observations: vec![
2029 Observation {
2030 satellite_id: sat(1),
2031 pseudorange_m: 2.1e7,
2032 },
2033 Observation {
2034 satellite_id: sat(2),
2035 pseudorange_m: 2.1e7,
2036 },
2037 ],
2038 t_rx_j2000_s: 646_315_200.0,
2039 t_rx_second_of_day_s: 0.0,
2040 day_of_year: 176.0,
2041 initial_guess: [0.0, 0.0, 0.0, 0.0],
2042 corrections: Corrections::NONE,
2043 klobuchar: KlobucharCoeffs {
2044 alpha: [0.0; 4],
2045 beta: [0.0; 4],
2046 },
2047 beidou_klobuchar: None,
2048 galileo_nequick: None,
2049 sbas_iono: None,
2050 glonass_channels: std::collections::BTreeMap::new(),
2051 met: SurfaceMet {
2052 pressure_hpa: 1013.25,
2053 temperature_k: 288.15,
2054 relative_humidity: 0.5,
2055 },
2056 robust: None,
2057 };
2058
2059 let result = solve_spp(&sp3, &inputs, false, SolvePolicy::default());
2060 assert!(matches!(result, Err(Error::Spp(_))), "got {result:?}");
2061 if let Err(err) = result {
2062 assert!(err.to_string().contains("SPP solve failed"));
2063 assert!(std::error::Error::source(&err).is_some());
2064 }
2065 }
2066
2067 #[test]
2068 fn spp_robust_fde_driver_is_reexported_from_facade_root() {
2069 let sp3 = load_sp3(DEGENERATE_SP3).expect("the fixture parses");
2070 let sat = |prn| GnssSatelliteId::new(GnssSystem::Gps, prn).expect("valid satellite id");
2071 let inputs = SolveInputs {
2072 observations: vec![
2073 Observation {
2074 satellite_id: sat(1),
2075 pseudorange_m: 2.1e7,
2076 },
2077 Observation {
2078 satellite_id: sat(2),
2079 pseudorange_m: 2.1e7,
2080 },
2081 ],
2082 t_rx_j2000_s: 646_315_200.0,
2083 t_rx_second_of_day_s: 0.0,
2084 day_of_year: 176.0,
2085 initial_guess: [0.0, 0.0, 0.0, 0.0],
2086 corrections: Corrections::NONE,
2087 klobuchar: KlobucharCoeffs {
2088 alpha: [0.0; 4],
2089 beta: [0.0; 4],
2090 },
2091 beidou_klobuchar: None,
2092 galileo_nequick: None,
2093 sbas_iono: None,
2094 glonass_channels: std::collections::BTreeMap::new(),
2095 met: SurfaceMet {
2096 pressure_hpa: 1013.25,
2097 temperature_k: 288.15,
2098 relative_humidity: 0.5,
2099 },
2100 robust: None,
2101 };
2102 let options = quality::FdeSppOptions {
2103 fde: quality::FdeOptions {
2104 raim: quality::RaimOptions::default(),
2105 max_iterations: 0,
2106 },
2107 validation: quality::SolutionValidationOptions::default(),
2108 };
2109
2110 let result = spp_robust_fde_driver(
2111 &sp3,
2112 &inputs,
2113 false,
2114 positioning::RobustConfig::default(),
2115 &options,
2116 );
2117
2118 assert!(
2119 matches!(
2120 result,
2121 Err(quality::FdeError::Solve(quality::FdeSppError::Spp(_)))
2122 ),
2123 "got {result:?}"
2124 );
2125 }
2126
2127 #[test]
2128 fn solve_velocity_delegates_to_core_solver_and_maps_errors() {
2129 let sp3 = load_sp3(DEGENERATE_SP3).expect("the fixture parses");
2130 let result = solve_velocity(
2131 &sp3,
2132 &[],
2133 [0.0; 3],
2134 646_315_200.0,
2135 VelocitySolveOptions::default(),
2136 );
2137
2138 assert!(
2139 matches!(
2140 result,
2141 Err(Error::Velocity(velocity::VelocityError::NoObservations))
2142 ),
2143 "got {result:?}"
2144 );
2145 if let Err(err) = result {
2146 assert!(err.to_string().contains("velocity solve failed"));
2147 assert!(std::error::Error::source(&err).is_some());
2148 }
2149 }
2150
2151 #[test]
2152 fn solve_rtk_float_positional_wrapper_maps_errors() {
2153 let epochs = Vec::new();
2154 let ambiguity_ids = Vec::<String>::new();
2155 let model = rtk_model();
2156
2157 let err = solve_rtk_float(
2158 &epochs,
2159 [0.0; 3],
2160 &ambiguity_ids,
2161 [0.0; 3],
2162 &model,
2163 rtk_float_options(),
2164 None,
2165 )
2166 .unwrap_err();
2167
2168 assert!(matches!(err, Error::RtkFloat(_)));
2169 assert!(err.to_string().contains("RTK float"));
2170 assert!(std::error::Error::source(&err).is_some());
2171 }
2172
2173 #[test]
2174 fn solve_rtk_fixed_positional_wrapper_maps_errors() {
2175 let epochs = Vec::new();
2176 let ambiguity_ids = Vec::<String>::new();
2177 let ambiguity_satellites = BTreeMap::new();
2178 let wavelengths_m = BTreeMap::new();
2179 let offsets_m = BTreeMap::new();
2180 let float_only_systems = Vec::new();
2181 let model = rtk_model();
2182 let ambiguity_set = AmbiguitySet {
2183 ids: &ambiguity_ids,
2184 satellites: &ambiguity_satellites,
2185 scale: rtk_filter::AmbiguityScale {
2186 wavelengths_m: &wavelengths_m,
2187 offsets_m: &offsets_m,
2188 },
2189 float_only_systems: &float_only_systems,
2190 };
2191
2192 let err = solve_rtk_fixed(
2193 &epochs,
2194 [0.0; 3],
2195 ambiguity_set,
2196 [0.0; 3],
2197 &model,
2198 rtk_fixed_options(),
2199 None,
2200 )
2201 .unwrap_err();
2202
2203 assert!(matches!(err, Error::RtkFixed(_)));
2204 assert!(err.to_string().contains("fixed RTK"));
2205 assert!(std::error::Error::source(&err).is_some());
2206 }
2207
2208 #[test]
2209 fn solve_ppp_float_positional_wrapper_maps_errors_with_fixture_source() {
2210 let sp3 = load_sp3(DEGENERATE_SP3).expect("the fixture parses");
2211 let epochs = Vec::new();
2212
2213 let err = solve_ppp_float(&sp3, &epochs, empty_ppp_state(), ppp_float_solve_config())
2214 .unwrap_err();
2215
2216 assert!(matches!(err, Error::PppFloat(_)));
2217 assert!(err.to_string().contains("PPP float"));
2218 assert!(std::error::Error::source(&err).is_some());
2219 }
2220
2221 #[test]
2222 fn solve_ppp_fixed_positional_wrapper_maps_errors_with_fixture_source() {
2223 let sp3 = load_sp3(DEGENERATE_SP3).expect("the fixture parses");
2224 let epochs = Vec::new();
2225
2226 let err = solve_ppp_fixed(
2227 &sp3,
2228 &epochs,
2229 empty_ppp_float_solution(),
2230 ppp_fixed_solve_config(),
2231 )
2232 .unwrap_err();
2233
2234 assert!(matches!(err, Error::PppFixed(_)));
2235 assert!(err.to_string().contains("PPP"));
2236 assert!(std::error::Error::source(&err).is_some());
2237 }
2238
2239 #[test]
2240 fn solve_rtk_float_with_delegates_to_positional_solver() {
2241 let epochs = Vec::new();
2242 let ambiguity_ids = Vec::new();
2243 let model = rtk_model();
2244 let config = RtkFloatConfig {
2245 epochs: &epochs,
2246 base_ecef_m: [0.0; 3],
2247 ambiguity_ids: &ambiguity_ids,
2248 initial_baseline_m: [0.0; 3],
2249 model: &model,
2250 options: rtk_float_options(),
2251 receiver_antenna_corrections: None,
2252 };
2253
2254 let typed = solve_rtk_float_with(config.clone()).unwrap_err();
2255 let positional = solve_rtk_float(
2256 config.epochs,
2257 config.base_ecef_m,
2258 config.ambiguity_ids,
2259 config.initial_baseline_m,
2260 config.model,
2261 config.options,
2262 config.receiver_antenna_corrections,
2263 )
2264 .unwrap_err();
2265
2266 assert!(matches!(typed, Error::RtkFloat(_)));
2267 assert_eq!(typed.to_string(), positional.to_string());
2268 }
2269
2270 #[test]
2271 fn solve_rtk_float_with_rejects_receiver_antenna_zero_base_geometry() {
2272 let base = [0.0; 3];
2273 let baseline = [1.0, 0.0, 0.0];
2274 let rover = [
2275 base[0] + baseline[0],
2276 base[1] + baseline[1],
2277 base[2] + baseline[2],
2278 ];
2279 let g01 = [15_000_000.0, 7_000_000.0, 21_000_000.0];
2280 let g02 = [-12_000_000.0, 18_000_000.0, 19_000_000.0];
2281 let range_m = |sat: [f64; 3], recv: [f64; 3]| {
2282 let dx = sat[0] - recv[0];
2283 let dy = sat[1] - recv[1];
2284 let dz = sat[2] - recv[2];
2285 (dx * dx + dy * dy + dz * dz).sqrt()
2286 };
2287 let mk = |sat: [f64; 3], id: &str| rtk_filter::SatMeas {
2288 sat: id.into(),
2289 sd_ambiguity_id: id.into(),
2290 base_code_m: range_m(sat, base),
2291 base_phase_m: range_m(sat, base),
2292 rover_code_m: range_m(sat, rover),
2293 rover_phase_m: range_m(sat, rover),
2294 base_tx_pos: sat,
2295 rover_tx_pos: sat,
2296 pos: sat,
2297 };
2298 let epochs = vec![rtk_filter::Epoch {
2299 references: vec![mk(g01, "G01")],
2300 nonref: vec![mk(g02, "G02")],
2301 velocity_mps: None,
2302 dt_s: 0.0,
2303 }];
2304 let ambiguity_ids = vec!["G02".to_string()];
2305 let model = rtk_model();
2306 let cal = rtk_filter::ReceiverAntennaCalibration {
2307 pco_neu_m: [0.0, 0.0, 0.0],
2308 noazi_pcv_m: vec![(0.0, 0.0)],
2309 azi_pcv_m: Vec::new(),
2310 };
2311 let corrections = ReceiverAntennaCorrections {
2312 base: cal.clone(),
2313 rover: cal,
2314 };
2315 let config =
2316 RtkFloatConfig::new(&epochs, base, &ambiguity_ids, &model, rtk_float_options())
2317 .with_initial_baseline_m(baseline)
2318 .with_receiver_antenna_corrections(Some(&corrections));
2319
2320 let err = solve_rtk_float_with(config).unwrap_err();
2321
2322 assert!(matches!(
2323 err,
2324 Error::RtkFloat(rtk_filter::FloatSolveError::ReceiverAntenna(
2325 rtk_filter::ReceiverAntennaError::InvalidGeometry
2326 ))
2327 ));
2328 }
2329
2330 #[test]
2331 fn solve_rtk_fixed_with_delegates_to_positional_solver() {
2332 let epochs = Vec::new();
2333 let ambiguity_ids = Vec::new();
2334 let ambiguity_satellites = BTreeMap::new();
2335 let wavelengths_m = BTreeMap::new();
2336 let offsets_m = BTreeMap::new();
2337 let float_only_systems = Vec::new();
2338 let model = rtk_model();
2339 let ambiguity_set = AmbiguitySet {
2340 ids: &ambiguity_ids,
2341 satellites: &ambiguity_satellites,
2342 scale: rtk_filter::AmbiguityScale {
2343 wavelengths_m: &wavelengths_m,
2344 offsets_m: &offsets_m,
2345 },
2346 float_only_systems: &float_only_systems,
2347 };
2348 let config = RtkFixedConfig {
2349 epochs: &epochs,
2350 base_ecef_m: [0.0; 3],
2351 initial_ambiguities: ambiguity_set,
2352 initial_baseline_m: [0.0; 3],
2353 model: &model,
2354 options: rtk_fixed_options(),
2355 receiver_antenna_corrections: None,
2356 };
2357
2358 let typed = solve_rtk_fixed_with(config.clone()).unwrap_err();
2359 let positional = solve_rtk_fixed(
2360 config.epochs,
2361 config.base_ecef_m,
2362 config.initial_ambiguities,
2363 config.initial_baseline_m,
2364 config.model,
2365 config.options,
2366 config.receiver_antenna_corrections,
2367 )
2368 .unwrap_err();
2369
2370 assert!(matches!(typed, Error::RtkFixed(_)));
2371 assert_eq!(typed.to_string(), positional.to_string());
2372 }
2373
2374 #[test]
2375 fn solve_ppp_float_with_delegates_to_positional_solver() {
2376 let sp3 = load_sp3(DEGENERATE_SP3).expect("the fixture parses");
2377 let epochs = Vec::new();
2378 let config = PppFloatConfig {
2379 source: &sp3,
2380 epochs: &epochs,
2381 initial_state: empty_ppp_state(),
2382 solve: ppp_float_solve_config(),
2383 };
2384
2385 let typed = solve_ppp_float_with(config.clone()).unwrap_err();
2386 let positional = solve_ppp_float(
2387 config.source,
2388 config.epochs,
2389 config.initial_state,
2390 config.solve,
2391 )
2392 .unwrap_err();
2393
2394 assert!(matches!(typed, Error::PppFloat(_)));
2395 assert_eq!(typed.to_string(), positional.to_string());
2396 }
2397
2398 #[test]
2399 fn solve_ppp_fixed_with_delegates_to_positional_solver() {
2400 let sp3 = load_sp3(DEGENERATE_SP3).expect("the fixture parses");
2401 let epochs = Vec::new();
2402 let config = PppFixedConfig {
2403 source: &sp3,
2404 epochs: &epochs,
2405 float_solution: empty_ppp_float_solution(),
2406 solve: ppp_fixed_solve_config(),
2407 };
2408
2409 let typed = solve_ppp_fixed_with(config.clone()).unwrap_err();
2410 let positional = solve_ppp_fixed(
2411 config.source,
2412 config.epochs,
2413 config.float_solution,
2414 config.solve,
2415 )
2416 .unwrap_err();
2417
2418 assert!(matches!(typed, Error::PppFixed(_)));
2419 assert_eq!(typed.to_string(), positional.to_string());
2420 }
2421}