Resolve space-based observatories via JPL Horizons (Closes #55)#377
Merged
Conversation
Spacecraft observatory codes (HST, JWST, WISE, ...) have no MPC parallax constants, so layup could only place them if the user supplied an explicit ADES per-observation position; otherwise the fit raised ValueError. Port the spacecraft obscode -> NAIF-id map from the Rust `spacerocks` project and query the JPL Horizons vector API for the observer's geocentric state when no position is given. - New `utilities/special_observatories.py`: the 9-entry SPACE_OBSERVATORIES map + `query_horizons_geocentric()`. Asks Horizons for a geometric (VEC_CORR=NONE), geocentric (CENTER=@399), ICRF-equatorial state in km/km-s (OUT_UNITS=KM-S) -- i.e. layup's internal moving-observer units -- and parses the CSV vector table. Epochs are batched (one request per spacecraft). - `populate_observatory()`: in the no-parallax-constants branch, route a space-based obscode without a usable ADES position to Horizons, caching the geocentric position + velocity under the same per-epoch key #147 uses, so `_barycentric_moving_observatory` adds Earth's barycentric state to it. A user-supplied ADES position always takes priority. - `obscodes_to_barycentric()`: prefetch/warm the cache for all space-based epochs up front, one batched HTTP call per spacecraft. Tests: pure-logic (map, JD conversion, CSV parser, HTTP client with the network mocked) run everywhere; LayupObservatory integration tests (ephem-gated, HTTP mocked) cover the Horizons path, the ADES-override priority, batching, and the end-to-end barycentric placement. Cross-checked live against Horizons (HST: |r|~6916 km, |v|~7.6 km/s). The existing moving-observer test switched from C51 to the roving code 247 (C51 is now Horizons-resolved). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
kjnapier
reviewed
Jun 30, 2026
kjnapier
reviewed
Jun 30, 2026
kjnapier
left a comment
Collaborator
There was a problem hiding this comment.
See comment re-Geocentric vs Barycentric queries
#377) Per the #377 review discussion (kjnapier/matthewholman): document why we query the geocentric spacecraft state (reproducible offset against a physical body vs the ephemeris-dependent barycenter) and flag the residual ~meter-level geocenter-realization mismatch as a known limitation for very high precision work (e.g. space-based occultations). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
kjnapier
approved these changes
Jun 30, 2026
kjnapier
left a comment
Collaborator
There was a problem hiding this comment.
Okay thanks for adding that comment, looks good to me
This was referenced Jul 2, 2026
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.
Closes #55. Scoped in #55 (comment).
Spacecraft observatory codes (HST, JWST, WISE, ...) have no MPC parallax constants, so layup could only place them when the user supplied an explicit ADES per-observation position; otherwise the fit raised
ValueError. This ports the spacecraftobscode -> NAIF-idmap from the Rustspacerocksproject and queries JPL Horizons for the observer's geocentric state when no position is given.What it does
utilities/special_observatories.py— the 9-entrySPACE_OBSERVATORIESmap +query_horizons_geocentric(). Asks Horizons for a geometric (VEC_CORR=NONE), geocentric (CENTER=@399), ICRF-equatorial state in km / km·s⁻¹ (OUT_UNITS=KM-S— layup's internal moving-observer units), and parses the CSV vector table. Epochs are batched (one request per spacecraft).populate_observatory()— in the no-parallax-constants branch, a space-based obscode without a usable ADES position is routed to Horizons; the geocentric position + velocity are cached under the same per-epoch key Read in and use user provided observatory velocities in layup orbitfit #147 uses, so_barycentric_moving_observatoryadds Earth's barycentric state to it. A user-supplied ADES position always takes priority.obscodes_to_barycentric()— pre-pass warms the cache for all space-based epochs, one batched HTTP call per spacecraft.Design decisions (per the issue)
CENTER=@399) — drops straight into the existing Read in and use user provided observatory velocities in layup orbitfit #147 "geocentric + add Earth" path; no new downstream code.Scope boundaries: roving (
247/270) and occultation (275) codes stay ADES-supplied (not Horizons-resolved).Validation
test_tno_validation(real HST data) still passes — confirmed no-op, since its fixture supplies ADES positions (which keep priority).Tests
Pure-logic tests (map, JD conversion, CSV parser, HTTP client with the network mocked) run everywhere.
LayupObservatoryintegration tests (ephem-gated, HTTP mocked) cover the Horizons path, the ADES-override priority, request batching, and end-to-end barycentric placement. No test touches the network.The one existing-test change swaps the generic moving-observer code
C51→ the roving code247intest_moving_observatory_coordinate_cache(sinceC51is now Horizons-resolved, it no longer exercises the "ADES position required" error path).🤖 Generated with Claude Code