Skip to content

astrometry: model the gaia/abs photocenter wobble of a COMPANION-group target (review 8.10.6) - #502

Merged
jdeast merged 2 commits into
masterfrom
feat/companion-wobble-8.10.6
Oct 27, 2026
Merged

jdeast merged 2 commits into
masterfrom
feat/companion-wobble-8.10.6

Conversation

@jdeast

@jdeast jdeast commented Oct 27, 2026 •

Copy link
Copy Markdown
Owner

What

A gaia/abs/catalog dataset whose target star sits in an orbit's COMPANION group now gets that orbit's photocenter wobble. Before, _photocenter_terms summed only the orbits whose PRIMARY group held the target and warned for the rest ("not modeled for gaia/abs data yet"), so the orbital term was silently dropped -- and since #498 tied a companion's distance/pm to its orbit anchor, such a fit was silently a system-pm fit with no orbit at all. With Gaia DR4 epoch astrometry for every source, the companion of a binary will routinely be the target. (Gap dated from 4d46179, the hierarchical body groups.)

Physics and signs (derived from the code, not assumed)

Orbit.get_sky_position(relative=False) traces the PRIMARY group's own orbit about the barycenter (omega_), i.e. the direction of -x_rel, with x_rel = x_companion_group - x_primary_group (relative=True, omega_ + 180). The existing primary-target amplitude a_rel (M_c/M - beta) through relative=False is therefore x_rel (beta - M_c/M) -- matching x_P = -(M_c/M) x_rel and a PSF photocenter (1 - b) x_target + b x_other. The brief's convention agrees with the code; nothing disagreed.

target role other light wobble (k x_rel) amplitude passed to relative=False
primary companion side b - M_c/M a_rel (M_c/M - b) -- the old expression, unchanged
companion primary side M_p/M - b a_rel (b - M_p/M) -- negated, like RV's -K M_p/M_c
same group as the other light -M_c/M or +M_p/M
target only (other light not in this orbit) (1 - b)(...)
other light only (target not in this orbit) b (...)

Summed over every Keplerian orbit that moves the photocenter, each with its own mass fractions -- so KELT-4 C gets BC (as primary) and A-BC (inside the companion group). The new sign claim is written up in skyframe.md, "The photocenter of an unresolved pair".

One spelling for the flux fraction. fluxfrac and the SED fraction now always mean the other star's share of the light in this PSF: the companion side's for a primary-group target, the primary side's for a companion-group one. companion_star_ndx names "the other luminous star inside the target's PSF" (for a companion target that is the primary); the SED computes F_c/(F_c + F_target) exactly as before, so a luminous primary dragging a companion target needs no new key. The PSF holds the target and at most one other light (orbit.bodies.photocenter_stars). On the SED path that star sits at its own position in every orbit, including orbits the target is not in (its own inner orbit moves it too); every other star is outside the PSF. With a sampled fluxfrac, the other light sits at the other group's barycenter.

_pair_beta (rel mode) was not reused. It weighs both sides of a nested orbit with full group fluxes, a different question; the absolute model's coefficients come from the per-star roles above.

One membership rule. bodies.photocenter_stars + Orbit.star_membership (both sides) is used by the model (_photocenter_orbits) and by every "which orbits does astrometry measure" predicate: _astrometric_orbits (-> amplitude_constrained_orbits / inclination_constrained_orbits, so fitmsini/log_q/transit-only), _node_degenerate_orbits, the Taylor refusal in _taylor_consumers, and epochs_constraining (whose amplitude estimate now uses M_p/M for a companion-group target). Before, they all said "primary only", so they agreed with the old model. Now they agree with the new one.

The reference point (interaction with #498 / #501)

The abs track is star.<target>.ra/dec at the epoch + the system pm and parallax (linked to the anchor for members) + the wobble about the system barycenter, for a primary and a companion target alike. So star.<target>.ra/dec is the barycenter's position at the epoch as that dataset sees it, not the star's own mean position (that was already true for primary targets, and is now documented). ra/dec is NOT on the #498 link list, so two datasets on two members of one system fit two independent barycenter zero points, and their separation information is not used. See Decision 2.

What raises (no warn-and-drop left)

  • A nonzero sampled fluxfrac when the target is in two or more Keplerian orbits (KELT-4 A: is the light the planet b's or B+C's?), naming the dataset and the orbits.
  • A nonzero sampled fluxfrac when the other group of the target's one orbit holds a Keplerian orbit of its own (the light wobbles inside that group by an amount one number cannot split). For both, the remedy in the message is the SED path or fluxfrac pinned at 0. The dark default (0) is legal in any topology.
  • companion_star_ndx sharing no Keplerian orbit with the target (stage 3). Such a star is a blend on its own track, and it used to be read as if its light rode the target's orbit. Two existing tests (test_sed_flux_constraints, test_component_override_channel) used exactly that topology (the companion star C in no orbit). C now rides the BH orbit's companion group, and their assertions are unchanged.
  • companion_star_ndx == star_ndx.
  • A band whose filter the SED's BC grid lacks, on the SED path. This used to warn and return the pinned fluxfrac, i.e. silently a dark-companion fit. It is the review's 1.10.12 fallback, in this path; the rel-mode twin in _pair_beta is left alone.

Tests (tests/test_astrometry_companion_wobble.py, slow-marked)

  • Mirror: companion target with b = 0.2 vs an independent rotation-matrix Kepler orbit, x_rel (M_p/M - b), atol 1e-9 of the amplitude.
  • Symmetry: the same orbit from A's data (b_c = 0.1) and B's (b_p = 0.2), opposite directions, ratio (M_p/M - b_p)/(b_c - M_c/M) to 1e-9. Also: with consistent fractions (b_p = 1 - b_c) the two tracks are identical (one PSF, one photocenter).
  • Equal mass, equal light: zero wobble on both sides (< 1e-12 of the scale).
  • SED path: the companion target with companion_star_ndx: A. fluxfrac_sed = F_A/(F_A+F_B), and the wobble uses that b.
  • Hierarchy: KELT-4 C (SED path, B in the PSF) vs the flux-weighted photocenter built from each star's own two-body positions in both orbits, 1e-9. Also the dark C target in both orbits.
  • Max-likelihood recovery: gaia data of the companion, start displaced by Omega +8 deg, i +5 deg, omega +0.15 rad, tc +5 d, e 0.25, P x1.003, recovered to < 1 deg in Omega, 0.02 in cos i and e, 2e-3 in P. It uses BFGS: L-BFGS-B stalls part-way down the (Omega, omega, tc) ridge, 140 logp units short of the truth.
  • C vs JAX dlogp agree (rtol 1e-6) on the companion-target model.
  • The four raises above.

The probes evaluate at system.get_raw_starts(model) with RVs replaced by value vars. Evaluating on the RVs draws from the prior, and model.initial_point() is not the start: both silently gave wrong numbers in the first draft of these tests.

Bit-identity (master 2d75d79 vs branch, repr of start logp)

Every top-level shipped example config (45 + theophylline, which fails identically on both for a missing data file): all identical. That includes every astrometry config (GaiaBH1, HIP1349, astrometry_sim/simbinary, kelt4 x3) and every config with orbits. No shipped config has a companion-group gaia/abs target, so none was expected to move. The primary-only path builds the identical expression (a[omap] * RSUN_AU * (m_c[omap]/m_tot[omap] - beta) * plx).

table
DC2018_128/DC2018_128.yaml 3374.9580594389927
GaiaBH1/gaiabh1.yaml -280.1884683043691
HIP1349/hip1349.yaml -342.9001487304094
KMT-2019-BLG-1806/KMT-2019-BLG-1806.yaml 45542.37003904056
KMT-2019-BLG-1806/KMT-2019-BLG-1806_nt24.yaml 45542.37003904056
KMT-2019-BLG-1806/KMT-2019-BLG-1806_nt8long.yaml 45542.37003904056
KMT-2021-BLG-1122L/KMT-2021-BLG-1122L.yaml -10881.286869037836
astrometry_sim/simbinary.yaml -247.570657169862
galactic_model/gm.yaml -1.147654486433269
gj1214/gj1214.yaml 108507.98057121363
hat3/hat3.yaml 272773.20403545885
hat3/hat3_mist.yaml -49.31237803166701
hat3/hat3_mist_claret.yaml 272777.8410903889
hat3/hat3_staronly.yaml -63.61527132827422
hat3/hat3_transitonly.yaml 257766.83025142687
hd80606/hd80606.yaml -132354.2709241234
hd80606/hd80606_rvonly.yaml -125231.1944982946
hd80606/hd80606_transitonly.yaml -7237.699431907968
kb180087_obj3/kb180087_obj3.yaml 56832.15897787771
kelt17/kelt17.yaml 660.5434506047156
kelt17/kelt17_all.yaml 9034.83927067534
kelt17/kelt17_dt.yaml 8885.06513262627
kelt17/kelt17_fast.yaml 16951.79164645858
kelt17/kelt17_rm.yaml -23541.90947462909
kelt4/kelt4.yaml 83450.08472039671
kelt4/kelt4_rv+transit+sed.yaml 84411.3135835002
kelt4/kelt4_rvonly.yaml -601.5560045465405
ob07224/ob07224.yaml 23203.064369043608
ob08092/ob08092.yaml 6252.18472912271
ob09020/ob09020*.yaml (x6) identical
ob140939/ob140939.yaml -1233.570220580738
ob140939/ob140939_today.yaml -1978.405083073234
ob161003/ob161003.yaml 79400.67660985189
ob161045/ob161045.yaml 22069.6246859237
ob170114/ob170114.yaml 5923.8002694345305
ogle0383/OGLE_0383LD.yaml 26739.696290781103
toi3919/toi3919*.yaml (x2) 8259.733675584166
wasp18/wasp18.yaml 83524.04648329612

Full suite

Local full suite (-n6, box load ~28): 5382 passed, 4 skipped, 1 xfailed, 1 failed: test_chen.py::test_astrometry_gaia_mode_uses_primary_membership. That test pinned the old primary-only gap (a gaia target in the companion group "does not count as mass-constrained"). It is updated in the second commit to the new rule (renamed ..._counts_companion_membership, and its fake system gains the raw config that photocenter_stars reads). test_chen.py then passes 36/36. The targeted astrometry/orbit set (14 files, 241 tests) passes.

Decisions for review

  1. One other light per PSF. The SED path takes one companion_star_ndx, so a PSF holding three luminous stars (KELT-4 C with both B and A unresolved) cannot be stated. It is not guessed: every star not named is outside the PSF. Accepting a list (scalar broadcast at the boundary) is a small extension if wanted. Merging this with the lensed datasets' psf_stars: (whose members ride their own tracks, not the orbit) would be the bigger, cleaner unification.
  2. ra/dec of orbit members is not linked. Linking it to the anchor (ra/dec = "the barycenter at the epoch", which is what the abs model already means) would let two members' datasets share one zero point. It is outside JDE's ruled Neighbor pm pinned only when unconstrained; orbit groups tie their stars (review 8.9.7(a)) #498 list and inside star: an orbit group ties its stars' systemic rv too; drop the starts the links made unused #501's lines, so it is not done here.
  3. New raises on previously "working" configs: a nonzero sampled fluxfrac with a multi-orbit target (the old code applied one beta to every primary orbit, wrong for KELT-4 A's planet orbit), and a companion_star_ndx outside the target's orbits. No shipped example hits either.
  4. fluxfrac's table description stays "Companion Flux Fraction" ("the target's companion in the PSF"); only the defaults.yaml comment and the docs changed.
  5. The orbit.py predicates send every non-rel mode through the new rule, including lensrel, as before. lensrel "measures no orbit" (epochs_constraining skips it), so excluding it there would be more correct, but it is a separate behavior change.

Conflicts

Rebased onto master after #501 (1c69fd8). The one conflict was orbit.md's "A star in an orbit group shares ..." paragraph. The resolution keeps #501's text (rv on the linked list, the star.rv sentence) and applies this PR's two edits to it: the wobble is from a star "in either group", and the "still primary-group only" sentence is replaced by the companion-wobble and ra/dec sentences.

Review item 8.10.6.

🤖 Generated with Claude Code

https://claude.ai/code/session_01S5pG2s9aJEo6Xqk6hWvnkW

jdeast and others added 2 commits October 27, 2026 12:03
…view 8.10.6)

_photocenter_terms summed only the orbits whose PRIMARY group held the
target and warned for the rest, so a Gaia DR4-style fit of a binary's
companion star dropped its orbit (and, since #498 tied its pm/distance
to the anchor, was silently a system-pm fit).

Every Keplerian orbit holding the target now moves its photocenter, on
either side: x_rel (b - M_c/M) from the primary group (unchanged, built
as the same expression -> bit-identical), x_rel (M_p/M - b) from the
companion group, b always "the other side's share of the PSF light".
On the SED path companion_star_ndx names that other star, placed by its
own role in every orbit (so nested targets such as KELT-4 C get BC and
A-BC); a sampled nonzero fluxfrac that cannot say where its light is
(target in two orbits, or a nested other group) raises.  The SED path's
companion must share a Keplerian orbit with the target (raises), and a
band the BC grid lacks now raises instead of silently going dark.

One membership rule, orbit.bodies.photocenter_stars, feeds the model and
every "which orbits does astrometry measure" predicate in orbit.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5pG2s9aJEo6Xqk6hWvnkW
…t (review 8.10.6)

The test pinned the old primary-only gap; its fake system gains the raw
config photocenter_stars reads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5pG2s9aJEo6Xqk6hWvnkW
@jdeast
jdeast force-pushed the feat/companion-wobble-8.10.6 branch from 5d123ba to b9d4f58 Compare October 27, 2026 16:22
@jdeast
jdeast merged commit 3b68c01 into master Oct 27, 2026
20 checks passed
@jdeast
jdeast deleted the feat/companion-wobble-8.10.6 branch October 27, 2026 22:30
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.

1 participant