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