sidereon 2.0.0__tar.gz → 2.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (274) hide show
  1. {sidereon-2.0.0 → sidereon-2.1.0}/CHANGELOG.md +16 -0
  2. {sidereon-2.0.0 → sidereon-2.1.0}/Cargo.lock +5 -5
  3. {sidereon-2.0.0 → sidereon-2.1.0}/Cargo.toml +3 -3
  4. {sidereon-2.0.0 → sidereon-2.1.0}/PKG-INFO +7 -4
  5. {sidereon-2.0.0 → sidereon-2.1.0}/README.md +6 -3
  6. {sidereon-2.0.0 → sidereon-2.1.0}/pyproject.toml +1 -1
  7. {sidereon-2.0.0 → sidereon-2.1.0}/python/sidereon/__init__.pyi +48 -4
  8. {sidereon-2.0.0 → sidereon-2.1.0}/src/ephemeris.rs +150 -34
  9. sidereon-2.1.0/tests/test_sp3_interpolation_policy.py +285 -0
  10. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_sp3_window_continuity.py +1 -1
  11. {sidereon-2.0.0 → sidereon-2.1.0}/uv.lock +1 -1
  12. {sidereon-2.0.0 → sidereon-2.1.0}/.cargo/config.toml.disabled +0 -0
  13. {sidereon-2.0.0 → sidereon-2.1.0}/.gitattributes +0 -0
  14. {sidereon-2.0.0 → sidereon-2.1.0}/.github/workflows/ci.yml +0 -0
  15. {sidereon-2.0.0 → sidereon-2.1.0}/.github/workflows/release.yml +0 -0
  16. {sidereon-2.0.0 → sidereon-2.1.0}/.gitignore +0 -0
  17. {sidereon-2.0.0 → sidereon-2.1.0}/COVERAGE.md +0 -0
  18. {sidereon-2.0.0 → sidereon-2.1.0}/LICENSE +0 -0
  19. {sidereon-2.0.0 → sidereon-2.1.0}/THIRD-PARTY-NOTICES.md +0 -0
  20. {sidereon-2.0.0 → sidereon-2.1.0}/bench/README.md +0 -0
  21. {sidereon-2.0.0 → sidereon-2.1.0}/bench/bench_fleet.py +0 -0
  22. {sidereon-2.0.0 → sidereon-2.1.0}/docs/public-gnss-distribution-sources.md +0 -0
  23. {sidereon-2.0.0 → sidereon-2.1.0}/examples/clean_a_phone_track.ipynb +0 -0
  24. {sidereon-2.0.0 → sidereon-2.1.0}/examples/static_occupation_vs_cors.ipynb +0 -0
  25. {sidereon-2.0.0 → sidereon-2.1.0}/python/sidereon/__init__.py +0 -0
  26. {sidereon-2.0.0 → sidereon-2.1.0}/python/sidereon/_compression.py +0 -0
  27. {sidereon-2.0.0 → sidereon-2.1.0}/python/sidereon/_exact_cache.py +0 -0
  28. {sidereon-2.0.0 → sidereon-2.1.0}/python/sidereon/_ingress.py +0 -0
  29. {sidereon-2.0.0 → sidereon-2.1.0}/python/sidereon/data.py +0 -0
  30. {sidereon-2.0.0 → sidereon-2.1.0}/python/sidereon/data.pyi +0 -0
  31. {sidereon-2.0.0 → sidereon-2.1.0}/python/sidereon/distribution.py +0 -0
  32. {sidereon-2.0.0 → sidereon-2.1.0}/python/sidereon/distribution.pyi +0 -0
  33. {sidereon-2.0.0 → sidereon-2.1.0}/python/sidereon/exact_cache.py +0 -0
  34. {sidereon-2.0.0 → sidereon-2.1.0}/python/sidereon/exact_cache.pyi +0 -0
  35. {sidereon-2.0.0 → sidereon-2.1.0}/python/sidereon/exact_sp3.py +0 -0
  36. {sidereon-2.0.0 → sidereon-2.1.0}/python/sidereon/exact_sp3.pyi +0 -0
  37. {sidereon-2.0.0 → sidereon-2.1.0}/python/sidereon/ntrip.py +0 -0
  38. {sidereon-2.0.0 → sidereon-2.1.0}/python/sidereon/ntrip.pyi +0 -0
  39. {sidereon-2.0.0 → sidereon-2.1.0}/python/sidereon/py.typed +0 -0
  40. {sidereon-2.0.0 → sidereon-2.1.0}/scripts/check-release.py +0 -0
  41. {sidereon-2.0.0 → sidereon-2.1.0}/scripts/smoke-release.py +0 -0
  42. {sidereon-2.0.0 → sidereon-2.1.0}/src/almanac.rs +0 -0
  43. {sidereon-2.0.0 → sidereon-2.1.0}/src/angles.rs +0 -0
  44. {sidereon-2.0.0 → sidereon-2.1.0}/src/anomaly.rs +0 -0
  45. {sidereon-2.0.0 → sidereon-2.1.0}/src/araim.rs +0 -0
  46. {sidereon-2.0.0 → sidereon-2.1.0}/src/atmosphere.rs +0 -0
  47. {sidereon-2.0.0 → sidereon-2.1.0}/src/bias.rs +0 -0
  48. {sidereon-2.0.0 → sidereon-2.1.0}/src/bodies.rs +0 -0
  49. {sidereon-2.0.0 → sidereon-2.1.0}/src/body_observe.rs +0 -0
  50. {sidereon-2.0.0 → sidereon-2.1.0}/src/broadcast_comparison.rs +0 -0
  51. {sidereon-2.0.0 → sidereon-2.1.0}/src/cdm.rs +0 -0
  52. {sidereon-2.0.0 → sidereon-2.1.0}/src/clock_stability.rs +0 -0
  53. {sidereon-2.0.0 → sidereon-2.1.0}/src/conjunction.rs +0 -0
  54. {sidereon-2.0.0 → sidereon-2.1.0}/src/constellation.rs +0 -0
  55. {sidereon-2.0.0 → sidereon-2.1.0}/src/covariance.rs +0 -0
  56. {sidereon-2.0.0 → sidereon-2.1.0}/src/coverage.rs +0 -0
  57. {sidereon-2.0.0 → sidereon-2.1.0}/src/data_catalog.rs +0 -0
  58. {sidereon-2.0.0 → sidereon-2.1.0}/src/defaults.rs +0 -0
  59. {sidereon-2.0.0 → sidereon-2.1.0}/src/dgnss.rs +0 -0
  60. {sidereon-2.0.0 → sidereon-2.1.0}/src/doppler.rs +0 -0
  61. {sidereon-2.0.0 → sidereon-2.1.0}/src/elements.rs +0 -0
  62. {sidereon-2.0.0 → sidereon-2.1.0}/src/emission.rs +0 -0
  63. {sidereon-2.0.0 → sidereon-2.1.0}/src/equinoctial.rs +0 -0
  64. {sidereon-2.0.0 → sidereon-2.1.0}/src/error_metrics.rs +0 -0
  65. {sidereon-2.0.0 → sidereon-2.1.0}/src/estimation.rs +0 -0
  66. {sidereon-2.0.0 → sidereon-2.1.0}/src/events.rs +0 -0
  67. {sidereon-2.0.0 → sidereon-2.1.0}/src/exact_cache.rs +0 -0
  68. {sidereon-2.0.0 → sidereon-2.1.0}/src/fallback.rs +0 -0
  69. {sidereon-2.0.0 → sidereon-2.1.0}/src/forces.rs +0 -0
  70. {sidereon-2.0.0 → sidereon-2.1.0}/src/frame_catalog.rs +0 -0
  71. {sidereon-2.0.0 → sidereon-2.1.0}/src/frames.rs +0 -0
  72. {sidereon-2.0.0 → sidereon-2.1.0}/src/fusion.rs +0 -0
  73. {sidereon-2.0.0 → sidereon-2.1.0}/src/geodesic.rs +0 -0
  74. {sidereon-2.0.0 → sidereon-2.1.0}/src/geodetic_time_series.rs +0 -0
  75. {sidereon-2.0.0 → sidereon-2.1.0}/src/geofence.rs +0 -0
  76. {sidereon-2.0.0 → sidereon-2.1.0}/src/geoid.rs +0 -0
  77. {sidereon-2.0.0 → sidereon-2.1.0}/src/geometry.rs +0 -0
  78. {sidereon-2.0.0 → sidereon-2.1.0}/src/geometry_quality.rs +0 -0
  79. {sidereon-2.0.0 → sidereon-2.1.0}/src/ils.rs +0 -0
  80. {sidereon-2.0.0 → sidereon-2.1.0}/src/iod.rs +0 -0
  81. {sidereon-2.0.0 → sidereon-2.1.0}/src/ionex.rs +0 -0
  82. {sidereon-2.0.0 → sidereon-2.1.0}/src/lambert.rs +0 -0
  83. {sidereon-2.0.0 → sidereon-2.1.0}/src/leap.rs +0 -0
  84. {sidereon-2.0.0 → sidereon-2.1.0}/src/least_squares.rs +0 -0
  85. {sidereon-2.0.0 → sidereon-2.1.0}/src/lib.rs +0 -0
  86. {sidereon-2.0.0 → sidereon-2.1.0}/src/lnav.rs +0 -0
  87. {sidereon-2.0.0 → sidereon-2.1.0}/src/marshal.rs +0 -0
  88. {sidereon-2.0.0 → sidereon-2.1.0}/src/nmea.rs +0 -0
  89. {sidereon-2.0.0 → sidereon-2.1.0}/src/normality.rs +0 -0
  90. {sidereon-2.0.0 → sidereon-2.1.0}/src/ntrip.rs +0 -0
  91. {sidereon-2.0.0 → sidereon-2.1.0}/src/observables.rs +0 -0
  92. {sidereon-2.0.0 → sidereon-2.1.0}/src/observation.rs +0 -0
  93. {sidereon-2.0.0 → sidereon-2.1.0}/src/oem.rs +0 -0
  94. {sidereon-2.0.0 → sidereon-2.1.0}/src/omm.rs +0 -0
  95. {sidereon-2.0.0 → sidereon-2.1.0}/src/opm.rs +0 -0
  96. {sidereon-2.0.0 → sidereon-2.1.0}/src/orbit_determination.rs +0 -0
  97. {sidereon-2.0.0 → sidereon-2.1.0}/src/ppp.rs +0 -0
  98. {sidereon-2.0.0 → sidereon-2.1.0}/src/ppp_corrections.rs +0 -0
  99. {sidereon-2.0.0 → sidereon-2.1.0}/src/products.rs +0 -0
  100. {sidereon-2.0.0 → sidereon-2.1.0}/src/propagation.rs +0 -0
  101. {sidereon-2.0.0 → sidereon-2.1.0}/src/qc.rs +0 -0
  102. {sidereon-2.0.0 → sidereon-2.1.0}/src/reduced_orbit.rs +0 -0
  103. {sidereon-2.0.0 → sidereon-2.1.0}/src/relative.rs +0 -0
  104. {sidereon-2.0.0 → sidereon-2.1.0}/src/reliability.rs +0 -0
  105. {sidereon-2.0.0 → sidereon-2.1.0}/src/rf.rs +0 -0
  106. {sidereon-2.0.0 → sidereon-2.1.0}/src/rinex.rs +0 -0
  107. {sidereon-2.0.0 → sidereon-2.1.0}/src/rtcm.rs +0 -0
  108. {sidereon-2.0.0 → sidereon-2.1.0}/src/rtk.rs +0 -0
  109. {sidereon-2.0.0 → sidereon-2.1.0}/src/sbas_pl.rs +0 -0
  110. {sidereon-2.0.0 → sidereon-2.1.0}/src/sbas_ssr.rs +0 -0
  111. {sidereon-2.0.0 → sidereon-2.1.0}/src/scenario.rs +0 -0
  112. {sidereon-2.0.0 → sidereon-2.1.0}/src/sidereal.rs +0 -0
  113. {sidereon-2.0.0 → sidereon-2.1.0}/src/signal_analysis.rs +0 -0
  114. {sidereon-2.0.0 → sidereon-2.1.0}/src/sky.rs +0 -0
  115. {sidereon-2.0.0 → sidereon-2.1.0}/src/source_localization.rs +0 -0
  116. {sidereon-2.0.0 → sidereon-2.1.0}/src/space_weather.rs +0 -0
  117. {sidereon-2.0.0 → sidereon-2.1.0}/src/spk.rs +0 -0
  118. {sidereon-2.0.0 → sidereon-2.1.0}/src/spp.rs +0 -0
  119. {sidereon-2.0.0 → sidereon-2.1.0}/src/staleness.rs +0 -0
  120. {sidereon-2.0.0 → sidereon-2.1.0}/src/static_positioning.rs +0 -0
  121. {sidereon-2.0.0 → sidereon-2.1.0}/src/tca.rs +0 -0
  122. {sidereon-2.0.0 → sidereon-2.1.0}/src/tdm.rs +0 -0
  123. {sidereon-2.0.0 → sidereon-2.1.0}/src/terrain.rs +0 -0
  124. {sidereon-2.0.0 → sidereon-2.1.0}/src/terrain_store.rs +0 -0
  125. {sidereon-2.0.0 → sidereon-2.1.0}/src/tides.rs +0 -0
  126. {sidereon-2.0.0 → sidereon-2.1.0}/src/tropo.rs +0 -0
  127. {sidereon-2.0.0 → sidereon-2.1.0}/src/unix_compress.rs +0 -0
  128. {sidereon-2.0.0 → sidereon-2.1.0}/tests/_helpers.py +0 -0
  129. {sidereon-2.0.0 → sidereon-2.1.0}/tests/fixtures/README.md +0 -0
  130. {sidereon-2.0.0 → sidereon-2.1.0}/tests/fixtures/antex/igs20_pasa_scoa_gps.atx +0 -0
  131. {sidereon-2.0.0 → sidereon-2.1.0}/tests/fixtures/antex/igs20_wettzell_trim.atx +0 -0
  132. {sidereon-2.0.0 → sidereon-2.1.0}/tests/fixtures/batch_fleet.json +0 -0
  133. {sidereon-2.0.0 → sidereon-2.1.0}/tests/fixtures/cdm.json +0 -0
  134. {sidereon-2.0.0 → sidereon-2.1.0}/tests/fixtures/clk/synthetic_rinex_clock.clk +0 -0
  135. {sidereon-2.0.0 → sidereon-2.1.0}/tests/fixtures/conjunction.json +0 -0
  136. {sidereon-2.0.0 → sidereon-2.1.0}/tests/fixtures/constellation/gps_ops_sample.json +0 -0
  137. {sidereon-2.0.0 → sidereon-2.1.0}/tests/fixtures/constellation/navcen_forecast_cases.html +0 -0
  138. {sidereon-2.0.0 → sidereon-2.1.0}/tests/fixtures/constellation/navcen_gps_sample.html +0 -0
  139. {sidereon-2.0.0 → sidereon-2.1.0}/tests/fixtures/events_bodies_dop.json +0 -0
  140. {sidereon-2.0.0 → sidereon-2.1.0}/tests/fixtures/frames_time.json +0 -0
  141. {sidereon-2.0.0 → sidereon-2.1.0}/tests/fixtures/geodesic/geodtest_row.dat +0 -0
  142. {sidereon-2.0.0 → sidereon-2.1.0}/tests/fixtures/geoid/egm2008_25_norcal_crop.bin +0 -0
  143. {sidereon-2.0.0 → sidereon-2.1.0}/tests/fixtures/gnss_data/igs22376.sp3.Z.b64 +0 -0
  144. {sidereon-2.0.0 → sidereon-2.1.0}/tests/fixtures/gnss_data/ncompress-current-b9-counter.Z.b64 +0 -0
  145. {sidereon-2.0.0 → sidereon-2.1.0}/tests/fixtures/listings/aiub-iono-p1p2-20260804.csv +0 -0
  146. {sidereon-2.0.0 → sidereon-2.1.0}/tests/fixtures/listings/gfz-ultra-w2430-20260804.html +0 -0
  147. {sidereon-2.0.0 → sidereon-2.1.0}/tests/fixtures/nav/BRDC00GOP_R_20210010000_01D_MN.rnx +0 -0
  148. {sidereon-2.0.0 → sidereon-2.1.0}/tests/fixtures/nav/ESBC00DNK_R_20201770000_01D_MN.rnx +0 -0
  149. {sidereon-2.0.0 → sidereon-2.1.0}/tests/fixtures/nav/KMS300DNK_R_20221591000_01H_MN.rnx +0 -0
  150. {sidereon-2.0.0 → sidereon-2.1.0}/tests/fixtures/numerical_propagation.json +0 -0
  151. {sidereon-2.0.0 → sidereon-2.1.0}/tests/fixtures/obs/ESBC00DNK_R_20201770000_01D_30S_MO_trim.crx +0 -0
  152. {sidereon-2.0.0 → sidereon-2.1.0}/tests/fixtures/obs/ESBC00DNK_R_20201770000_01D_30S_MO_trim.rnx +0 -0
  153. {sidereon-2.0.0 → sidereon-2.1.0}/tests/fixtures/obs/algo0010_2015001_v1_trim.crx +0 -0
  154. {sidereon-2.0.0 → sidereon-2.1.0}/tests/fixtures/obs/algo0010_2015001_v1_trim.rnx +0 -0
  155. {sidereon-2.0.0 → sidereon-2.1.0}/tests/fixtures/omm.json +0 -0
  156. {sidereon-2.0.0 → sidereon-2.1.0}/tests/fixtures/pass_finder.json +0 -0
  157. {sidereon-2.0.0 → sidereon-2.1.0}/tests/fixtures/ppp_esbc.json +0 -0
  158. {sidereon-2.0.0 → sidereon-2.1.0}/tests/fixtures/rf_link_budget.json +0 -0
  159. {sidereon-2.0.0 → sidereon-2.1.0}/tests/fixtures/rtk_wtzr.json +0 -0
  160. {sidereon-2.0.0 → sidereon-2.1.0}/tests/fixtures/sgp4_topocentric.json +0 -0
  161. {sidereon-2.0.0 → sidereon-2.1.0}/tests/fixtures/sp3/IGS0OPSFIN_20261200945_02H30M_15M_ORB.SP3 +0 -0
  162. {sidereon-2.0.0 → sidereon-2.1.0}/tests/fixtures/sp3-merge-input-v1.json +0 -0
  163. {sidereon-2.0.0 → sidereon-2.1.0}/tests/fixtures/sp3_bodies.json +0 -0
  164. {sidereon-2.0.0 → sidereon-2.1.0}/tests/fixtures/spk/horizons_eros_type21.bsp +0 -0
  165. {sidereon-2.0.0 → sidereon-2.1.0}/tests/fixtures/tdm/annex_e_01.kvn +0 -0
  166. {sidereon-2.0.0 → sidereon-2.1.0}/tests/fixtures/tle_roundtrip.json +0 -0
  167. {sidereon-2.0.0 → sidereon-2.1.0}/tests/golden/sp3-terminal-record-v1.json +0 -0
  168. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_012_bindings.py +0 -0
  169. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_013_bindings.py +0 -0
  170. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_014_bindings.py +0 -0
  171. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_015_bindings.py +0 -0
  172. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_016_wave2_bindings.py +0 -0
  173. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_017_domain_exposure.py +0 -0
  174. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_018_domain_exposure.py +0 -0
  175. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_019_track.py +0 -0
  176. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_020_error_metrics.py +0 -0
  177. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_antex.py +0 -0
  178. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_atmosphere.py +0 -0
  179. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_attested_open.py +0 -0
  180. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_batch.py +0 -0
  181. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_broadcast_comparison.py +0 -0
  182. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_broadcast_fallback.py +0 -0
  183. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_cdm.py +0 -0
  184. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_conjunction.py +0 -0
  185. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_constellation.py +0 -0
  186. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_constellation_fleet.py +0 -0
  187. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_core_defaults.py +0 -0
  188. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_core_public_api_parity.py +0 -0
  189. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_covariance.py +0 -0
  190. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_covariance_propagation.py +0 -0
  191. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_coverage.py +0 -0
  192. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_crinex.py +0 -0
  193. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_data.py +0 -0
  194. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_data_publication_resilience.py +0 -0
  195. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_dgnss.py +0 -0
  196. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_distribution.py +0 -0
  197. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_distribution_fallback.py +0 -0
  198. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_dop_at_epoch.py +0 -0
  199. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_dop_convention.py +0 -0
  200. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_doppler.py +0 -0
  201. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_egm96.py +0 -0
  202. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_elements.py +0 -0
  203. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_events_bodies_dop.py +0 -0
  204. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_exact_cache_atomicity.py +0 -0
  205. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_field_mode_bindings.py +0 -0
  206. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_forces.py +0 -0
  207. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_frames_time.py +0 -0
  208. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_ftp_transport.py +0 -0
  209. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_geoid.py +0 -0
  210. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_geometry.py +0 -0
  211. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_ils.py +0 -0
  212. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_import.py +0 -0
  213. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_ionex.py +0 -0
  214. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_ionosphere_nequick.py +0 -0
  215. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_klobuchar_native.py +0 -0
  216. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_lambert_iod.py +0 -0
  217. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_leap_seconds.py +0 -0
  218. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_least_squares.py +0 -0
  219. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_lnav.py +0 -0
  220. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_moving_baseline.py +0 -0
  221. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_new_core_api.py +0 -0
  222. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_next_issue_due.py +0 -0
  223. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_ntrip.py +0 -0
  224. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_numerical.py +0 -0
  225. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_observables.py +0 -0
  226. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_observables_broadcast.py +0 -0
  227. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_observation.py +0 -0
  228. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_observation_qc.py +0 -0
  229. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_observe_batch.py +0 -0
  230. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_oem.py +0 -0
  231. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_omm.py +0 -0
  232. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_opm.py +0 -0
  233. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_parity_gaps.py +0 -0
  234. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_parse_tle_file.py +0 -0
  235. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_passes.py +0 -0
  236. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_ppp.py +0 -0
  237. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_ppp_corrections.py +0 -0
  238. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_precise_samples.py +0 -0
  239. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_propagation.py +0 -0
  240. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_qc.py +0 -0
  241. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_reduced_orbit.py +0 -0
  242. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_reduced_orbit_piecewise.py +0 -0
  243. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_residual_stats.py +0 -0
  244. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_rf.py +0 -0
  245. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_rinex_clock.py +0 -0
  246. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_rinex_nav.py +0 -0
  247. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_rinex_obs.py +0 -0
  248. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_rtcm.py +0 -0
  249. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_rtk.py +0 -0
  250. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_sky.py +0 -0
  251. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_sp3_bodies.py +0 -0
  252. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_sp3_catalog_exact.py +0 -0
  253. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_sp3_clock_reference.py +0 -0
  254. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_sp3_merge.py +0 -0
  255. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_sp3_terminal_record.py +0 -0
  256. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_spk.py +0 -0
  257. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_spp.py +0 -0
  258. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_spp_glonass.py +0 -0
  259. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_spp_robustness.py +0 -0
  260. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_staleness.py +0 -0
  261. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_stub_accuracy.py +0 -0
  262. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_tca.py +0 -0
  263. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_tides.py +0 -0
  264. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_tle_fit.py +0 -0
  265. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_tle_roundtrip.py +0 -0
  266. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_tropo.py +0 -0
  267. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_unix_compress.py +0 -0
  268. {sidereon-2.0.0 → sidereon-2.1.0}/tests/test_visible_ground_track.py +0 -0
  269. {sidereon-2.0.0 → sidereon-2.1.0}/third_party_licenses/ERFA-BSD-3-Clause.txt +0 -0
  270. {sidereon-2.0.0 → sidereon-2.1.0}/third_party_licenses/IERS-CONVENTIONS-SOFTWARE-LICENSE.txt +0 -0
  271. {sidereon-2.0.0 → sidereon-2.1.0}/third_party_licenses/SciPy-BSD-3-Clause.txt +0 -0
  272. {sidereon-2.0.0 → sidereon-2.1.0}/third_party_source/sidereon-core-0.35.0/tides/mod.rs +0 -0
  273. {sidereon-2.0.0 → sidereon-2.1.0}/third_party_source/sidereon-core-0.35.0/tides/ocean.rs +0 -0
  274. {sidereon-2.0.0 → sidereon-2.1.0}/third_party_source/sidereon-core-0.35.0/tides/pole.rs +0 -0
@@ -4,6 +4,22 @@ All notable changes to the Sidereon Python interface are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [2.1.0] - 2026-09-05
8
+
9
+ ### Added
10
+
11
+ - Configurable SP3 coverage-gap interpolation policy (`gap_threshold_factor`, default 1.5):
12
+ - `load_sp3(..., gap_threshold_factor=None)` loads SP3 products with an explicit gap threshold factor.
13
+ - `Sp3.gap_threshold_factor` reads the active factor; `Sp3.with_interpolation_options(factor)` returns a copy with the updated policy.
14
+ - `Sp3.check_continuity(..., gap_threshold_factor=None)` and `Sp3.continuity_verdict(..., gap_threshold_factor=None)` accept an optional gap threshold factor override.
15
+ - `PreciseEphemerisSamples.from_samples(..., gap_threshold_factor=None)` preserves or overrides the factor; `PreciseEphemerisSamples.gap_threshold_factor` and `with_interpolation_options(factor)` inspect and update it.
16
+ - `PreciseEphemerisInterpolant.from_sp3(..., gap_threshold_factor=None)`, `from_samples(..., gap_threshold_factor=None)`, and `from_precise_ephemeris_samples(..., gap_threshold_factor=None)` accept an optional factor; `PreciseEphemerisInterpolant.gap_threshold_factor` and `with_interpolation_options(factor)` inspect and update it.
17
+ - `Sp3.precise_interpolant_artifact_bytes(gap_threshold_factor=None)`, `build_precise_interpolant_artifact_bytes(sp3, gap_threshold_factor=None)`, and `PreciseInterpolantArtifact.gap_threshold_factor` serialize, build, and inspect precomputed artifacts with the interpolation policy.
18
+
19
+ ### Changed
20
+
21
+ - Engine update: sidereon 2.1.0 / sidereon-core 2.1.0. Additive upstream release: the SP3 coverage-gap threshold is now a validated, product-carried policy (`Sp3InterpolationOptions`, default 1.5 and bit-identical to before), the SP3 window-scoped continuity reach is derived from the interpolator's actual selectable node spans, and RINEX 4 CNAV week/TOW round trips are stable at the week boundary.
22
+
7
23
  ## [2.0.0] - 2026-09-03
8
24
 
9
25
  ### Changed
@@ -588,9 +588,9 @@ checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
588
588
 
589
589
  [[package]]
590
590
  name = "sidereon"
591
- version = "2.0.0"
591
+ version = "2.1.0"
592
592
  source = "registry+https://github.com/rust-lang/crates.io-index"
593
- checksum = "7aaadf9ee0b3917f63996d4183cafe03937c352c715a9135681dae1aa3e81325"
593
+ checksum = "34f5f297673a6c59dd5f22fa93124f8502a1ffa39b4c5c3c2f75d764a2ead2d0"
594
594
  dependencies = [
595
595
  "crc32fast",
596
596
  "flate2",
@@ -599,9 +599,9 @@ dependencies = [
599
599
 
600
600
  [[package]]
601
601
  name = "sidereon-core"
602
- version = "2.0.0"
602
+ version = "2.1.0"
603
603
  source = "registry+https://github.com/rust-lang/crates.io-index"
604
- checksum = "5b3fe43597d67daf76b3e030cee2de20cea5b94e8044189e97b49f0cdf14ba03"
604
+ checksum = "f6dea92a59883415b579bded7065f8f233c4d069a1677c629685eef7b6620d17"
605
605
  dependencies = [
606
606
  "approx",
607
607
  "cc",
@@ -622,7 +622,7 @@ dependencies = [
622
622
 
623
623
  [[package]]
624
624
  name = "sidereon-py"
625
- version = "2.0.0"
625
+ version = "2.1.0"
626
626
  dependencies = [
627
627
  "nalgebra",
628
628
  "numpy",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "sidereon-py"
3
- version = "2.0.0"
3
+ version = "2.1.0"
4
4
  authors = []
5
5
  edition = "2021"
6
6
  description = "Python (PyO3) bindings over the sidereon ergonomic API: SP3 loading and SPP/RTK/PPP positioning solves"
@@ -21,8 +21,8 @@ exact-cache-test-failpoints = ["sidereon-core/exact-cache-test-failpoints"]
21
21
  # The thin ergonomic engine surface this binding marshals to. No modeling logic
22
22
  # lives here: every solve delegates to a `sidereon` entry point, which in turn
23
23
  # delegates to `sidereon-core`.
24
- sidereon = "2.0.0"
25
- sidereon-core = { version = "2.0.0", features = ["mmap"] }
24
+ sidereon = "2.1.0"
25
+ sidereon-core = { version = "2.1.0", features = ["mmap"] }
26
26
  # The standalone general-purpose trust-region least-squares solver: a data-driven
27
27
  # `least_squares` whose residual and Jacobian are evaluated entirely in Rust, so
28
28
  # no per-iteration Python callback is paid.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sidereon
3
- Version: 2.0.0
3
+ Version: 2.1.0
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: 3
6
6
  Classifier: Topic :: Scientific/Engineering
@@ -157,9 +157,12 @@ The Python package mirrors the full breadth of the engine.
157
157
  decode and SSR store helpers, and NTRIP client stream handling.
158
158
  - **Ephemeris and time:** broadcast ephemeris and precise SP3 products, JPL SPK
159
159
  (DAF/.bsp) kernels, uniform satellite-state sampling across broadcast and
160
- precise sources with batched multi-satellite interpolation, scale-aware time
161
- (UTC/TAI/TT/TDB/UT1 and the GNSS system times) with leap-second handling,
162
- and Earth orientation parameters (EOP).
160
+ precise sources with batched multi-satellite interpolation, configurable
161
+ SP3 coverage-gap interpolation policy (`gap_threshold_factor`, default 1.5)
162
+ and window-scoped continuity verification options (`check_continuity` and
163
+ `continuity_verdict`), scale-aware time (UTC/TAI/TT/TDB/UT1 and the GNSS
164
+ system times) with leap-second handling, and Earth orientation parameters
165
+ (EOP).
163
166
  - **Timing and clocks:** Allan-family stability analysis (ADEV/MDEV/HDEV/TDEV)
164
167
  and power-law clock-noise identification with a five-coefficient fit
165
168
  (IEEE 1139).
@@ -138,9 +138,12 @@ The Python package mirrors the full breadth of the engine.
138
138
  decode and SSR store helpers, and NTRIP client stream handling.
139
139
  - **Ephemeris and time:** broadcast ephemeris and precise SP3 products, JPL SPK
140
140
  (DAF/.bsp) kernels, uniform satellite-state sampling across broadcast and
141
- precise sources with batched multi-satellite interpolation, scale-aware time
142
- (UTC/TAI/TT/TDB/UT1 and the GNSS system times) with leap-second handling,
143
- and Earth orientation parameters (EOP).
141
+ precise sources with batched multi-satellite interpolation, configurable
142
+ SP3 coverage-gap interpolation policy (`gap_threshold_factor`, default 1.5)
143
+ and window-scoped continuity verification options (`check_continuity` and
144
+ `continuity_verdict`), scale-aware time (UTC/TAI/TT/TDB/UT1 and the GNSS
145
+ system times) with leap-second handling, and Earth orientation parameters
146
+ (EOP).
144
147
  - **Timing and clocks:** Allan-family stability analysis (ADEV/MDEV/HDEV/TDEV)
145
148
  and power-law clock-noise identification with a five-coefficient fit
146
149
  (IEEE 1139).
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "sidereon"
7
- version = "2.0.0"
7
+ version = "2.1.0"
8
8
  description = "Python (PyO3) bindings over the sidereon engine: SP3 loading and SPP/RTK/PPP positioning solves"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -110,6 +110,24 @@ class Sp3:
110
110
  @property
111
111
  def satellites(self) -> list[str]: ...
112
112
  @property
113
+ def gap_threshold_factor(self) -> float: ...
114
+ def with_interpolation_options(self, gap_threshold_factor: float) -> Sp3: ...
115
+ def check_continuity(
116
+ self,
117
+ orbit_class: str | None = ...,
118
+ residual_tolerance_m: float | None = ...,
119
+ gap_threshold_factor: float | None = ...,
120
+ ) -> dict[str, Any]: ...
121
+ def stencil_extent(self) -> tuple[float, float]: ...
122
+ def continuity_verdict(
123
+ self,
124
+ from_j2000_s: float,
125
+ through_j2000_s: float,
126
+ orbit_class: str | None = ...,
127
+ residual_tolerance_m: float | None = ...,
128
+ gap_threshold_factor: float | None = ...,
129
+ ) -> dict[str, Any]: ...
130
+ @property
113
131
  def epochs_j2000_seconds(self) -> npt.NDArray[np.float64]: ...
114
132
  def interpolate(
115
133
  self, satellite: str, j2000_seconds: npt.NDArray[np.float64]
@@ -118,7 +136,9 @@ class Sp3:
118
136
  def prediction_summary(self) -> Sp3PredictionSummary: ...
119
137
  def to_sp3_string(self) -> str: ...
120
138
  def precise_ephemeris_samples(self) -> list[PreciseEphemerisSample]: ...
121
- def precise_interpolant_artifact_bytes(self) -> bytes: ...
139
+ def precise_interpolant_artifact_bytes(
140
+ self, gap_threshold_factor: float | None = ...
141
+ ) -> bytes: ...
122
142
  def __repr__(self) -> str: ...
123
143
 
124
144
  class ExactSp3ValidationError(ValueError): ...
@@ -248,6 +268,12 @@ class PreciseEphemerisSamples:
248
268
  @staticmethod
249
269
  def from_samples(
250
270
  samples: Sequence[PreciseEphemerisSample],
271
+ gap_threshold_factor: float | None = ...,
272
+ ) -> PreciseEphemerisSamples: ...
273
+ @property
274
+ def gap_threshold_factor(self) -> float: ...
275
+ def with_interpolation_options(
276
+ self, gap_threshold_factor: float
251
277
  ) -> PreciseEphemerisSamples: ...
252
278
  @property
253
279
  def time_scale(self) -> TimeScale: ...
@@ -523,14 +549,24 @@ class PreciseEphemerisInterpolant:
523
549
  """Reusable precise-ephemeris interpolant with cached per-satellite nodes."""
524
550
 
525
551
  @staticmethod
526
- def from_sp3(source: Sp3) -> PreciseEphemerisInterpolant: ...
552
+ def from_sp3(
553
+ source: Sp3,
554
+ gap_threshold_factor: float | None = ...,
555
+ ) -> PreciseEphemerisInterpolant: ...
527
556
  @staticmethod
528
557
  def from_samples(
529
558
  samples: Sequence[PreciseEphemerisSample],
559
+ gap_threshold_factor: float | None = ...,
530
560
  ) -> PreciseEphemerisInterpolant: ...
531
561
  @staticmethod
532
562
  def from_precise_ephemeris_samples(
533
563
  source: PreciseEphemerisSamples,
564
+ gap_threshold_factor: float | None = ...,
565
+ ) -> PreciseEphemerisInterpolant: ...
566
+ @property
567
+ def gap_threshold_factor(self) -> float: ...
568
+ def with_interpolation_options(
569
+ self, gap_threshold_factor: float
534
570
  ) -> PreciseEphemerisInterpolant: ...
535
571
  @property
536
572
  def time_scale(self) -> TimeScale: ...
@@ -568,6 +604,8 @@ class PreciseInterpolantArtifact:
568
604
  def digest_provenance(self) -> Literal["verified", "attested"]: ...
569
605
  def verify(self) -> None: ...
570
606
  @property
607
+ def gap_threshold_factor(self) -> float: ...
608
+ @property
571
609
  def time_scale(self) -> TimeScale: ...
572
610
  @property
573
611
  def satellites(self) -> list[str]: ...
@@ -4955,13 +4993,19 @@ def leap_second_table_info() -> LeapSecondTable: ...
4955
4993
  def ut1_coverage_info() -> Ut1Coverage: ...
4956
4994
  def timescale_offset(from_: TimeScale, to: TimeScale) -> float: ...
4957
4995
  def timescale_offset_at(from_: TimeScale, to: TimeScale, utc_jd: float) -> float: ...
4958
- def load_sp3(source: bytes | bytearray | str | os.PathLike[str]) -> Sp3: ...
4996
+ def load_sp3(
4997
+ source: bytes | bytearray | str | os.PathLike[str],
4998
+ gap_threshold_factor: float | None = ...,
4999
+ ) -> Sp3: ...
4959
5000
  def parse_exact_sp3(
4960
5001
  source: bytes | bytearray | str | os.PathLike[str],
4961
5002
  request: ExactSp3Request,
4962
5003
  ) -> tuple[Sp3, ExactSp3Coverage]: ...
4963
5004
  def validate_exact_sp3(sp3: Sp3, request: ExactSp3Request) -> ExactSp3Coverage: ...
4964
- def build_precise_interpolant_artifact_bytes(sp3: Sp3) -> bytes: ...
5005
+ def build_precise_interpolant_artifact_bytes(
5006
+ sp3: Sp3,
5007
+ gap_threshold_factor: float | None = ...,
5008
+ ) -> bytes: ...
4965
5009
  def observable_states_at_j2000_s(
4966
5010
  source: (
4967
5011
  Sp3 | PreciseEphemerisSamples | PreciseEphemerisInterpolant | BroadcastEphemeris
@@ -34,7 +34,8 @@ use sidereon_core::ephemeris::{
34
34
  };
35
35
  use sidereon_core::ephemeris::{
36
36
  check_continuity, ContinuityDefect, ContinuityOptions, EpochWindow, MergeContinuityViolation,
37
- OrbitClass, SpeedBound, StencilExtent, WindowContinuityDecision, WindowContinuityVerdict,
37
+ OrbitClass, Sp3InterpolationOptions, SpeedBound, StencilExtent, WindowContinuityDecision,
38
+ WindowContinuityVerdict,
38
39
  };
39
40
  use sidereon_core::DigestProvenance;
40
41
  use sidereon_core::Error as CoreError;
@@ -137,6 +138,7 @@ fn parse_sat(token: &str) -> PyResult<GnssSatelliteId> {
137
138
  fn continuity_options(
138
139
  orbit_class: Option<&str>,
139
140
  residual_tolerance_m: Option<f64>,
141
+ gap_threshold_factor: Option<f64>,
140
142
  ) -> PyResult<ContinuityOptions> {
141
143
  let speed_bound = match orbit_class {
142
144
  None => None,
@@ -152,6 +154,11 @@ fn continuity_options(
152
154
  let mut options = ContinuityOptions::new(speed_bound, residual_tolerance_m);
153
155
  options.speed_bound = speed_bound;
154
156
  options.residual_tolerance_m = residual_tolerance_m;
157
+ if let Some(factor) = gap_threshold_factor {
158
+ let interpolation = Sp3InterpolationOptions::new(factor)
159
+ .map_err(|error| PyValueError::new_err(error.to_string()))?;
160
+ options = options.with_interpolation_options(interpolation);
161
+ }
155
162
  Ok(options)
156
163
  }
157
164
 
@@ -307,6 +314,21 @@ impl PySp3 {
307
314
  .collect()
308
315
  }
309
316
 
317
+ /// SP3 interpolation gap threshold factor carried by this product.
318
+ #[getter]
319
+ fn gap_threshold_factor(&self) -> f64 {
320
+ self.inner.interpolation_options().gap_threshold_factor()
321
+ }
322
+
323
+ /// Return a copy of this product with an explicit gap threshold factor.
324
+ fn with_interpolation_options(&self, gap_threshold_factor: f64) -> PyResult<Self> {
325
+ let options = Sp3InterpolationOptions::new(gap_threshold_factor)
326
+ .map_err(|error| PyValueError::new_err(error.to_string()))?;
327
+ Ok(Self {
328
+ inner: self.inner.clone().with_interpolation_options(options),
329
+ })
330
+ }
331
+
310
332
  /// The product's parsed epochs as seconds since J2000, in the file's own time
311
333
  /// scale, ascending, as a numpy `(n,)` `float64` array.
312
334
  ///
@@ -330,21 +352,28 @@ impl PySp3 {
330
352
  /// `orbit_class` is one of `"meo_gnss"` (default), `"geosynchronous"`,
331
353
  /// `"leo"`, or `None` to disable the speed gate.
332
354
  /// `residual_tolerance_m` enables the residual check; `None` disables it.
355
+ /// `gap_threshold_factor` configures the hold-out interpolation policy;
356
+ /// `None` leaves the core default (1.5).
333
357
  ///
334
358
  /// Returns a dict with `defects`, `attested`, and the counts of what was
335
359
  /// examined, so "checked and clean" stays distinguishable from "not
336
360
  /// checked". This reports rather than refuses: whether a product with
337
361
  /// defects is acceptable is the caller's decision.
338
- #[pyo3(signature = (orbit_class = "meo_gnss", residual_tolerance_m = Some(1.0)))]
362
+ #[pyo3(signature = (
363
+ orbit_class = "meo_gnss",
364
+ residual_tolerance_m = Some(1.0),
365
+ gap_threshold_factor = None,
366
+ ))]
339
367
  fn check_continuity(
340
368
  &self,
341
369
  py: Python<'_>,
342
370
  orbit_class: Option<&str>,
343
371
  residual_tolerance_m: Option<f64>,
372
+ gap_threshold_factor: Option<f64>,
344
373
  ) -> PyResult<PyObject> {
345
374
  let report = check_continuity(
346
375
  &self.inner.precise_ephemeris_samples(),
347
- &continuity_options(orbit_class, residual_tolerance_m)?,
376
+ &continuity_options(orbit_class, residual_tolerance_m, gap_threshold_factor)?,
348
377
  );
349
378
 
350
379
  let defects = continuity_defects_to_list(py, &report.defects)?;
@@ -377,6 +406,7 @@ impl PySp3 {
377
406
  through_j2000_s,
378
407
  orbit_class = "meo_gnss",
379
408
  residual_tolerance_m = Some(1.0),
409
+ gap_threshold_factor = None,
380
410
  ))]
381
411
  fn continuity_verdict(
382
412
  &self,
@@ -385,6 +415,7 @@ impl PySp3 {
385
415
  through_j2000_s: f64,
386
416
  orbit_class: Option<&str>,
387
417
  residual_tolerance_m: Option<f64>,
418
+ gap_threshold_factor: Option<f64>,
388
419
  ) -> PyResult<PyObject> {
389
420
  let window = EpochWindow::new(from_j2000_s, through_j2000_s)
390
421
  .map_err(|error| PyValueError::new_err(error.to_string()))?;
@@ -392,7 +423,7 @@ impl PySp3 {
392
423
  .map_err(|error| PyValueError::new_err(error.to_string()))?;
393
424
  let report = check_continuity(
394
425
  &self.inner.precise_ephemeris_samples(),
395
- &continuity_options(orbit_class, residual_tolerance_m)?,
426
+ &continuity_options(orbit_class, residual_tolerance_m, gap_threshold_factor)?,
396
427
  );
397
428
  continuity_verdict_to_py(py, report.verdict_for_window(window, stencil))
398
429
  }
@@ -528,12 +559,19 @@ impl PySp3 {
528
559
  }
529
560
 
530
561
  /// Build deterministic memory-mappable precise-interpolant artifact bytes.
562
+ #[pyo3(signature = (gap_threshold_factor = None))]
531
563
  fn precise_interpolant_artifact_bytes<'py>(
532
564
  &self,
533
565
  py: Python<'py>,
566
+ gap_threshold_factor: Option<f64>,
534
567
  ) -> PyResult<Bound<'py, PyBytes>> {
535
- let bytes = self
536
- .inner
568
+ let mut product = self.inner.clone();
569
+ if let Some(factor) = gap_threshold_factor {
570
+ let options = Sp3InterpolationOptions::new(factor)
571
+ .map_err(|e| PyValueError::new_err(e.to_string()))?;
572
+ product = product.with_interpolation_options(options);
573
+ }
574
+ let bytes = product
537
575
  .precise_interpolant_store_bytes()
538
576
  .map_err(precise_artifact_error_without_bytes)?;
539
577
  Ok(PyBytes::new(py, &bytes))
@@ -1098,13 +1136,38 @@ impl PyPreciseEphemerisSamples {
1098
1136
  /// scales, a sample is non-finite, or an epoch is not representable as J2000
1099
1137
  /// seconds.
1100
1138
  #[staticmethod]
1101
- fn from_samples(py: Python<'_>, samples: Vec<Py<PyPreciseEphemerisSample>>) -> PyResult<Self> {
1139
+ #[pyo3(signature = (samples, gap_threshold_factor = None))]
1140
+ fn from_samples(
1141
+ py: Python<'_>,
1142
+ samples: Vec<Py<PyPreciseEphemerisSample>>,
1143
+ gap_threshold_factor: Option<f64>,
1144
+ ) -> PyResult<Self> {
1102
1145
  let samples = samples.iter().map(|s| s.borrow(py).to_core());
1103
- let inner = PreciseEphemerisSamples::from_samples(samples)
1146
+ let mut inner = PreciseEphemerisSamples::from_samples(samples)
1104
1147
  .map_err(|e| PyValueError::new_err(e.to_string()))?;
1148
+ if let Some(factor) = gap_threshold_factor {
1149
+ let options = Sp3InterpolationOptions::new(factor)
1150
+ .map_err(|e| PyValueError::new_err(e.to_string()))?;
1151
+ inner = inner.with_interpolation_options(options);
1152
+ }
1105
1153
  Ok(Self { inner })
1106
1154
  }
1107
1155
 
1156
+ /// SP3 interpolation gap threshold factor carried by this source.
1157
+ #[getter]
1158
+ fn gap_threshold_factor(&self) -> f64 {
1159
+ self.inner.interpolation_options().gap_threshold_factor()
1160
+ }
1161
+
1162
+ /// Return a copy of these samples with an explicit gap threshold factor.
1163
+ fn with_interpolation_options(&self, gap_threshold_factor: f64) -> PyResult<Self> {
1164
+ let options = Sp3InterpolationOptions::new(gap_threshold_factor)
1165
+ .map_err(|e| PyValueError::new_err(e.to_string()))?;
1166
+ Ok(Self {
1167
+ inner: self.inner.clone().with_interpolation_options(options),
1168
+ })
1169
+ }
1170
+
1108
1171
  /// Time scale every sample epoch is expressed in.
1109
1172
  #[getter]
1110
1173
  fn time_scale(&self) -> PyTimeScale {
@@ -1141,27 +1204,65 @@ pub struct PyPreciseEphemerisInterpolant {
1141
1204
  impl PyPreciseEphemerisInterpolant {
1142
1205
  /// Build a cached interpolant from a parsed SP3 product.
1143
1206
  #[staticmethod]
1144
- fn from_sp3(source: &PySp3) -> Self {
1145
- Self {
1146
- inner: PreciseEphemerisInterpolant::from_sp3(&source.inner),
1207
+ #[pyo3(signature = (source, gap_threshold_factor = None))]
1208
+ fn from_sp3(source: &PySp3, gap_threshold_factor: Option<f64>) -> PyResult<Self> {
1209
+ let mut inner = PreciseEphemerisInterpolant::from_sp3(&source.inner);
1210
+ if let Some(factor) = gap_threshold_factor {
1211
+ let options = Sp3InterpolationOptions::new(factor)
1212
+ .map_err(|e| PyValueError::new_err(e.to_string()))?;
1213
+ inner = inner.with_interpolation_options(options);
1147
1214
  }
1215
+ Ok(Self { inner })
1148
1216
  }
1149
1217
 
1150
1218
  /// Build a cached interpolant directly from precise-ephemeris samples.
1151
1219
  #[staticmethod]
1152
- fn from_samples(py: Python<'_>, samples: Vec<Py<PyPreciseEphemerisSample>>) -> PyResult<Self> {
1220
+ #[pyo3(signature = (samples, gap_threshold_factor = None))]
1221
+ fn from_samples(
1222
+ py: Python<'_>,
1223
+ samples: Vec<Py<PyPreciseEphemerisSample>>,
1224
+ gap_threshold_factor: Option<f64>,
1225
+ ) -> PyResult<Self> {
1153
1226
  let samples = samples.iter().map(|s| s.borrow(py).to_core());
1154
- let inner = PreciseEphemerisInterpolant::from_samples(samples)
1227
+ let mut inner = PreciseEphemerisInterpolant::from_samples(samples)
1155
1228
  .map_err(|e| PyValueError::new_err(e.to_string()))?;
1229
+ if let Some(factor) = gap_threshold_factor {
1230
+ let options = Sp3InterpolationOptions::new(factor)
1231
+ .map_err(|e| PyValueError::new_err(e.to_string()))?;
1232
+ inner = inner.with_interpolation_options(options);
1233
+ }
1156
1234
  Ok(Self { inner })
1157
1235
  }
1158
1236
 
1159
1237
  /// Build a cached interpolant from an existing sample-backed source.
1160
1238
  #[staticmethod]
1161
- fn from_precise_ephemeris_samples(source: &PyPreciseEphemerisSamples) -> Self {
1162
- Self {
1163
- inner: PreciseEphemerisInterpolant::from_precise_ephemeris_samples(&source.inner),
1239
+ #[pyo3(signature = (source, gap_threshold_factor = None))]
1240
+ fn from_precise_ephemeris_samples(
1241
+ source: &PyPreciseEphemerisSamples,
1242
+ gap_threshold_factor: Option<f64>,
1243
+ ) -> PyResult<Self> {
1244
+ let mut inner = PreciseEphemerisInterpolant::from_precise_ephemeris_samples(&source.inner);
1245
+ if let Some(factor) = gap_threshold_factor {
1246
+ let options = Sp3InterpolationOptions::new(factor)
1247
+ .map_err(|e| PyValueError::new_err(e.to_string()))?;
1248
+ inner = inner.with_interpolation_options(options);
1164
1249
  }
1250
+ Ok(Self { inner })
1251
+ }
1252
+
1253
+ /// SP3 interpolation gap threshold factor carried by this interpolant.
1254
+ #[getter]
1255
+ fn gap_threshold_factor(&self) -> f64 {
1256
+ self.inner.interpolation_options().gap_threshold_factor()
1257
+ }
1258
+
1259
+ /// Return a copy of this interpolant with an explicit gap threshold factor.
1260
+ fn with_interpolation_options(&self, gap_threshold_factor: f64) -> PyResult<Self> {
1261
+ let options = Sp3InterpolationOptions::new(gap_threshold_factor)
1262
+ .map_err(|e| PyValueError::new_err(e.to_string()))?;
1263
+ Ok(Self {
1264
+ inner: self.inner.clone().with_interpolation_options(options),
1265
+ })
1165
1266
  }
1166
1267
 
1167
1268
  /// Time scale of the source epochs used to build this handle.
@@ -1324,6 +1425,12 @@ impl PyPreciseInterpolantArtifact {
1324
1425
  self.inner.time_scale().into()
1325
1426
  }
1326
1427
 
1428
+ /// SP3 interpolation gap threshold factor read from the artifact header.
1429
+ #[getter]
1430
+ fn gap_threshold_factor(&self) -> f64 {
1431
+ self.inner.interpolation_options().gap_threshold_factor()
1432
+ }
1433
+
1327
1434
  /// Satellite tokens present in the artifact.
1328
1435
  #[getter]
1329
1436
  fn satellites(&self) -> Vec<String> {
@@ -1465,28 +1572,35 @@ fn precise_artifact_error(err: PreciseInterpolantStoreError, truncated: bool) ->
1465
1572
  /// directly; or
1466
1573
  /// - a path (`str` or `os.PathLike`): the file is read and parsed.
1467
1574
  ///
1575
+ /// `gap_threshold_factor` optionally configures the product-carried SP3
1576
+ /// coverage-gap interpolation policy; `None` leaves the core default (1.5).
1577
+ ///
1468
1578
  /// Raises [`Sp3ParseError`](crate::Sp3ParseError) on malformed content, `OSError`
1469
- /// if the path cannot be read, and `TypeError` if `source` is neither bytes nor a
1470
- /// path.
1579
+ /// if the path cannot be read, `ValueError` if `gap_threshold_factor` is not
1580
+ /// finite or <= 1.0, and `TypeError` if `source` is neither bytes nor a path.
1471
1581
  #[pyfunction]
1472
- fn load_sp3(source: &Bound<'_, PyAny>) -> PyResult<PySp3> {
1582
+ #[pyo3(signature = (source, gap_threshold_factor = None))]
1583
+ fn load_sp3(source: &Bound<'_, PyAny>, gap_threshold_factor: Option<f64>) -> PyResult<PySp3> {
1473
1584
  // bytes-like first, so a `bytes` argument keeps the prior "content" meaning.
1474
- if let Ok(bytes) = source.downcast::<PyBytes>() {
1475
- let inner = sidereon::load_sp3(bytes.as_bytes()).map_err(to_sp3_err)?;
1476
- return Ok(PySp3 { inner });
1477
- }
1478
- if let Ok(buf) = source.downcast::<PyByteArray>() {
1585
+ let mut inner = if let Ok(bytes) = source.downcast::<PyBytes>() {
1586
+ sidereon::load_sp3(bytes.as_bytes()).map_err(to_sp3_err)?
1587
+ } else if let Ok(buf) = source.downcast::<PyByteArray>() {
1479
1588
  // SAFETY: the buffer is copied into the parser synchronously here; no
1480
1589
  // Python code runs in between to mutate or free it.
1481
- let inner = sidereon::load_sp3(unsafe { buf.as_bytes() }).map_err(to_sp3_err)?;
1482
- return Ok(PySp3 { inner });
1483
- }
1484
- // Otherwise treat it as a path (str / os.PathLike via PyO3's fspath support).
1485
- let path: PathBuf = source.extract().map_err(|_| {
1486
- PyValueError::new_err("load_sp3 expects bytes, bytearray, or a path (str/os.PathLike)")
1487
- })?;
1488
- let data = std::fs::read(&path)?;
1489
- let inner = sidereon::load_sp3(&data).map_err(to_sp3_err)?;
1590
+ sidereon::load_sp3(unsafe { buf.as_bytes() }).map_err(to_sp3_err)?
1591
+ } else {
1592
+ // Otherwise treat it as a path (str / os.PathLike via PyO3's fspath support).
1593
+ let path: PathBuf = source.extract().map_err(|_| {
1594
+ PyValueError::new_err("load_sp3 expects bytes, bytearray, or a path (str/os.PathLike)")
1595
+ })?;
1596
+ let data = std::fs::read(&path)?;
1597
+ sidereon::load_sp3(&data).map_err(to_sp3_err)?
1598
+ };
1599
+ if let Some(factor) = gap_threshold_factor {
1600
+ let options = Sp3InterpolationOptions::new(factor)
1601
+ .map_err(|e| PyValueError::new_err(e.to_string()))?;
1602
+ inner = inner.with_interpolation_options(options);
1603
+ }
1490
1604
  Ok(PySp3 { inner })
1491
1605
  }
1492
1606
 
@@ -1645,11 +1759,13 @@ fn _validate_exact_sp3(
1645
1759
 
1646
1760
  /// Build deterministic precise-interpolant artifact bytes from an SP3 product.
1647
1761
  #[pyfunction]
1762
+ #[pyo3(signature = (sp3, gap_threshold_factor = None))]
1648
1763
  fn build_precise_interpolant_artifact_bytes<'py>(
1649
1764
  py: Python<'py>,
1650
1765
  sp3: &PySp3,
1766
+ gap_threshold_factor: Option<f64>,
1651
1767
  ) -> PyResult<Bound<'py, PyBytes>> {
1652
- sp3.precise_interpolant_artifact_bytes(py)
1768
+ sp3.precise_interpolant_artifact_bytes(py, gap_threshold_factor)
1653
1769
  }
1654
1770
 
1655
1771
  pub(crate) fn parse_satellites(tokens: &[String]) -> PyResult<Vec<GnssSatelliteId>> {