Split out from #92, which made this arbitrary choice deterministic without making it right.
The relations are correct; the inverse problem is not determined
The forward physics in components/mulensing/symbolic_physics.py is right:
| relation |
check |
mu_rel_mag**2 = mu_ra_rel**2 + mu_dec_rel**2 |
Pythagorean |
pi_E_N = (pi_rel/theta_E)*(mu_dec_rel/mu_rel_mag) |
pi_E || mu_rel; North<->dec |
pi_E_E = (pi_rel/theta_E)*(mu_ra_rel/mu_rel_mag) |
East<->RA |
pi_rel = KAPPA*m*(pi_E_N**2 + pi_E_E**2) |
from theta_E = sqrt(kappaMpi_rel), pi_E = pi_rel/theta_E |
Forward (physical -> observable) is single-valued and fine. Initialization runs it backwards, and there the system is underdetermined.
What the seed actually contains
For examples/DC2018_128, mmexofast.json carries only:
parameters: t_0, u_0, t_E, s, alpha, rho, q
sigmas: t_0, u_0, t_E, log_rho, log_s, log_q, alpha
No pi_E, in either fit entry. So with t_E seeded and theta_E from mass/distance, the engine knows only the magnitude mu_rel_mag = theta_E / t_E. Splitting that into (mu_ra_rel, mu_dec_rel) is one equation in two unknowns -- a circle of solutions. The pi_E vector direction is undetermined for the same reason.
What the engine does with that
It assigns the entire magnitude to whichever component it reaches first and leaves the other at (numerically) zero. Observed start values at the pinned GOOD_RAW:
Both satisfy the magnitude equation exactly. They are different physical models.
Why this matters even now that it is deterministic
- The direction is observable. With annual parallax modeled, the sky direction of mu_rel affects the light curve -- which is precisely why the two branches gave chi2/N 9.96 vs 268.93 on the same data.
- Even the good branch is a poor start. chi2/N ~ 10 at a seed meant to sit near the MMEXOFAST solution suggests the arbitrary direction is leaving real information on the table on every machine, not just the one that exposed the bug.
- Exactly zero is a bad place to start. Putting the whole magnitude in one component makes the other exactly
0, so pi_E_N = 0 begins at a cusp of the normalization rather than in the interior.
Options
- Seed the direction from the galactic model, which already computes expected proper motions (
components/galacticmodel/galacticmodel.py:364-365) -- a physically motivated split instead of "all in RA".
- Stop solving for it. Treat the mu_rel position angle as a free parameter with a prior, since the seed genuinely does not determine it.
- Have MMEXOFAST fit parallax and seed
pi_E_N/pi_E_E for events with parallax signal.
(1) looks cheapest and strictly better than the status quo; (2) is the honest representation of what is known.
Unrelated thing noticed nearby
galacticmodel.py:364 passes pm_ra_cosdec=pm_ra, i.e. it reads star.pm_ra as mu_alpha*cos(delta), while mu_ra_rel = lens_pm_ra - source_pm_ra is convention-agnostic. Self-consistent only if every consumer of star.pm_ra uses the cos(delta)-multiplied convention. Not audited -- flagging, not claiming a bug.
Reproducing
diag_mulens.py (in #92's discussion) prints versions, all resolved initvals/scales, every parameter's physical value at GOOD_RAW, and the mulens residual summary. Run it on two machines and diff.
🤖 Generated with Claude Code
Split out from #92, which made this arbitrary choice deterministic without making it right.
The relations are correct; the inverse problem is not determined
The forward physics in
components/mulensing/symbolic_physics.pyis right:mu_rel_mag**2 = mu_ra_rel**2 + mu_dec_rel**2pi_E_N = (pi_rel/theta_E)*(mu_dec_rel/mu_rel_mag)pi_E_E = (pi_rel/theta_E)*(mu_ra_rel/mu_rel_mag)pi_rel = KAPPA*m*(pi_E_N**2 + pi_E_E**2)Forward (physical -> observable) is single-valued and fine. Initialization runs it backwards, and there the system is underdetermined.
What the seed actually contains
For
examples/DC2018_128,mmexofast.jsoncarries only:No
pi_E, in either fit entry. So witht_Eseeded andtheta_Efrom mass/distance, the engine knows only the magnitudemu_rel_mag = theta_E / t_E. Splitting that into(mu_ra_rel, mu_dec_rel)is one equation in two unknowns -- a circle of solutions. Thepi_Evector direction is undetermined for the same reason.What the engine does with that
It assigns the entire magnitude to whichever component it reaches first and leaves the other at (numerically) zero. Observed start values at the pinned
GOOD_RAW:mu_ra_rel = -11.4113305,mu_dec_rel = 0.0,pi_E_N = 0.0,pi_E_E = -0.22019125mu_ra_rel = 4.7e-125Both satisfy the magnitude equation exactly. They are different physical models.
Why this matters even now that it is deterministic
0, sopi_E_N = 0begins at a cusp of the normalization rather than in the interior.Options
components/galacticmodel/galacticmodel.py:364-365) -- a physically motivated split instead of "all in RA".pi_E_N/pi_E_Efor events with parallax signal.(1) looks cheapest and strictly better than the status quo; (2) is the honest representation of what is known.
Unrelated thing noticed nearby
galacticmodel.py:364passespm_ra_cosdec=pm_ra, i.e. it readsstar.pm_raas mu_alpha*cos(delta), whilemu_ra_rel = lens_pm_ra - source_pm_rais convention-agnostic. Self-consistent only if every consumer ofstar.pm_rauses the cos(delta)-multiplied convention. Not audited -- flagging, not claiming a bug.Reproducing
diag_mulens.py(in #92's discussion) prints versions, all resolved initvals/scales, every parameter's physical value atGOOD_RAW, and the mulens residual summary. Run it on two machines and diff.🤖 Generated with Claude Code