Skip to content

Resolve space-based observatories via JPL Horizons (Closes #55)#377

Merged
matthewholman merged 2 commits into
mainfrom
issue/55/jpl-special-observatories
Jul 1, 2026
Merged

Resolve space-based observatories via JPL Horizons (Closes #55)#377
matthewholman merged 2 commits into
mainfrom
issue/55/jpl-special-observatories

Conversation

@matthewholman

Copy link
Copy Markdown
Collaborator

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 spacecraft obscode -> NAIF-id map from the Rust spacerocks project and queries JPL Horizons for the observer's geocentric state when no position is given.

What it does

  • 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 — 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_observatory adds 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)

  1. Horizons HTTP (not SPK kernels) — the case is rare; avoids per-spacecraft kernel management. SPK-backed resolution can be a future enhancement.
  2. Geocentric query (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.
  3. In-run batched cache only; cross-run disk caching deferred.

Scope boundaries: roving (247/270) and occultation (275) codes stay ADES-supplied (not Horizons-resolved).

Validation

  • 63 tests pass; black clean.
  • Live Horizons cross-check (HST): |r| ≈ 6916 km (≈537 km LEO altitude ✓), |v| ≈ 7.59 km/s ✓ — confirms units, frame, and parsing against the real server.
  • 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. LayupObservatory integration 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 code 247 in test_moving_observatory_coordinate_cache (since C51 is now Horizons-resolved, it no longer exercises the "ADES position required" error path).

🤖 Generated with Claude Code

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>
@matthewholman matthewholman added this to the v1.0 milestone Jun 30, 2026
Comment thread src/layup/utilities/special_observatories.py

@kjnapier kjnapier left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 kjnapier left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay thanks for adding that comment, looks good to me

@matthewholman matthewholman merged commit 8a01b8e into main Jul 1, 2026
7 checks passed
@matthewholman matthewholman deleted the issue/55/jpl-special-observatories branch July 1, 2026 11:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Convert Rust code for querying JPL for special case observatories

2 participants