Replace Space-Track with IAU CPS SatChecker for orbit records - #44
Open
chrisfinlay wants to merge 3 commits into
Open
chrisfinlay wants to merge 3 commits into
chrisfinlay wants to merge 3 commits into
Conversation
Vendors tabascal/satchecker/ from epfl-radio-astro/tabascal#92 and rebuilds tabsim's satellite retrieval on top of it. No account or credentials are required and the spacetrack dependency is removed. Both orbital-data formats SatChecker serves are handled: TLEs for epochs before its 2026-07-12 handover, and OMM records after it. An OMM record has no lines to parse, so its elements go straight into an sgp4.Satrec via sgp4init; both kinds end up as the same propagator over the same model. Retrieval gains per-satellite source precedence (extra_orbit_dir, managed cache, SatChecker), a validated per-NORAD cache, an age policy, and a reproducible used_orbits.json written into each simulation's input_data. Satellite names resolve through SatChecker's substring search rather than Space-Track's LIKE query, preserving the old semantics including the upper-casing tabsim always applied. Removes spacetrack_path, tle_dir, the tabsim-setup-spacetrack entry point, the shipped Space-Track TLE cache, and the Space-Track credentials from CI.
chrisfinlay
force-pushed
the
satchecker-orbits
branch
from
September 16, 2026 11:19
634d352 to
8c4dddf
Compare
The SatChecker client this branch vendored from epfl-radio-astro/tabascal#92 has since been extracted into its own MIT-licensed distribution (github.com/epfl-radio-astro/satchecker-client), which tabascal already depends on. Depend on it here too rather than carrying a second copy. On the API the two are interchangeable: every name the vendored __init__ exported is still exported, no signature changed, and the package adds set_client_identifier/user_agent. What it gained since the copy was taken is internal — an exclusive lock around cache writes, NORAD-ID precision filtering on responses, and a dedicated eccentricity-field parser. They are not interchangeable in behaviour, in one respect. The vendored copy documented a single deviation from upstream and that deviation is a fix: read_legacy_tle_records passes precise_float=True to pandas.read_json, without which an OMM eccentricity of 0.0066635 reads back as 0.006663499999999999 and a replayed trajectory disagrees with the run its file was written to reproduce. That fix went upstream as satchecker-client 0.1.2, so the floor here is >=0.1.2. Against 0.1.1 tests/test_orbit.py fails on exactly that case, by design. What moves and what stays: - tabsim.satchecker imports become satchecker_client imports; the policy layer (orbit.py, orbit_config.py, tle.py) is untouched beyond that, and tabsim.satchecker_names keeps the name lookup that has no counterpart in the client. Its docstring no longer justifies itself by keeping a vendored copy byte-comparable, and notes that it leans on two of the client's private helpers. - orbit.py names tabsim in the shared client's User-Agent via the package's set_client_identifier, so SatChecker sees traffic attributable to tabsim rather than to the library every consumer shares. - The tests needed no rework: tests/test_orbit.py already covered only the tabsim side of the seam, leaving the client's own suite to its repository. requires-python rises to >=3.10, the floor satchecker-client declares, and the CI matrix swaps 3.9 for 3.10 to match.
test_omm_elements_survive_the_file_bit_for_bit checked MEAN_MOTION 1.8959772500000001 alongside the eccentricity. That literal is the double 1.89597725, which every parser involved decodes exactly, so the assertion could not fail. BSTAR 3.2e-05 replaces it: satchecker-client 0.1.1 reads it back as 3.2000000000000005e-05. Against 0.1.1 each value now fails on its own; against 0.1.2 the test passes. The docstring names both halves the test guards, the writer's float formatting and the reader's float parsing, where it named only the first.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Replaces tabsim's Space-Track TLE retrieval with the credential-free
IAU CPS SatChecker service, built on the
satchecker-clientpackage —the client extracted from epfl-radio-astro/tabascal#92,
which tabascal also depends on — with tabsim's satellite selection rebuilt on top of it.
That PR explicitly left this out of scope — "Generating a simulation with
sim-visfrom the separately installed
tabsimstill uses Space-Track; that is out of scopehere." This is that follow-up.
Both orbital-data formats SatChecker serves are supported: TLEs for epochs before its
2026-07-12 handover, and OMM (Orbit Mean-Elements Message) records after it. No account
or credentials are required, and the
spacetrackdependency is removed.The client:
satchecker-client>=0.1.2This PR first vendored
tabascal/satchecker/astabsim/satchecker/. That client hassince been extracted into its own MIT-licensed distribution, so tabsim now depends on
satchecker-clientinstead of carrying asecond copy (d01171f). Its API is a superset of what was vendored: no exported name was
removed and no signature changed.
The vendored copy had one functional deviation from upstream:
read_legacy_tle_recordspassedprecise_float=Truetopandas.read_json. pandas'default JSON float parser is not correctly rounded: it reads
0.0066635back as0.006663499999999999, a different double. For an OMM record that is theeccentricity, so a run replayed from its own
used_orbits.jsonpropagated to ameasurably different trajectory than the run that wrote it. Found by asserting exact
equality on a real end-to-end replay rather than
approx; see Verification. The fixwent upstream in epfl-radio-astro/satchecker-client#2
and shipped as 0.1.2,
which is therefore the floor here.
Consequences of the switch:
requires-pythonrises from>=3.9to>=3.10, the floor satchecker-clientdeclares; the CI matrix swaps 3.9 for 3.10.
User-Agent(
tabsim/<version> (+https://github.com/chrisfinlay/tab-sim)), viaset_client_identifier.tabsim/satchecker_names.py, which uses two of the client's private helpers(
_http_get,_load_json); a satchecker-client release could rename them, and theversion floor does not guard against that.
What tabsim needed that TABASCAL did not
TABASCAL resolves a fixed list of NORAD IDs at one epoch. tabsim additionally
searches: it takes a candidate set (IDs and/or names), propagates each one, and
keeps the satellites that actually pass near the target. Two consequences shaped this PR.
Names
tabsim's
sat_nameshas no TABASCAL counterpart. SatChecker offers two name endpointsand only one of them is right here:
navstarnorad-ids-from-namesearch-satellitesNAVSTAR)Space-Track's
op.like(name)wraps the pattern in wildcards, so it was a substringsearch, and tabsim always passed
name.upper().search-satellitesplus the sameupper-casing reproduces that exactly. The exact-name index would silently have reduced
sat_names: [navstar]— which every shipped example config uses — to nothing at all,and the simulation would have run to completion with no satellites and no error. This
was caught end-to-end rather than by unit test, and the reasoning is written into the
module so it does not recur.
Decayed objects are dropped: they cannot be observed, have no record near any
present-day epoch, and would otherwise turn every re-entered namesake into a coverage
failure.
Coverage: numbered vs named satellites
TABASCAL's rule is that every configured satellite must resolve or the run stops.
Applied unchanged here it would be wrong for names, so the two are separated:
norad_ids,norad_ids_path) keep the strict rule. The usernamed these individually; one dropped for want of a record is indistinguishable from
one that simply never passed the target, which is exactly the silent failure the rule
exists to prevent.
OrbitErrornames each failure, how close the best availablerecord was, and the remedies.
no satellite for a record to be missing for — reported, not fatal. A named satellite
whose record cannot be obtained is reported and excluded.
Retrieval behaviour
Unchanged from #92, since it is the same client code. Each NORAD ID resolves independently:
extra_orbit_dir→ managed per-NORAD cache → SatChecker, with endpoint selectionagainst the handover date, failover to the other archive when the first yields nothing
acceptable, bounded concurrency, outage handling, and validated caching.
tabsim/orbit.pyis ported fromtabascal/orbit.pyless the multi-process broadcastand the Measurement Set preflight — tabsim's simulation is single-process and builds its
own time grid — plus
resolve_names.tabsim/orbit_config.pyis ported fromtabascal/orbit_config.pyless the MS epoch derivation and model-componentintrospection, which tabsim has no equivalent of.
Propagation
FixedOrbit-style propagation builds a SkyfieldEarthSatellitefrom two TLE lines, soan OMM record could not be propagated at all — post-handover simulations would fail on
the default satellite component. An OMM's elements now go straight into an
sgp4.Satrecvia
sgp4init, wrapped byEarthSatellite.from_satrec. The TLE path is untouched.ndot/nddotare passed as zero — SGP4 models drag throughbstaralone and neverreads them while propagating.
Output schema
rfi_tle_sat_orbitis a fixed-width string array of the two TLE lines. An OMM recordhas none — its elements are the record — so those rows are written empty rather than
filled with something that looks like a TLE and is not one. The zarr/MS schema is
otherwise unchanged.
The complete record goes to
input_data/used_orbits.jsoninstead, inextra_orbit_dirformat. Pointing a later run'sextra_orbit_dirat that directoryreproduces the run's satellite trajectories exactly, independently of the shared cache,
of the age ceiling, and of what SatChecker serves by then. It is written by hand rather
than with
DataFrame.to_json, which formats floats to a fixed number of decimal places— the default 10 rounds an OMM element outright, and even the maximum 15 writes
0.0066635as0.006663499999999999.json.dumpwrites a float throughrepr, theshortest representation that reads back identically.
Configuration
Added under
rfi_sources.tle_satellite:extra_orbit_dirnullextra_orbit_max_age_daysnullnull= unlimited, keeping exact replay workingremote_max_age_days3cache_reuse_max_age_days1Also:
ORBIT_CACHE_DIRrelocates the managed cache, andsim-vis --extra_orbit_dir(
-eod) setsextra_orbit_dirat runtime. A path given on the command line resolvesagainst the working directory; one in the config resolves against the config, like every
other path there.
Removed:
spacetrack_path,tle_dir, the-st/--spacetrackflags onsim-visandtle-region, thetabsim-setup-spacetrackentry point and its script, the shippedSpace-Track TLE cache under
tabsim/data/rfi/tles/, the Space-Track credentials step inCI, and the
spacetrackdependency.sgp4andplatformdirsare added;pandasispromoted from transitive to declared, since it is imported directly.
tabsim-import-tlesnow writes to./orbitsby default rather than into the installedpackage, and its output is documented as an
extra_orbit_dir— which is what it alwaysproduced, but there was no longer anything reading the old location.
norad_ids_pathis read byread_norad_ids_filerather thannp.loadtxt, which reportsthe offending file and line number on a typo. It still takes the first column, so a
file pairing IDs with a name or note reads as before.
Verification
tests/test_orbit.pycoveringsource precedence, the age policy, endpoint failover, outage handling, coverage
errors, replay, configuration validation, name lookup, and both propagation paths.
Everything in it runs offline.
same TLE it is compared against, so a degrees-for-radians or rev/day-for-rad/min slip
would show up as kilometres. Measured max separation over 12 h on a 6800 km orbit:
0.11 m, and a field-by-field
Satreccomparison shows every orbital elementtransfers exactly — the residual is entirely the OMM
EPOCHbeing an ISO 8601string, so the epoch survives at microsecond resolution (~15 µs here, ~0.1 m of
along-track motion at 7.7 km/s).
sat_names: [navstar]): 80NAVSTAR satellites resolved via
nearest-TLE, 3 visible, RFI added, zarr + MS written,rfi_tle_sat_orbitcarrying the expected TLE lines.resolved via
nearest-OMM, propagated, and written with empty line columns and fullOMM records in
used_orbits.json.--extra_orbit_dirpointed at the first run'sinput_dataproducesbyte-identical
rfi_tle_sat_xyzandvis_obs(np.array_equal, notapprox).This is the assertion that surfaced the
precise_floatbug; before the fix it was offby 11 nm and nothing would ever have said so.
measuring what it claims to.
The measurements above predate the switch to satchecker-client. For the switch itself
(Python 3.10.12):
tests/test_orbit.pyagainst satchecker-client 0.1.2 installed from PyPI:52 passed.
(
ECCENTRICITY0.0066635,BSTAR3.2e-05) each fail on their own. Its former secondvalue,
MEAN_MOTION1.8959772500000001, is the double 1.89597725 and decodes exactlywith either parser, so it could never fail;
BSTARreplaced it (0054dc1).Python 3.10, 3.11 and 3.13.
Known limitations
so
sat_names: [starlink](>21000 objects) is that many requests on a cold cache,where Space-Track answered in one bulk query. Inherent to the service's API, and
equally true of #92. A warning fires above 500 matches;
navstar(80) is the realisticcase, and every response is cached.
remote_max_age_days: 3is a backstop against obviously unsuitable records, not aclaim of three-day positional accuracy.
re-derived; range checks and an absolute epoch plausibility window are what remain.
Documented in satchecker-client's
records.py.tests/test_sim-vis.py::test_simulation_runs_with_configreaches the network. Itpreviously required Space-Track credentials, so this is a reduction in what CI needs,
not a new requirement.