Skip to content

parameter: logit raw coordinates are flat; delete the raw-cancellation clip (review 1.2.9) - #505

Merged
jdeast merged 1 commit into
masterfrom
fix/flat-logit-raw-1.2.9
Oct 28, 2026
Merged

jdeast merged 1 commit into
masterfrom
fix/flat-logit-raw-1.2.9

Conversation

@jdeast

@jdeast jdeast commented Oct 28, 2026

Copy link
Copy Markdown
Owner

What

Review item 1.2.9 (JDE ruling 2026-10-27: do it now, verify no regression). A logit-transformed element (two finite bounds) no longer gives its raw coordinate an N(0, 1) density that section C then cancels. Its raw coordinate is flat, so its whole prior is the logit_uniform_prior.<label> potential: the sigmoid Jacobian plus the existing saturation guard. Non-logit elements keep their N(0, 1), which is their prior.

Why: the -0.5*raw**2 / +0.5*raw**2 pair added up to zero, but in float64 it left a residual of ~raw**2 * 2**-52. The DC2018_128 PTDE runaway fed on that residual. The clip at |raw| = 1e4 that bounded it put a -0.5*(raw**2 - 1e8) wall into the "uniform" prior, 1e4 whitened widths from seed 0's anchor. The whitening probe had to raise that wall (review 1.2.1). It also cost the close seeds of a multi-seed OGLE-2013-BLG-0341 start -1.5e8 nats. Making the raw coordinate flat deletes both terms. With nothing left to cancel there is no residual and nothing to clip.

Design

  • Names and layout do not change. There is still one raw vector RV per parameter, <label>_raw, untransformed, with the same initval. Trace reuse, restart files (mkparam), polish, PTDE start dicts, the persisted whitening (_whitening.json schema unchanged) and trace_meta all key on that name, so a split was ruled out.
  • A vector with any logit element is a pm.CustomDist. _raw_logp (module-level, so it pickles) is mask * (-0.5*raw**2) - ln sqrt(2 pi). The mask is a constant 0/1 vector, 1 on the non-logit elements. There is no pt.where, and a NaN still propagates on every element. A vector with no logit element keeps the plain pm.Normal.
  • -ln sqrt(2 pi) per logit element is kept on purpose. Because of it, start logp, logged lp, per-term values at raw = 0, the evidence normalization (evidence_log_norm_terms) and the nested sampler's u-space bridge are all unchanged.
  • Forward draws: random= draws N(0, 1) on every element, so .eval() on a graph that still contains its RVs behaves as before. This is documented as NOT being the prior. Nothing in exozippy samples a prior predictive.
  • Removed: _RAW_CANCELLATION_CLIP, its shared variable, get_/set_/raised_raw_cancellation_clip, whitening's _PROBE_RAW_CLIP and the probe-time raise. _CLIP_HI becomes 0.5 * _WHITEN_MAX_STEP.
  • What the wall did that was needed. The wall gave every logit element a 0.5-nat contour within the probe's reach, so an element too tight by more than the reach still escalated. Without the wall, such an element probes FLAT. A logit element cannot really be flat: its Jacobian alone costs 0.5 nats within |delta lq| ~ 1.5. So the new whitening._flat_logit_is_clipped books a flat probe of a logit element at the reach, and escalation proceeds as before. A flat non-logit element keeps NaN.

Audit (what relied on a proper raw N(0, 1) for logit elements)

  • sample_prior_predictive / pm.draw: no callers in src. Graph .eval() with RVs left in: tests/test_element_parameterization.py did this. Keeping it working is the reason for the N(0, 1) random=.
  • PyMC initial point and jitter, nutpie, numpyro, blackjax: all start from the given initval plus uniform jitter. None draws from the RV. Sampled a mixed logit/unbounded toy with pymc (cores=2), nutpie, numpyro and blackjax; all four recover both marginals. That is now a slow test.
  • ADVI: no callers.
  • PTDE start populations (ptde.py, system._jitter_*, samplers/_common.py): these are jitter proposals around centers and never use the raw density.
  • Seed polish: optimizes the compiled logp. Total logp is unchanged.
  • Whitening probe: covered by the flat booking above. Measured multipliers are bit-identical to master on kelt4, ob140939, DC2018_128 and ob161003.
  • Anchor re-centering / set_whitening invariance: still a pure change of variables. The flat raw plus the Jacobian gives a uniform prior for any anchor and scale.
  • samplers/nested.py: it uses 0.5*raw**2 + ln sqrt(2 pi) only for its own Normal-prior class, which it detects by saturation, so logit elements are unaffected. The constant is kept, so its likelihood is unchanged.
  • Evidence normalization (evidence.py, parameter.md): unchanged, because the constant is kept.
  • Branch mixtures (system._add_branch_mixtures): free-RV priors are not in the mixture. The logit potential depends only on raw and factors out, as before.
  • Per-term logp consumers (the startup table, diagnostics.point_logps, acceptance fixtures, example decomposition scripts): at raw = 0 every term is unchanged. Away from the start, -0.5*raw**2 leaves RV:<x>_raw and +0.5*raw**2 leaves POT:logit_uniform_prior.<x>, and the total stays the same. The example scripts already collapse the pair. Their comments are updated.
  • NaN-reject argument (mulensing/physics.py, mulensevent.py): still holds, because mask * NaN**2 is NaN. Pinned by a test.
  • Fold of node-degenerate labels (orbit.md, system.fold_degenerate_draws): re-measured. tc's raw-space terms now differ by only its logit Jacobian, -1.386 -> -2.604. On master they differed by -2.97e10 against a correction capped at +5e7. The docs are updated.

No runaway. Far from the anchor a logit element's logp is log q(1-q) ~ -|lq| plus the quadratic saturation guard. Both are computed directly, both are monotone past the logistic peak, and neither ever exceeds the prior's own maximum. A sampler that accepts increases has nothing to ratchet. tests/test_flat_logit_raw.py pins this out to |raw| = 1e17, plus 2000 random large excursions. test_runaway_logp_regression.py, which replays the historical DC2018_128 runaway draw, still gives a hugely negative logp, and its good-draw lp is unchanged.

Bit-identity (start logp, repr, master bfa0ea1 vs this branch)

The same sweep driver as #498/#501/#502/#503 (every top-level shipped config), plus a second point displaced +0.37 on every raw element. Table and scripts are in the session scratch; summary:

  • 37/45 bit-identical.
  • 8/45 differ by 1-2 ulp at the start: gj1214, hat3, hat3_mist_claret, hd80606, kelt17_all, kelt17_dt, kelt17_rm, wasp18. Example: gj1214 108507.98057121363 -> ...364. Traced term by term on gj1214: the only terms that move are the RV terms of all-logit vectors, by one ulp each (transit.baseline_raw -7.35150826563738 -> -7.351508265637381). The cause is that the masked quadratic constant-folds away, so the n copies of -ln sqrt(2 pi) are summed one at a time. pm.Normal's graph produced n*c. This is float summation order, not a model change.
  • At the displaced point every config agrees to <= 1.7e-15 relative. That point is inside the old clip, where the cancellation was exact up to round-off.
  • theophylline fails the same way on both sides (its data file is not shipped).

Total-logp claim: identical inside the old clip, as measured above. Past it, the old wall is gone; that is the fix.

OGLE-2013-BLG-0341 (private config, built read-only from a scratch copy, ulimit -v 12000000, serial; peak RSS 0.95 GB)

seed solution source.t_0_raw master this branch
0 Wide(+) 0 149673.19832904811 149673.19832904811
1 Wide(-) -2331 160464.93845465267 160464.93845465282
2 Close(-) 20082 -151477592.43 161311.12
3 Close(+) 20082 -151476115.42 161006.56
4 Wide*(-) -2082 159874.13587441234 159874.13587441263

The closes lose the -1.515e8 wall and land where their data fit puts them. Seeds 1 and 4 move by about 1e-10 nats. That is the old cancellation's round-off at |raw| ~ 2e3, gone.

Tests

  • New tests/test_flat_logit_raw.py, which fails on master at every point past the old clip:
    • uniform physical density out to +/-1e7 widths from the anchor;
    • a seed 2e4 widths away pays exactly the Jacobian difference (< 0.1 nat; on master, -1.5e8);
    • no runaway out to 1e17;
    • NaN propagates;
    • a mixed vector keeps N(0, 1) on its unbounded element only;
    • an all-non-logit vector is still pm.Normal;
    • (slow) all four NUTS backends sample it.
  • tests/test_whitening.py: the clip-raise test is replaced by tests for the flat-logit booking and for a non-logit NaN surviving. The ">= 3 escalation rounds" case moves from 1e-19 to 1e-28, because one round now reaches ~9 orders instead of 6. The "What flat means" table in whitening.md is re-measured; 1e-9 now resolves in one round.
  • Locally, on this box at load ~28: fast tier -m "not slow" -n6: 4513 passed, 1 skipped. Targeted slow set (mulens acceptance a/b, multiseed PTDE, ptde, ptde_async, nested, runaway regression, polish, nuts_start, node_degeneracy, whitening_anchor, evidence, mode evidence, the new file): 113 passed. CI is the gate for the rest.
  • Note for CI time: this changes the graph of every logit element, so expect one cold compile-cache run (docs/testing-cache.md).

Docs

  • parameter.md: new section "The raw coordinate of a logit element is FLAT -- do not give it back an N(0, 1), and do not re-add a clip", written as a why-it-is-this-way ruling.
  • whitening.md: the third-limit bullet is now history plus what replaces it; the flat table is re-measured.
  • orbit.md: node-degeneracy labels re-measured.
  • Comments in system.py, polish.py, run.py and mulensing/*.
  • docs/testing-cache.md.
  • The example decomposition scripts' comments.

Review item 1.2.9.

🤖 Generated with Claude Code

https://claude.ai/code/session_01S5pG2s9aJEo6Xqk6hWvnkW

…raw-cancellation clip (review 1.2.9)

Every logit (two finite bounds) element was sampled through a pm.Normal(0, 1)
raw coordinate whose -0.5*raw**2 section C cancelled with +0.5*raw**2,
clipped at |raw| = 1e4 since the DC2018_128 PTDE runaway.  The clip put a
-0.5*(raw**2 - 1e8) wall in the "uniform" prior at 1e4 whitened widths from
seed 0's anchor (a multi-seed microlensing start paid -1.5e8 nats at its
close seeds) and had to be raised during the whitening probe (review 1.2.1).

Stop cancelling.  The raw RV keeps its name `<label>_raw` and one vector per
parameter, but a vector with any logit element is now a pm.CustomDist whose
density is -0.5*raw**2 on the non-logit elements only (a constant 0/1 mask,
no pt.where) and -ln sqrt(2 pi) on every element -- kept so start logp,
logged lp, per-term values at the start, the evidence normalization and the
nested bridge are unchanged.  Section C's potential is now the Jacobian plus
the saturation guard, nothing else.  With no large term to cancel there is
no float64 residual for PTDE to ratchet, so there is nothing to clip.

Removed: _RAW_CANCELLATION_CLIP, its shared variable, get/set/raised_
raw_cancellation_clip, whitening's _PROBE_RAW_CLIP and the probe-time raise;
_CLIP_HI is now 0.5 * _WHITEN_MAX_STEP.  The one thing the wall did that was
needed (a contour within reach for a logit element too tight by more than one
round) is replaced by _flat_logit_is_clipped: a logit element cannot be flat,
so a flat probe of one is booked at the reach and escalates.

Docs: parameter.md (new ruling section), whitening.md (third-limit bullet,
the "What flat means" table re-measured), orbit.md (node-degeneracy labels,
re-measured), system.py/polish.py/run.py comments, testing-cache.md.
Tests: tests/test_flat_logit_raw.py (uniform out to 1e7 widths, a seed 2e4
widths away pays only the Jacobian, no runaway out to 1e17, NaN still
propagates, mixed vectors, all four NUTS backends sample it);
test_whitening.py's clip test replaced by the flat-booking tests.

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