Skip to content

Fix #286: handle observatories with zero-valued parallax constants - #339

Merged
matthewholman merged 1 commit into
mainfrom
issue/286/geocentric-obscode
Jun 23, 2026
Merged

Fix #286: handle observatories with zero-valued parallax constants#339
matthewholman merged 1 commit into
mainfrom
issue/286/geocentric-obscode

Conversation

@matthewholman

Copy link
Copy Markdown
Collaborator

Summary

Fixes #286. layup orbitfit crashed with ValueError: Observatory <code> has invalid coordinates for several real observatory codes — most importantly the common geocentric code 500, plus 000 (Greenwich), 782 (Quito), and 244/248.

Root cause

The crash originates in Sorcha's Observatory.convert_to_geocentric (simulation_parsing.py), which gates on the truthiness of the parallax constants:

if (obs_location.get("Longitude", False)
    and obs_location.get("cos", False)
    and obs_location.get("sin", False)):

A parallax constant that is legitimately 0.0 is falsy, so the observatory is wrongly reported as having no fixed position. Layup then routes it to the moving-observatory path, which demands per-observation pos1/2/3 fields and raises for plain MPC input that carries none. The affected codes all have a constant that is exactly 0.0:

code name zero constant
500 / 244 / 248 Geocentric / Occultation / Hipparcos Longitude = cos = sin = 0
000 Greenwich Longitude = 0
782 Quito (equatorial) sin = 0

Fix

Override convert_to_geocentric in LayupObservatory to test for the presence of the constants (is not None) rather than their truthiness. Since super().__init__ calls self.convert_to_geocentric, the override is used when the observatory table is built — no upstream release needed.

  • Codes with no position keys at all (roving observer 247, space telescopes WISE/TESS/HST) still return (None, None, None) and correctly take the ADES per-observation position path.
  • The geocenter resolves to a (0, 0, 0) offset from Earth's center, which is exactly right.

The same bug should also be fixed upstream in Sorcha (a separate one-line PR); this override makes layup correct immediately and is robust regardless of the installed Sorcha version.

Verification

  • New regression test test_fixed_observatory_with_zero_parallax_constant — passes with the fix, fails without it.
  • Full tests/layup/test_data_processing_utilities.py: 40 passed.
  • End-to-end: a geocentric 500 observation now flows through obscodes_to_barycentric and resolves to Earth's barycentric position; Greenwich 000 lands exactly one Earth radius (4.25e-5 AU) from the geocenter.

🤖 Generated with Claude Code

LayupObservatory.obscodes_to_barycentric crashed with "Observatory ...
has invalid coordinates" for several real observatory codes, including the
common geocentric code 500. The root cause is in Sorcha's
Observatory.convert_to_geocentric, which gates on the truthiness of the
parallax constants (obs_location.get("sin", False)). A constant that is
legitimately 0.0 -- the geocenter (500/244/248: Longitude=cos=sin=0),
Greenwich (000: Longitude=0), or an equatorial station such as Quito
(782: sin=0) -- is falsy, so the observatory is wrongly reported as having
no fixed position. Layup then routes it to the moving-observatory path,
which raises for plain MPC input that carries no per-observation position.

Override convert_to_geocentric in LayupObservatory to test for the presence
of the constants (is not None) rather than their truthiness. Codes with no
position keys at all (roving observer 247, space telescopes WISE/TESS/HST)
still return (None, None, None) and correctly take the ADES-position path;
the geocenter resolves to a (0, 0, 0) offset from Earth's center, which is
exactly right. Add a regression test that fails without the fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
matthewholman added a commit that referenced this pull request Jun 22, 2026
Brings the branch up to date with main (36 commits: the multi-root
IOD picker + residual prefilter, IAS15 adaptive_mode=2, #339-#344
fixes, docs/dash_ui). Resolves the one conflict in src/layup/orbitfit.py,
where bk-everywhere had added the engine= dispatch to the pre-picker
do_fit while main rewrote do_fit around the multi-root picker.

Resolution keeps main's picker/prefilter pipeline and threads `engine`
through it:
  - _run_fit() gains iter_max; it is forwarded to the Cartesian engine
    (run_from_vector_with_initial_guess, which main parameterized) and
    ignored by bk_native (run_bk_native_fit takes mu and uses its own
    internal LM cap).
  - do_fit() keeps all of main's picker knobs and adds engine="cartesian".
  - the two picker LM passes and the incremental-fit loop now dispatch
    through _run_fit(..., engine, iter_max).

Validated in an isolated build: 54/54 tests pass across test_bk_basis,
test_bk_fit, test_bk_everywhere, and test_orbit_fit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@matthewholman
matthewholman requested a review from kjnapier June 22, 2026 22:41

@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.

The new gate logic looks good to me. I did not check the correctness of the old expressions, but assuming they did not change, all is good.

@matthewholman
matthewholman merged commit f1ae67f into main Jun 23, 2026
7 checks passed
@matthewholman
matthewholman deleted the issue/286/geocentric-obscode branch June 23, 2026 00:54
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.

observatory code coming up nans

2 participants