sidereon 0.8.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 (107) hide show
  1. sidereon-0.8.0/.gitignore +7 -0
  2. sidereon-0.8.0/COVERAGE.md +278 -0
  3. sidereon-0.8.0/Cargo.lock +576 -0
  4. sidereon-0.8.0/Cargo.toml +31 -0
  5. sidereon-0.8.0/PKG-INFO +95 -0
  6. sidereon-0.8.0/README.md +77 -0
  7. sidereon-0.8.0/bench/README.md +127 -0
  8. sidereon-0.8.0/bench/bench_fleet.py +300 -0
  9. sidereon-0.8.0/pyproject.toml +33 -0
  10. sidereon-0.8.0/python/sidereon/__init__.py +667 -0
  11. sidereon-0.8.0/python/sidereon/__init__.pyi +3386 -0
  12. sidereon-0.8.0/python/sidereon/data.py +1288 -0
  13. sidereon-0.8.0/python/sidereon/data.pyi +177 -0
  14. sidereon-0.8.0/python/sidereon/py.typed +0 -0
  15. sidereon-0.8.0/src/bodies.rs +113 -0
  16. sidereon-0.8.0/src/broadcast_comparison.rs +231 -0
  17. sidereon-0.8.0/src/cdm.rs +307 -0
  18. sidereon-0.8.0/src/conjunction.rs +408 -0
  19. sidereon-0.8.0/src/constellation.rs +785 -0
  20. sidereon-0.8.0/src/dgnss.rs +197 -0
  21. sidereon-0.8.0/src/ephemeris.rs +337 -0
  22. sidereon-0.8.0/src/events.rs +949 -0
  23. sidereon-0.8.0/src/fallback.rs +267 -0
  24. sidereon-0.8.0/src/forces.rs +65 -0
  25. sidereon-0.8.0/src/frames.rs +812 -0
  26. sidereon-0.8.0/src/ionex.rs +233 -0
  27. sidereon-0.8.0/src/lib.rs +246 -0
  28. sidereon-0.8.0/src/marshal.rs +318 -0
  29. sidereon-0.8.0/src/observables.rs +2143 -0
  30. sidereon-0.8.0/src/omm.rs +423 -0
  31. sidereon-0.8.0/src/ppp.rs +939 -0
  32. sidereon-0.8.0/src/ppp_corrections.rs +351 -0
  33. sidereon-0.8.0/src/products.rs +787 -0
  34. sidereon-0.8.0/src/propagation.rs +1247 -0
  35. sidereon-0.8.0/src/qc.rs +270 -0
  36. sidereon-0.8.0/src/rf.rs +198 -0
  37. sidereon-0.8.0/src/rinex.rs +2008 -0
  38. sidereon-0.8.0/src/rtk.rs +957 -0
  39. sidereon-0.8.0/src/spk.rs +313 -0
  40. sidereon-0.8.0/src/spp.rs +552 -0
  41. sidereon-0.8.0/src/staleness.rs +448 -0
  42. sidereon-0.8.0/src/tropo.rs +110 -0
  43. sidereon-0.8.0/tests/_helpers.py +88 -0
  44. sidereon-0.8.0/tests/fixtures/README.md +57 -0
  45. sidereon-0.8.0/tests/fixtures/antex/igs20_pasa_scoa_gps.atx +6256 -0
  46. sidereon-0.8.0/tests/fixtures/antex/igs20_wettzell_trim.atx +2653 -0
  47. sidereon-0.8.0/tests/fixtures/batch_fleet.json +113 -0
  48. sidereon-0.8.0/tests/fixtures/cdm.json +133 -0
  49. sidereon-0.8.0/tests/fixtures/clk/synthetic_rinex_clock.clk +9 -0
  50. sidereon-0.8.0/tests/fixtures/conjunction.json +192 -0
  51. sidereon-0.8.0/tests/fixtures/constellation/gps_ops_sample.json +78 -0
  52. sidereon-0.8.0/tests/fixtures/constellation/navcen_gps_sample.html +62 -0
  53. sidereon-0.8.0/tests/fixtures/events_bodies_dop.json +157 -0
  54. sidereon-0.8.0/tests/fixtures/frames_time.json +480 -0
  55. sidereon-0.8.0/tests/fixtures/nav/BRDC00GOP_R_20210010000_01D_MN.rnx +50 -0
  56. sidereon-0.8.0/tests/fixtures/nav/ESBC00DNK_R_20201770000_01D_MN.rnx +17935 -0
  57. sidereon-0.8.0/tests/fixtures/nav/KMS300DNK_R_20221591000_01H_MN.rnx +2533 -0
  58. sidereon-0.8.0/tests/fixtures/numerical_propagation.json +166 -0
  59. sidereon-0.8.0/tests/fixtures/obs/ESBC00DNK_R_20201770000_01D_30S_MO_trim.crx +147 -0
  60. sidereon-0.8.0/tests/fixtures/obs/ESBC00DNK_R_20201770000_01D_30S_MO_trim.rnx +143 -0
  61. sidereon-0.8.0/tests/fixtures/obs/algo0010_2015001_v1_trim.crx +69 -0
  62. sidereon-0.8.0/tests/fixtures/obs/algo0010_2015001_v1_trim.rnx +106 -0
  63. sidereon-0.8.0/tests/fixtures/omm.json +347 -0
  64. sidereon-0.8.0/tests/fixtures/pass_finder.json +65 -0
  65. sidereon-0.8.0/tests/fixtures/ppp_esbc.json +12364 -0
  66. sidereon-0.8.0/tests/fixtures/rf_link_budget.json +40 -0
  67. sidereon-0.8.0/tests/fixtures/rtk_wtzr.json +27499 -0
  68. sidereon-0.8.0/tests/fixtures/sgp4_topocentric.json +305 -0
  69. sidereon-0.8.0/tests/fixtures/sp3_bodies.json +255 -0
  70. sidereon-0.8.0/tests/fixtures/spk/horizons_eros_type21.bsp +0 -0
  71. sidereon-0.8.0/tests/fixtures/tle_roundtrip.json +40 -0
  72. sidereon-0.8.0/tests/test_antex.py +80 -0
  73. sidereon-0.8.0/tests/test_batch.py +204 -0
  74. sidereon-0.8.0/tests/test_broadcast_comparison.py +81 -0
  75. sidereon-0.8.0/tests/test_broadcast_fallback.py +190 -0
  76. sidereon-0.8.0/tests/test_cdm.py +161 -0
  77. sidereon-0.8.0/tests/test_conjunction.py +188 -0
  78. sidereon-0.8.0/tests/test_constellation.py +168 -0
  79. sidereon-0.8.0/tests/test_crinex.py +55 -0
  80. sidereon-0.8.0/tests/test_data.py +314 -0
  81. sidereon-0.8.0/tests/test_dgnss.py +145 -0
  82. sidereon-0.8.0/tests/test_events_bodies_dop.py +181 -0
  83. sidereon-0.8.0/tests/test_forces.py +63 -0
  84. sidereon-0.8.0/tests/test_frames_time.py +239 -0
  85. sidereon-0.8.0/tests/test_import.py +58 -0
  86. sidereon-0.8.0/tests/test_ionex.py +157 -0
  87. sidereon-0.8.0/tests/test_numerical.py +173 -0
  88. sidereon-0.8.0/tests/test_observables.py +758 -0
  89. sidereon-0.8.0/tests/test_omm.py +179 -0
  90. sidereon-0.8.0/tests/test_passes.py +170 -0
  91. sidereon-0.8.0/tests/test_ppp.py +184 -0
  92. sidereon-0.8.0/tests/test_ppp_corrections.py +123 -0
  93. sidereon-0.8.0/tests/test_propagation.py +142 -0
  94. sidereon-0.8.0/tests/test_qc.py +111 -0
  95. sidereon-0.8.0/tests/test_rf.py +96 -0
  96. sidereon-0.8.0/tests/test_rinex_clock.py +91 -0
  97. sidereon-0.8.0/tests/test_rinex_nav.py +234 -0
  98. sidereon-0.8.0/tests/test_rinex_obs.py +146 -0
  99. sidereon-0.8.0/tests/test_rtk.py +159 -0
  100. sidereon-0.8.0/tests/test_sp3_bodies.py +246 -0
  101. sidereon-0.8.0/tests/test_sp3_merge.py +86 -0
  102. sidereon-0.8.0/tests/test_spk.py +129 -0
  103. sidereon-0.8.0/tests/test_spp.py +117 -0
  104. sidereon-0.8.0/tests/test_spp_glonass.py +230 -0
  105. sidereon-0.8.0/tests/test_staleness.py +174 -0
  106. sidereon-0.8.0/tests/test_tle_roundtrip.py +107 -0
  107. sidereon-0.8.0/tests/test_tropo.py +148 -0
@@ -0,0 +1,7 @@
1
+ /target
2
+ /dist
3
+ /.venv
4
+ __pycache__/
5
+ *.py[cod]
6
+ .pytest_cache/
7
+ *.so
@@ -0,0 +1,278 @@
1
+ # Python binding coverage map (`sidereon-core` -> `sidereon` Python package)
2
+
3
+ Tracks how much of the `sidereon-core` public API is reachable from the Python
4
+ binding (`bindings/python`, PyO3), and whether each exposed capability meets the
5
+ idiomatic bar in `PYTHON_IMPROVE_BRIEF.md` (real exception hierarchy, numpy
6
+ in/out, typed config objects with `.pyi`, `@property`/`__repr__`/`__eq__`,
7
+ `enum.Enum` status fields, no `dict[str, Any]` blobs).
8
+
9
+ The binding is a thin INTERFACE: marshal + idiom only, ZERO modeling logic. Every
10
+ exposed item must reproduce the engine numbers exactly and is cross-checked in
11
+ pytest against an env-gated fixture dumped from the validated Rust harness.
12
+
13
+ ## Status legend
14
+
15
+ - `exposed-idiomatic` - reachable from Python and meets the full bar above.
16
+ - `not-idiomatic` - reachable but violates the bar (dict blobs, no typed inputs,
17
+ collapses every failure into one `SidereonError`, no enums, etc.).
18
+ - `not-exposed` - present in the Rust public API, unreachable from Python.
19
+ - `deferred` - intentionally not exposed; reason stated.
20
+
21
+ Source of truth for the API surface: `grep '^pub'` over
22
+ `crates/sidereon-core/src/**` on branch `sidereon/rename`, verified
23
+ 2026-06-17. Current Python surface: `python/sidereon/__init__.py(.pyi)` plus
24
+ `src/{lib,spp,rtk,ppp,propagation,frames,bodies,ephemeris}.rs`.
25
+
26
+ ---
27
+
28
+ ## Cross-cutting (applies to every area)
29
+
30
+ | Capability | Status | Notes |
31
+ | -------------------------------------------------------------------------------------------------------- | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
32
+ | Exception hierarchy (`SidereonError` base + `Sp3ParseError`, `TleParseError`, `SolveError`, convergence) | exposed-idiomatic | Tree built: `SidereonError` base; `ParseError` -> {`Sp3ParseError`, `TleParseError`}; `SolveError`. `load_sp3` raises `Sp3ParseError`, `Tle(..)` raises `TleParseError`, SGP4/numerical propagation raise `SolveError`, and GNSS solver failures raise `SolveError`. Bad input still raises `TypeError`/`ValueError`. |
33
+ | numpy in/out for batched calls | partial | Propagation/look-angles/frame transforms, Sun/Moon, and SP3 interpolation return numpy `(n,3)`/`(n,)`. SPP/RTK/PPP return scalar/list/dict, take Python sequences. |
34
+ | Typed `#[pyclass]` config objects (no `dict[str, Any]`) | partial | Area 9 GNSS solvers now use typed pyclasses (`SppConfig`, `Rtk*Config`, `Ppp*Config`, typed epoch/observation/state records) instead of dict blobs. Remaining future areas must follow the same rule as they are exposed. |
35
+ | `enum.Enum` for status/discrete fields | partial | `TimeScale` exposed as a typed PyO3 enum (Area 3). RTK/PPP `integer_status` now returns `IntegerStatus`, not a raw string. Remaining future discrete fields must use enums as they are exposed. |
36
+ | `os.PathLike` file-load helpers | exposed-idiomatic | `load_sp3` accepts `bytes`/`bytearray` OR a path (`str`/`os.PathLike`); a missing path raises `OSError`, non-path/non-bytes raises `ValueError` (Area 4). |
37
+
38
+ ---
39
+
40
+ ## Area 1 - SGP4 + TLE (astro)
41
+
42
+ Core: `astro::sgp4` (`Satellite`, `from_tle*`, `propagate`/`propagate_jd`,
43
+ `propagate_elements*`, `ElementSet`, `OpsMode`, `GravConstType`, raw
44
+ `sgp4`/`sgp4init`), `astro::tle` (`parse`, `encode`, `TleElements`,
45
+ `ParsedTle`, `TleError`, `ChecksumWarning`).
46
+
47
+ | Capability | Status | Notes |
48
+ | --------------------------------------------------- | ----------------- | -------------------------------------------------------------------------------------------- |
49
+ | Parse TLE (two lines) -> elements | exposed-idiomatic | `Tle(line1, line2, opsmode=...)`; element fields as `@property`; `__repr__`. |
50
+ | SGP4 propagate over epoch grid (TEME) | exposed-idiomatic | `Tle.propagate(epochs_unix_us)` -> `TlePropagation` numpy `(n,3)` pos/vel km. |
51
+ | Topocentric look angles (az/el/range) | exposed-idiomatic | `Tle.look_angles(station, epochs)` -> `LookAngles` numpy. (Also covers Area 5 look-angle.) |
52
+ | Format/encode elements -> TLE lines (`tle::encode`) | exposed-idiomatic | `Tle.to_lines()` -> `(str, str)`; character-exact round-trip cross-checked vs `tle::encode`. |
53
+ | Checksum warnings (`ChecksumWarning`) surfaced | exposed-idiomatic | `Tle.checksum_warnings` -> `list[ChecksumWarning]` (typed, `@property`/`__repr__`/`__eq__`). |
54
+ | `TleParseError` vs generic error | exposed-idiomatic | `Tle(..)` raises `TleParseError` (a `ParseError`/`SidereonError`). |
55
+ | Raw `ElementSet` / `sgp4init` kernels | deferred | Low-level `pub` kernels; not the intended user API. Idiomatic surface is `Tle`. |
56
+
57
+ ## Area 2 - Numerical propagation (astro)
58
+
59
+ Core: `astro::forces` (`TwoBodyGravity`, `J2Gravity`, `CompositeForceModel`,
60
+ `ForceModel`), `astro::integrators` (`RK4`, `DP54`, `Integrator`,
61
+ `DynamicsModel`), `astro::propagator` (`PropagationContext`,
62
+ `PropagationResult`, `PropagationPoint`, `PropagationStats`,
63
+ `OrbitalDynamics`, `IntegratorOptions`, `DenseOutput`, `DenseSegment`,
64
+ `PIController`), `astro::state` (`CartesianState`, `StateDerivative`).
65
+
66
+ | Capability | Status | Notes |
67
+ | ------------------------------------------- | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
68
+ | Cartesian state value type | exposed-idiomatic | Crossed as numpy `position_km` / `velocity_km_s` (length 3) into `propagate_state` and back out via `Ephemeris`. |
69
+ | Force models (two-body, J2, composite) | exposed-idiomatic | `propagate_state(force_model="two_body" \| "two_body_j2", mu_km3_s2=...)`; backed by core `ForceModelKind`. |
70
+ | Integrators (RK4 fixed-step, DP54 adaptive) | exposed-idiomatic | `propagate_state(integrator="dp54" \| "rk4", abs_tol=, rel_tol=, initial_step_s=, min_step_s=, max_step_s=, max_steps=)`. |
71
+ | Propagate a state over a time grid | exposed-idiomatic | `propagate_state(epoch_s, position_km, velocity_km_s, times_s, ...)` -> `Ephemeris` numpy `times_s` `(n,)`, pos/vel `(n,3)`, `states` `(n,6)`. Backed by `StatePropagator`. |
72
+ | Dense output / interpolation | not-exposed | `DenseOutput.eval(t)`; the grid-sampling `Ephemeris` path covers the common need. |
73
+
74
+ ## Area 3 - Frames + time (astro)
75
+
76
+ Core: `astro::frames` (`gcrs_to_itrs_*`, `itrs_to_gcrs_*`, `teme_to_gcrs_*`,
77
+ `mean_of_date_to_itrs_matrix`, geodetic<->ITRS, precession/nutation/GAST
78
+ helpers, `mat3_vec3_mul`, `TemeStateKm`, `GeodeticStationKm`), `astro::time`
79
+ (`Instant`, `Time`, `TimeScale`, `TimeScales`, `Duration`, `JulianDateSplit`,
80
+ `GnssWeekTow`, leap seconds, UT1 coverage/provenance, `Validated<T>`).
81
+
82
+ | Capability | Status | Notes |
83
+ | ---------------------------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
84
+ | ITRS/GCRS/TEME transforms (matrices + apply) | exposed-idiomatic | Batched numpy `(n,3)`: `teme_to_gcrs` -> `FrameStates`, `gcrs_to_itrs`, `itrs_to_gcrs`; `skyfield_compat` flag. Per-row `TimeScales` from the parity path. Cross-checked bit-exact (both compat modes). |
85
+ | Geodetic <-> ECEF/ITRS | exposed-idiomatic | `geodetic_to_ecef` / `ecef_to_geodetic`, batched `(n,3)`. PROJ-parity variant `geodetic_from_ecef_proj` deferred (see below). |
86
+ | Precession/nutation/GAST/obliquity | exposed-idiomatic | `Instant.precession_matrix()`/`nutation_matrix()` (numpy `(3,3)`), `nutation_angles()`, `mean_obliquity_radians`, `gmst_radians()`/`gast_radians()`. GMST/GAST added as core public wrappers; cross-checked bit-exact. |
87
+ | Time scales (TT/TAI/UT1/UTC/GPS) + conversions | exposed-idiomatic | `Instant.from_utc`/`from_unix_micros`; `tt_jd`/`ut1_jd`/`tdb_jd` + fractions + `delta_t_seconds`; `TimeScale` enum. Cross-checked bit-exact. |
88
+ | Julian date / GNSS week-TOW / leap seconds | exposed-idiomatic | `JulianDate` (split), `Instant.{tt,ut1,tdb}_jd_split`; `GnssWeekTow` (`normalized`/`unrolled_week`); `leap_seconds(y,m,d)` + `leap_seconds_batch`, `leap_second_table_info()`, `ut1_coverage_info()`. Cross-checked bit-exact. |
89
+ | PROJ-parity ECEF->geodetic variant | deferred | `geodetic_from_ecef_proj` returns `[lon_deg, lat_deg, alt_m]` (pyproj convention, distinct column order/units) for pyproj 3.6.1 exactness. The primary `ecef_to_geodetic` (Skyfield WGS84, lat/lon/alt km) is exposed; revisit if pyproj-exact ECEF->LLA is needed. |
90
+
91
+ ## Area 4 - Ephemeris / bodies (astro + GNSS)
92
+
93
+ Core: `astro::bodies` (`sun_moon_eci`, `sun_moon_ecef`, `SunMoon`),
94
+ `ephemeris`/`sp3` (`Sp3::parse`, `epoch_count`, `satellites`, position/clock
95
+ interpolation, `sp3::write`, `sp3::combine`), broadcast (`rinex_nav`
96
+ `BroadcastStore`/`BroadcastRecord`).
97
+
98
+ | Capability | Status | Notes |
99
+ | ----------------------------------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
100
+ | Sun/Moon position (ECI, ECEF) | exposed-idiomatic | `sun_moon_eci(epochs_unix_us)` / `sun_moon_ecef(...)` -> `SunMoon` with `sun`/`moon` numpy `(n,3)` metres; batched (one FFI crossing). Backed by core `sun_moon_eci_at`/`sun_moon_ecef`; cross-checked bit-exact. |
101
+ | Parse SP3 product | exposed-idiomatic | `load_sp3(bytes)` -> `Sp3`; `epoch_count`, `satellites`, `__repr__`; raises `Sp3ParseError`. |
102
+ | SP3 position/clock query at epoch | exposed-idiomatic | `Sp3.epochs_j2000_seconds` (node axis), `Sp3.interpolate(sat, j2000_seconds)` -> `Sp3Interpolation` (`position_m` `(n,3)`, `clock_s` `(n,)` NaN-where-missing), `Sp3.state(sat, idx)` -> `Sp3State`. Backed by `position_at_j2000_seconds`/`state`; cross-checked bit-exact. |
103
+ | SP3 path-accepting loader (`os.PathLike`) | exposed-idiomatic | `load_sp3` accepts `bytes`/`bytearray` or a path (`str`/`os.PathLike`); missing path -> `OSError`. |
104
+ | SP3 write | exposed-idiomatic | `Sp3.to_sp3_string()` -> deterministic SP3 text; byte-exact vs `to_sp3_string` and round-trips via `load_sp3`. |
105
+ | SP3 combine (multi-product) | not-exposed | `sp3::merge` + `MergeOptions`/`MergeReport`; scheduled as the next Area 4 step (typed merge config + audit-trail result). |
106
+ | Broadcast ephemeris access | not-exposed | `ephemeris::BroadcastEphemeris` (`rinex_nav::BroadcastStore`); bound together with RINEX-nav parsing in Area 10. |
107
+
108
+ ## Area 5 - Geometry + events (astro)
109
+
110
+ Core: `astro::passes` (`predict_passes`, `visible_from_constellation`,
111
+ `PassPredictionOptions`, `PredictedPass`, `ConstellationMember`,
112
+ `VisibleSatellite`), `astro::events` (`status`, `shadow_fraction`,
113
+ `EclipseStatus`, `DetectedEvent`), `astro::angles` (`sun_angle`, `phase_angle`,
114
+ `sun_elevation`, `moon_angle`, `earth_angular_radius`), GNSS `geometry`
115
+ (`dop_series`, `visibility_series`, `passes`, DOP/visibility types).
116
+
117
+ | Capability | Status | Notes |
118
+ | ----------------------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
119
+ | Look angle az/el/range (single + arc) | exposed-idiomatic | Via `Tle.look_angles` (Area 1). |
120
+ | Pass finding (AOS/LOS/culmination + mask) | exposed-idiomatic | `Tle.find_passes(station, start_unix_us, end_unix_us, elevation_mask_deg=, step_seconds=, time_tolerance_s=)` -> `list[SatellitePass]`. Dense-sample finder; backed by `find_passes`. |
121
+ | Pass prediction (rise/set windows) | not-exposed | Legacy `predict_passes` / `PredictedPass` / `PassPredictionOptions`; the `find_passes` path above supersedes it for the binding. |
122
+ | Constellation visibility | not-exposed | `visible_from_constellation`, GNSS `visibility_series`. |
123
+ | Eclipse / shadow status + fraction | exposed-idiomatic | `shadow_fraction` -> numpy `(n,)`; `eclipse_status` -> `list[EclipseStatus]` enum over batched `(n,3)` satellite/Sun km vectors. Cross-checked bit-exact against Rust fixture. |
124
+ | Sun/phase/moon angles | exposed-idiomatic | `sun_angle`, `moon_angle`, `sun_elevation`, `phase_angle`, `earth_angular_radius` over batched `(n,3)` km vectors, returning numpy `(n,)` degrees. Cross-checked bit-exact. |
125
+ | GNSS DOP from line-of-sight geometry | exposed-idiomatic | `gnss_dop(line_of_sight, weights, Wgs84Geodetic)` -> typed `Dop` (`gdop/pdop/hdop/vdop/tdop`, `__repr__`, `__eq__`); bad geometry uses `ValueError`/`SolveError`. Cross-checked bit-exact. |
126
+ | GNSS DOP series / visibility planning | not-exposed | SP3-backed `geometry::dop_series`, `visibility_series`, `passes`, `DopWeighting`; outside this branch's assigned direct-DOP scope. |
127
+
128
+ ## Area 6 - Conjunction (astro)
129
+
130
+ Core: `astro::conjunction` (`collision_probability`, `encounter_frame`,
131
+ `encounter_plane_covariance`, `CollisionPc`, `ConjunctionState`,
132
+ `EncounterFrame`, `PcMethod`), `astro::covariance` (`rtn_to_eci`, `symmetric`,
133
+ `positive_semidefinite`, `RtnFrameError`).
134
+
135
+ | Capability | Status | Notes |
136
+ | ------------------------------------------- | ----------- | -------------------------------------------------------- |
137
+ | Close-approach / collision probability (Pc) | exposed-idiomatic | `ConjunctionState`, `PcMethod`, `collision_probability` -> `CollisionProbability`; numpy states/covariances, typed enum, `SolveError` for undefined relative motion. |
138
+ | Encounter frame + B-plane covariance | exposed-idiomatic | `encounter_frame` -> `EncounterFrame`; `encounter_plane_covariance` returns numpy `(2, 2)` B-plane covariance. Cross-checked against core fixture. |
139
+ | RTN-frame covariance transform | exposed-idiomatic | `rtn_to_eci_covariance`, `covariance_is_symmetric`, `covariance_is_positive_semidefinite`; numpy `(3, 3)` in/out, core validation errors mapped to `ValueError`. |
140
+
141
+ ## Area 7 - CCSDS CDM (astro)
142
+
143
+ Core: `astro::cdm` (`parse_kvn`, `parse_xml`, `encode_kvn`, `encode_xml`,
144
+ `CdmKvn`, `CdmObject`, `CdmError`).
145
+
146
+ | Capability | Status | Notes |
147
+ | -------------------- | ----------- | ------------------------------------------------------- |
148
+ | Parse CDM (KVN, XML) | exposed-idiomatic | `parse_cdm_kvn` / `parse_cdm_xml` -> `Cdm`; `CdmParseError` under `ParseError`; typed `CdmObject` with numpy state/covariance properties. |
149
+ | Write CDM (KVN, XML) | exposed-idiomatic | `Cdm.to_kvn_string()` / `Cdm.to_xml_string()` delegate to core `encode_kvn` / `encode_xml`; constructor supports typed value creation. |
150
+
151
+ ## Area 7b - CCSDS OMM (astro)
152
+
153
+ Core: `astro::omm` (`parse_kvn`, `parse_xml`, `parse_json`, `encode_kvn`,
154
+ `encode_xml`, `encode_json`, `Omm`, `OmmEpoch`, `OmmError`).
155
+
156
+ | Capability | Status | Notes |
157
+ | -------------------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
158
+ | Parse OMM (KVN, XML, JSON) | exposed-idiomatic | `parse_omm_kvn` / `parse_omm_xml` / `parse_omm_json` -> `Omm`; `OmmParseError` under `ParseError`; typed `OmmEpoch` value. |
159
+ | Write OMM (KVN, XML, JSON) | exposed-idiomatic | `Omm.to_kvn_string()` / `to_xml_string()` / `to_json_string()` delegate to core encoders; constructor supports typed value creation. |
160
+
161
+ ## Area 8 - RF link budget (astro)
162
+
163
+ Core: `astro::rf` (`fspl`, `eirp`, `cn0`, `dish_gain`, `link_margin`,
164
+ `wavelength`, `LinkBudget`).
165
+
166
+ | Capability | Status | Notes |
167
+ | --------------------------------------------------------- | ----------- | -------------------------------------------- |
168
+ | Link budget helpers (FSPL, EIRP, C/N0, dish gain, margin) | exposed-idiomatic | `fspl`, `eirp`, `cn0`, `wavelength`, `dish_gain`, `link_margin(LinkBudget)`; scalar floats plus typed `LinkBudget` (`@property`, `__repr__`, `__eq__`). Cross-checked bit-exact against Rust fixture. |
169
+
170
+ ## Area 9 - GNSS positioning solvers (already exposed; bring to bar)
171
+
172
+ Core: `positioning` (`solve_with_policy`, `SolveInputs`, `ReceiverSolution`,
173
+ `SolvePolicy`), `rtk_filter` (`solve_float_baseline`,
174
+ `solve_fixed_baseline_validated`, `MeasModel`, `Epoch`, opts/result structs),
175
+ `precise_positioning` (`solve_float_epochs`, `solve_fixed_from_float`,
176
+ `FloatEpoch`/`FloatState`/`*Config`, solutions).
177
+
178
+ | Capability | Status | Notes |
179
+ | ----------------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
180
+ | SPP single-point solve | exposed-idiomatic | `solve_spp(sp3, SppConfig)` uses typed `SppObservation`, correction, Klobuchar, and meteorology pyclasses; failures map to `SolveError`/`ValueError`; pytest reuses the Rust SPP trace fixture. |
181
+ | RTK float baseline | exposed-idiomatic | `solve_rtk_float(RtkFloatConfig)` uses typed epoch/satellite records, measurement model, and float options; pytest cross-checks the Rust-dumped WTZR fixture bit-exact. |
182
+ | RTK fixed baseline | exposed-idiomatic | `solve_rtk_fixed(RtkFixedConfig)` uses typed config/options and returns `IntegerStatus`; pytest cross-checks the Rust-dumped WTZR fixed fixture bit-exact. |
183
+ | PPP float arc | exposed-idiomatic | `solve_ppp_float(sp3, epochs, PppFloatState, PppFloatConfig)` uses typed epoch/observation/state/config pyclasses; pytest cross-checks the Rust-dumped ESBC fixture bit-exact. |
184
+ | PPP fixed (search + re-solve) | exposed-idiomatic | `solve_ppp_fixed(sp3, epochs, PppFloatSolution, PppFixedConfig)` exposes the ergonomic crate's fixed solver; the ESBC fixture emitter dumps fixed config/result metadata for pytest. |
185
+
186
+ ## Area 10 - GNSS data parsing + products (not exposed)
187
+
188
+ Core modules: `rinex` (nav/obs parse, CRINEX/Hatanaka decode, clock,
189
+ iono corrections), `antex` (PCO/PCV), `frequencies` (carrier table),
190
+ `navigation` (LNAV codecs), `combinations` (iono-free), `observables`
191
+ (forward predict), `velocity` (Doppler/range-rate solve), `dgnss`,
192
+ `quality` (RAIM/FDE/weighting), `signal` (C/A code, correlation, acquisition),
193
+ `carrier_phase` (geometry-free, Melbourne-Wubbena, cycle slip, smoothing),
194
+ `ppp_corrections`, `broadcast_comparison` (SISRE), `orbit` (reduced orbit),
195
+ `tides`, `terrain` (DTED), `atmosphere` (IONEX/Klobuchar).
196
+
197
+ | Capability | Status | Notes |
198
+ | --------------------------------------------- | ----------- | ---------------------------------------------------------------------------------- |
199
+ | RINEX navigation parse | not-exposed | `rinex::parse_nav`, `BroadcastStore`. |
200
+ | RINEX observation parse + CRINEX decode | not-exposed | `rinex` obs, Hatanaka. |
201
+ | RINEX clock parse/interp | not-exposed | `rinex_clock` (private; via `rinex`). |
202
+ | ANTEX PCO/PCV | not-exposed | `antex::Antex`, `pco`/`pcv`. |
203
+ | Carrier-frequency table | not-exposed | `frequencies::CarrierBand`, `wavelength_m`. |
204
+ | LNAV navigation-message codec | not-exposed | `navigation::{decode,encode,parity}`. |
205
+ | Observable linear combinations (iono-free) | not-exposed | `combinations::ionosphere_free*`. |
206
+ | Forward observable prediction | not-exposed | `observables::predict`. |
207
+ | Receiver velocity / clock-drift solve | not-exposed | `velocity::solve`, Doppler<->range-rate. |
208
+ | DGNSS code-differential corrections | not-exposed | `dgnss`. |
209
+ | Quality: RAIM / FDE / weighting | not-exposed | `quality::{raim,fde,weight_vector,validate_receiver_solution}`. |
210
+ | Signal: C/A code, correlation, acquisition | not-exposed | `signal::{ca_code,correlate,acquire}`. |
211
+ | Carrier-phase combos + cycle-slip + smoothing | not-exposed | `carrier_phase::{geometry_free,melbourne_wubbena,detect_cycle_slips,smooth_code}`. |
212
+ | PPP correction tables | not-exposed | `ppp_corrections::build`. |
213
+ | Broadcast-vs-precise SISRE | not-exposed | `broadcast_comparison::compare`. |
214
+ | Reduced-orbit fit/eval | not-exposed | `orbit::ReducedOrbitModel`. |
215
+ | Solid-earth tides | not-exposed | `tides::solid_earth_tide`. |
216
+ | DTED terrain lookup | not-exposed | `terrain::DtedTerrain`. |
217
+ | Atmosphere (IONEX/Klobuchar) | not-exposed | `atmosphere` (used internally by SPP). |
218
+
219
+ ## Area 11 - Estimation substrate / low-level kernels
220
+
221
+ Core: `estimation` (recipe/strategy substrate), `ils` (integer least squares),
222
+ `astro::math` (`vec3`/`mat3`/`linear`/`least_squares`/`polynomial`/`special`),
223
+ `astro::constants`, `astro::tolerances`, `constants`/`tolerances`.
224
+
225
+ | Capability | Status | Notes |
226
+ | ------------------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------- |
227
+ | Estimation recipe substrate (`estimate`, recipes, strategies) | deferred | Internal operation-order substrate behind the public solvers; not a user-facing API. Re-evaluate if a user needs custom recipes. |
228
+ | ILS ambiguity-search kernels | deferred | Low-level kernel consumed by RTK/PPP; the user-facing path is the fixed solvers. |
229
+ | math/constants/tolerances | deferred | Engine internals; constants can be surfaced as module-level Python constants if a need appears. |
230
+
231
+ ---
232
+
233
+ ## Iteration order (proposed; one area per iteration)
234
+
235
+ 1. [DONE] Exception hierarchy (cross-cutting) + brought SGP4/TLE area fully onto
236
+ it (`TleParseError`, `Sp3ParseError`, `SolveError`), added `tle::encode`
237
+ round-trip (`Tle.to_lines`) and `Tle.checksum_warnings`.
238
+ 2. [DONE] Frames + time (Area 3): scale-tagged `Instant` (TT/UT1/TDB + sidereal
239
+ time + precession/nutation), batched `teme_to_gcrs`/`gcrs_to_itrs`/
240
+ `itrs_to_gcrs` + geodetic<->ECEF, `TimeScale`/`JulianDate`/`GnssWeekTow`,
241
+ leap seconds + UT1/leap-second provenance. Added core public GMST/GAST
242
+ wrappers and made `UtcInstant::time_scales` public (additive). The PROJ
243
+ ECEF->geodetic variant is deferred (distinct convention, stated reason).
244
+ 3. Numerical propagation (Area 2): already exposed-idiomatic (`propagate_state`)
245
+ from a prior increment; only the `DenseOutput` interpolation row remains
246
+ not-exposed. Revisit that row in a later pass.
247
+ 4. [DONE, partial] Bodies + SP3 (Area 4): Sun/Moon ECI+ECEF batched numpy,
248
+ SP3 `epochs_j2000_seconds`/`interpolate`/`state`, path-accepting `load_sp3`,
249
+ and `to_sp3_string`. Added core `sun_moon_eci_at` + `Sp3::epochs_j2000_seconds`
250
+ (additive). Remaining Area 4 rows: SP3 combine (`merge`) - next step; broadcast
251
+ ephemeris - folded into Area 10 (RINEX nav).
252
+ 5. Geometry + events (Area 5): pass prediction, eclipse, angles, DOP.
253
+ 6. Conjunction (Area 6).
254
+ 7. CCSDS CDM (Area 7).
255
+ 8. RF link budget (Area 8).
256
+ 9. [DONE] GNSS solvers to the bar (Area 9): typed config objects replacing
257
+ dict blobs, `solve_ppp_fixed`, status enums.
258
+ 10. GNSS data parsing + products (Area 10), split across iterations as needed.
259
+ 11. Late iteration: calibrated propagation and topocentric benchmark on the propagation /
260
+ 6. [DONE] Conjunction (Area 6): Pc methods, encounter frame + B-plane
261
+ covariance, RTN->ECI covariance, symmetry/PSD checks. Python pytest
262
+ cross-checks the core env-gated fixture bit-for-bit.
263
+ 7. [DONE] CCSDS CDM (Area 7): KVN/XML parse and encode with typed `Cdm` /
264
+ `CdmObject`, CDM parse exception, and pytest cross-check against a core
265
+ env-gated fixture.
266
+ 8. [DONE] CCSDS OMM (Area 7b): KVN/XML/JSON parse and encode with typed
267
+ `Omm` / `OmmEpoch`, OMM parse exception, and pytest cross-check against a
268
+ core env-gated fixture.
269
+ 9. RF link budget (Area 8).
270
+ 10. GNSS solvers to the bar (Area 9): typed config objects replacing dict blobs,
271
+ `solve_ppp_fixed`, status enums.
272
+ 11. GNSS data parsing + products (Area 10), split across iterations as needed.
273
+ 12. Late iteration: calibrated propagation and topocentric benchmark on the propagation /
274
+ topocentric overlap (`bench/`), real measured numbers, recorded measured numbers.
275
+
276
+ When every row above is `exposed-idiomatic` or `deferred` with a stated reason
277
+ and the benchmark is committed: append "PYTHON IMPROVE DONE" to PROGRESS.md and
278
+ touch the completion sentinel.