Skip to content

mulensing: force a second limb-darkening annulus in VBM (workaround for a false convergence) - #504

Merged
jdeast merged 2 commits into
masterfrom
fix/vbm-minannuli
Oct 28, 2026
Merged

jdeast merged 2 commits into
masterfrom
fix/vbm-minannuli

Conversation

@jdeast

@jdeast jdeast commented Oct 28, 2026

Copy link
Copy Markdown
Owner

What

A workaround for a VBMicrolensing false convergence, pending an upstream fix. Every VBMDirectMagOp that carries limb darkening now sets vbm.minannuli = 2. A uniform-source Op keeps VBM's default of 1.

The bug (VBM 5.6.1 and current dev)

VBM integrates a limb-darkened source in annuli (ESPLMagDark, BinaryMagDark, MultiMagDark). After the first annulus its only error estimate is |(A_uniform(rho) - A_point(center)) (f(0) - f(1)) / 4|. When the uniform-disk and center point-source magnifications happen to agree, which is common near a caustic, the estimate is ~0 and the refinement loop never starts. With the default minannuli = 1, the call then returns the uniform-source magnification with therr < Tol and no warning.

Reproducer: a three-lens geometry from a fit of OGLE-2013-BLG-0341 (Gould et al. 2014) near the central caustic, linear LD a1 = 0.754, Tol = 1e-3, RelTol = 0. MultiMag2 returns 170.1815, which is the uniform value. The correct value is 174.6366 with minannuli = 2 (174.6367 at Tol = 1e-5), so the default answer is 2.55% low. On an ~8900-epoch light curve of that event, one such epoch moved chi2 by 28.

Changes

  • op.py _build_vbm: if self.bandpass is not None: vbm.minannuli = 2. A uniform source is exact in one annulus, and BinaryMag2/MultiMag2 send uniform sources through the same integrator, so forcing a second annulus there would cost ~1.9x for a change below 1e-5.
  • mulensing.md: a new section explaining why the setting is there and when to remove it (once VBM's own default guards it; name that version there).
  • tests/test_vbm_minannuli.py (~1 s): runs the reproducer geometry through the Op's own _magnify and checks it returns 174.6366, not 170.1815. A precondition check asserts that a raw default VBM still returns the uniform value. Once upstream fixes the bug that check fails, and its message says the workaround can come out.
  • Tol/RelTol and the VBM pin are unchanged.

What moved

Acceptance fixtures were re-replayed at each stored start with minannuli 1 and 2 on one machine. Every start is bit-identical, and the only term that moves is RV:mulensinstrument.model:

fixture Delta logp (nats) max per-epoch dA/A error vs a Tol = 1e-5 reference, minannuli 1 -> 2
ob09020 +0.2534 2.7e-5 3.8e-5 -> 2.9e-5
OGLE_0383LD -0.0526 1.8e-4 (A ~ 1.1-1.2) 2.6e-4 -> 2.0e-4
ob07224 (ESPL) -0.0155 8.6e-6 1.2e-5 -> 5.2e-6
ob161003 -0.0061
ob170114 +0.0051
KMT-2019-BLG-1806 (x3 configs) +0.0003
DC2018_128 x3, ob08092, ob140939 x2 0 (no LD finite-source VBM call)

None of these fixtures contains a falsely converged epoch. The moves are ordinary Tol-level changes toward the reference. ob09020 and OGLE_0383LD exceeded the physics tolerance. All eight moved fixtures were re-recorded so the strict tier stays bit-exact. The re-record also absorbed two drifts that master already had (identical at minannuli 1), both itemized in tests/mulens_acceptance_replay.py.

Three kinds of test compare against a kernel that runs VBM's default minannuli = 1. They now set the direct Op back to 1 for the comparison:

  • the MulensModel A/B parity tests (test_vbm_direct_vs_mulensmodel.py, two in test_lens_orbital_motion.py). MulensModel's own VBM instance runs the default, and these tests check the plumbing against an identical kernel at rtol 1e-8; the second annulus moves A by ~2e-5 there.
  • test_ob161003_collapse_reconciles_against_the_presplit_recording. The stage-0 recording ran the default, and the test already switches later physics off the same way (1.10.13).

Remove these together with the workaround.

Tests

  • tests/test_vbm_minannuli.py passes.
  • The two acceptance files pass after the re-record, including the ob161003 reconciliation.
  • 11 microlensing Op/LD/orbital-motion/astrometry files pass.
  • The pre-push fast tier passed. CI is the gate.

🤖 Generated with Claude Code

https://claude.ai/code/session_01S5pG2s9aJEo6Xqk6hWvnkW

jdeast and others added 2 commits October 27, 2026 21:33
VBMicrolensing's limb-darkened Mag2 calls (ESPLMagDark, BinaryMagDark,
MultiMagDark) can accept the first annulus as converged when the
uniform-disk magnification happens to equal the center point-source
magnification, returning the UNIFORM-source value with no warning: 2.55%
low on a triple-lens point of OGLE-2013-BLG-0341 at the default
Tol = 1e-3 (170.1815 against 174.6366).  Set vbm.minannuli = 2 on every
VBMDirectMagOp that carries limb darkening; a uniform-source Op keeps
VBM's default, since one annulus is exact there and a second nearly
doubles its cost.  Workaround pending upstream; mulensing.md says when to
remove it.  Tol/RelTol unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5pG2s9aJEo6Xqk6hWvnkW
…efault annuli

The forced second limb-darkening annulus moves only
RV:mulensinstrument.model at each stored start (starts bit-identical):
ob09020 +0.2534 nats, OGLE_0383LD -0.0526, ob07224 -0.0155, ob161003
-0.0061, ob170114 +0.0051, KMT-2019-BLG-1806 x3 +0.0003 -- ordinary
Tol-level moves toward a Tol = 1e-5 reference, no false convergence among
them.  Explained in mulens_acceptance_replay.py.

The MulensModel A/B parity tests and the ob161003 pre-split
reconciliation compare against a kernel that runs VBM's default
minannuli = 1 (MulensModel's own instance; the stage-0 recording), so
they set the direct Op back to 1 for the comparison.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5pG2s9aJEo6Xqk6hWvnkW
@jdeast
jdeast merged commit 127d893 into master Oct 28, 2026
20 checks passed
@jdeast
jdeast deleted the fix/vbm-minannuli branch October 28, 2026 05:18
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