Skip to content

fix: exclude zero-weight Delaunay mappings from adaptive pixel signals (NumPy/JAX log-det parity) - #631

Merged
Jammy2211 merged 2 commits into
mainfrom
feature/adaptsplit-logdet-backend-parity
Oct 15, 2026
Merged

Jammy2211 merged 2 commits into
mainfrom
feature/adaptsplit-logdet-backend-parity

Conversation

@Jammy2211

Copy link
Copy Markdown
Collaborator

Summary

Before this change, NumPy and JAX gave different AdaptSplit regularization matrices H, and so different log_det_regularization_matrix_term values, from identical inputs. On the Aris 125×125 interferometer setup the log-det came out as 2443.22 on NumPy and 2561.98 on JAX.

Cause. The log-det code is not at fault. mapper_util.adaptive_pixel_signals_from divides each pixel's summed adapt signal by a count, and that count included mapping entries with zero weight.

  • A data point lying exactly on a Delaunay vertex has barycentric weights (0, 0, 1) in any triangle around that vertex.
  • A point on an edge has a zero weight in either neighbouring triangle.
  • scipy's find_simplex and the JAX Delaunay walk legitimately pick different triangles for such points, so each backend counted different phantom neighbours.
  • That changed the pixel signals, then the AdaptSplit weights, then H.

Fix (scoped to Delaunay tables).

  • adaptive_pixel_signals_from gains exclude_zero_weight_mappings: bool = False. When it is True, zero-weight entries are dropped from both the summed signal and pixel_counts.
  • The flag is a class attribute on the interpolators, and Mapper.pixel_signals_from passes it through:
    • AbstractInterpolator: False. This also covers the rectangular interpolators and the mocks.
    • InterpolatorDelaunay: True. InterpolatorDelaunayNN inherits True.
    • InterpolatorKNearestNeighbor: False. InterpolatorKNNBarycentric inherits False.
  • The flag is a static Python bool, so the function stays jit-safe and differentiable. The log-det code is unchanged.

Closes #627

Decision taken

The human made this decision; the agent did not. The first version of the branch excluded zero-weight entries for every interpolator. The independent review then found that KNNBarycentric clips negative barycentric weights to exactly 0 for every point outside its 3-nearest triangle (knn.py).

  • Those zeros cover whole regions: 48–65% of its mapping slots on the test geometry, against 0–2 of about 4,200 for Delaunay and DelaunayNN.
  • Excluding them would have changed KNN evidence broadly and added a jump in the likelihood.

The run parked with three options posted on #627. The live human chose (b) "Delaunay only (Recommended by review)" (decision). KNN and rectangular keep the previous counting.

  • Rejected alternatives:
    • (a) exclude zero weights for all interpolators;
    • (c) make the scipy and JAX triangle choice deterministic, which keeps the semantically wrong count and stays fragile across backends.
  • Revert: git revert this PR's commit.

Because a human chose the scope, this PR does not carry the decision-taken label.

API Changes

There is no signature break. One new keyword and one new class attribute were added, and there is one behaviour change, limited to Delaunay-family meshes:

  • Pixel signals, adaptive regularization weights, H and the evidence change on both backends.
  • This happens only for Delaunay and DelaunayNN meshes that have data points exactly on vertices or edges, typically Overlay or grid-subsample image meshes.
  • KNN, KNNBarycentric and rectangular meshes, and Delaunay meshes without zero-weight entries, are unchanged bit for bit. The reviewer checked this against origin/main.

See full details below.

Test Plan

  • PyAutoArray pytest test_autoarray -x -q: 2039 passed, 4 xfailed.
  • Downstream pytest -x -q, run against this branch's autoarray: PyAutoGalaxy 1357 passed; PyAutoLens 832 passed, 1 xfailed. No existing expectation changed.
  • New tests:
    • scipy and JAX-walk Delaunay tables give identical pixel signals on a vertex-coincident grid. Before the fix they differed by up to 0.10.
    • A NumPy check that a vertex point described through two triangles gives identical signals.
    • AdaptSplit H parity, at rtol 1e-12.
    • NumPy and JAX log-dets agree with each other, and each is within 1e-6 of a 50-digit mpmath reference. This test fails with the Delaunay flag forced off.
    • With the default flag, the util still counts zero-weight entries; checked against hand-computed values.
    • KNNBarycentric mapper signals equal the default (counting) util path, on a table that contains clipped zeros.
    • Delaunay and DelaunayNN mappers route the exclusion.
  • Smoke: pyauto-heart smoke autogalaxy autolens autolens_test euclid howtolens against this branch: 151 passed, 0 failed (rc 0; covers adapt-pixelization and interferometer scripts). The earlier global-scope version also passed 151/151.
  • Black-clean on the changed files.

Witness: "numba and JAX log_det_regularization_matrix_term agree to <1e-3 on the Aris setup, with a unit test pinning the value on an ill-conditioned AdaptSplit matrix".

  • PASS on the committed synthetic test. It is an ill-conditioned AdaptSplit Delaunay matrix, with cond(H) ≈ 2.2e9:
    • H parity is 3.6e-15.
    • The two log-dets differ by 2.1e-9.
    • Both are within 1.7e-9 of a 50-digit mpmath reference.
  • Aris setup: not re-run on this branch. The investigation's simulated fix on the Aris 125×125 dump gave a log-det of 2468.446318 on both backends (Δ4e-8), but the scratchpad data was deleted afterwards to free disk.
    • The Aris mesh is a Delaunay grid-subsample, so the Delaunay-only scope covers it.
    • In that simulation, 12 of 20 prior draws agreed to ≤1e-6 and 2 more to <1e-3.
    • The 6 draws with high regularization coefficients disagreed by 5e-3 to 2.2. The cause is the absolute 1e-8 jitter, which is out of scope and filed as a scope note on fix: AdaptSplit regularization log-det parity between numba and JAX #627.

Reviewer observations (non-blocking):

  • Only exact zeros are excluded. A point that lies on an edge only up to rounding gets a tiny non-zero weight, so it can still differ between backends. Vertex coincidence, the measured Aris case, is exact.
  • Points in a degenerate triangle (area ≤ 1e-12) get all-zero weights. They now add 0 to the counts instead of 3. This is rare and harmless.

Validation checklist (--auto run — plan was not pre-approved)

  • Effective level: supervised (header: supervised, Consequence: glance, Difficulty: large). The scope decision was made by the live human mid-run, after the review FINDINGS park.
  • Plan: on the issue (fix: AdaptSplit regularization log-det parity between numba and JAX #627). The original plan was amended by the "Root cause found" comment, then scoped by the human's option (b).
  • Gate:
    • Tests: PyAutoArray 2039 passed, 4 xfailed; PyAutoGalaxy 1357; PyAutoLens 832, 1 xfailed.
    • Smoke: 151/151 (autogalaxy, autolens, autolens_test, euclid, howtolens).
    • Review: CLEAN from an independent Opus reader on head f052e38, with one disposition per lifted claim. The earlier global version got FINDINGS, which the human's scoping resolved.
    • Heart: RED (release validation FAILED (stage integrate)), shipped under the development override below.
  • Human: plan sound in hindsight?
  • Human: diff matches plan (no scope creep)?
  • Human: merge, amend, or reject — then log the outcome

Heart RED development override

Heart was RED at launch (2026-10-15 evening). The reasons recorded on #626/#627 were:

  • Heart: RED — Release RED; monitoring RED · 10/100 · 415 unresolved
  • [red] release validation FAILED (stage integrate)
  • [red] PyAutoHands/workflows/Python Version Matrix: failure
  • [red] PyAutoHeart/workflows/Release Integrate: failure
  • [red] PyAutoMind/workflows/Lifecycle Drift: failure
  • [red] PyAutoArray/Tests: red
  • [red] autofit_workspace_test/Smoke Tests: red
  • [red] autogalaxy_workspace_test/Smoke Tests: red
  • [red] autolens_workspace_test/Smoke Tests: red
  • [red] Release validation: NOT release_ready
  • [red] Worktree drift
  • [red] autolens_inference: red
  • [red] autolens_profiling: red
  • [red] euclid_strong_lens_modeling_pipeline: red

At ship time the reason was release validation FAILED (stage integrate).

The live human authorized a development-only override in session 3e3dea38 (AskUserQuestion: "Yes, override for development (Recommended)").

  • It permits commit, push and PR-open only.
  • Merge is a separate human act (/prm). Heart stays RED for release purposes.

Campaign

Logged under the PyAutoPulse interferometer campaign, PyAutoLabs/PyAutoPulse#62, task interferometer_large_mask_aris. Record the before/after witness there at close-out.

Full API Changes (for automation & release notes)

Added

  • autoarray.inversion.mappers.mapper_util.adaptive_pixel_signals_from(..., exclude_zero_weight_mappings: bool = False, xp=np): if True, mapping entries with weight exactly 0 are not counted toward a pixel's mapped-data count.
  • AbstractInterpolator.exclude_zero_weight_mappings class attribute: False by default, True on InterpolatorDelaunay (and so on InterpolatorDelaunayNN), and False again on InterpolatorKNearestNeighbor (and so on InterpolatorKNNBarycentric).

Changed Behaviour

  • Mapper.pixel_signals_from now passes self.interpolator.exclude_zero_weight_mappings. For Delaunay and DelaunayNN meshes with data points exactly on vertices or edges, pixel signals change, and so do adaptive regularization (AdaptSplit / Adapt…) matrices and Bayesian evidence. The change is the same on NumPy and JAX, and both backends now agree.

Migration

  • None. Callers of adaptive_pixel_signals_from keep the previous result by default.

Generated by the PyAutoLabs agent workflow.

🤖 Generated with Claude Code

adaptive_pixel_signals_from normalised each pixel's summed adapt signal by
a count that included mapping entries with zero weight. For Delaunay tables
those entries are not unique. A point lying exactly on a Delaunay vertex has
barycentric weights (0, 0, 1) in any triangle around that vertex. A point on
an edge has a zero weight in either neighbouring triangle. scipy's
find_simplex and the JAX Delaunay walk legitimately pick different
triangles, so the two backends counted different phantom neighbours. That
gave different pixel signals, AdaptSplit regularization weights and
regularization matrices H from identical inputs (PyAutoArray#627). On the
125x125 Aris setup this was the whole 118.76 log det H gap between NumPy and
JAX. That setup is a stride-2 grid-subsample mesh with 1,375
vertex-coincident data points. The log-det factorizations agree with a
50-digit reference to <=2e-8 on each H.

adaptive_pixel_signals_from gains a keyword exclude_zero_weight_mappings
(default False, which keeps the previous result exactly). When True, the
validity mask also requires a non-zero weight, so zero-weight entries add
to neither the summed signal nor pixel_counts. The set of non-zero (index,
weight) pairs is unique, so the result no longer depends on which simplex
was chosen. The flag is a static Python bool, so the function stays
jit-safe and differentiable under JAX.

Scope: Delaunay tables only. The live human chose this on the issue
("Delaunay only (Recommended by review)"). The flag is the class attribute
AbstractInterpolator.exclude_zero_weight_mappings:
- False by default, which covers the rectangular interpolators.
- True on InterpolatorDelaunay, inherited by InterpolatorDelaunayNN.
- False again on InterpolatorKNearestNeighbor and so on KNNBarycentric.
Mapper.pixel_signals_from passes the attribute through.

KNN is excluded because KNNBarycentric clips negative barycentric weights to
exactly zero for every data point outside its 3-nearest triangle. Those
zeros cover whole regions, not rare coincidences. Excluding them would
change KNN pixel signals broadly and would make the count, and so the
likelihood, jump wherever a clipped weight reaches zero. On the 16x16 test
geometry, 48% (vertex-coincident mesh) to 65% (jittered mesh) of
KNNBarycentric's mapping slots are exact zeros. Delaunay and DelaunayNN have
0-2 such slots out of about 4,200.

Behaviour change: pixel signals, adaptive regularization weights, H and
the Bayesian evidence change on both backends, but only for Delaunay and
DelaunayNN meshes that have data points exactly on vertices or edges. These
are typically Overlay or grid-subsample image meshes. KNN, KNNBarycentric
and rectangular meshes are unchanged bit for bit. So are Delaunay meshes
without zero-weight entries.

Tests:
- scipy and JAX-walk Delaunay tables give identical pixel signals on a
  vertex-coincident grid. Without the fix they differed by up to 0.10.
- A NumPy check that one vertex point described through two triangles
  gives identical signals.
- AdaptSplit H parity at rtol 1e-12, and NumPy and JAX log det H agreeing
  with each other and with a high-precision reference to <1e-6. With the
  Delaunay flag forced off, the H check fails.
- The default flag still counts zero-weight entries, checked against
  hand-computed values.
- KNNBarycentric mapper signals equal the default (counting) util path,
  on a table that has clipped zero weights.
- Delaunay and DelaunayNN mappers route the exclusion.

Closes #627

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The KNN interpolator computes its weights with JAX, so the test added for
PyAutoArray#627 failed with ModuleNotFoundError on the unittest-nojax leg
(run 38795253065). importorskip("jax") matches the file's other JAX tests.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@Jammy2211
Jammy2211 merged commit 0377eea into main Oct 15, 2026
3 checks passed
@Jammy2211
Jammy2211 deleted the feature/adaptsplit-logdet-backend-parity branch October 15, 2026 19:55
@Jammy2211 Jammy2211 removed the pending-release PR queued for the next release build label Oct 16, 2026
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.

fix: AdaptSplit regularization log-det parity between numba and JAX

1 participant