Repository navigation
perf: skip Sibson row padding in adaptive signals and unique mappings (DelaunayNN imaging) - #637
Merged
Merged
Conversation
… (DelaunayNN imaging) DelaunayNN (Sibson) mapping tables are SIBSON_MAX_NEIGHBORS (32) columns wide but hold at most ~15 live entries per row. On the NumPy path: - adaptive_pixel_signals_from: the np.add.at scatter over every padded slot is replaced by a numba kernel (mapper_numba_util.adaptive_pixel_signal_sums_from) visiting only the first pix_sizes[i] entries of every row, with a vectorised np.bincount fallback over the compacted live entries when numba is absent or disabled. Both are bit-identical to the padded scatter (same row-major summation order, same products, skipped entries only ever reached the discarded out-of-bounds slot). The JAX branch is unchanged. - data_slim_to_pixelization_unique_from: reset only the pix_check entries the previous data pixel touched instead of all pix_pixels per data pixel; the mapper passes its tables with np.asarray (no 32-wide copies). Outputs and shapes unchanged, bit-identical. - Sibson: a failed row (overflow / degenerate) with no selected entry used to come out at size 0, so every consumer skipped its NaN weights and the evidence was finite and wrong. Reference, numba kernel and JAX path now keep such a row at size 1 mapped to the nearest seed vertex, so NaN propagates. Tests: bit-parity of both functions against the padded / full-reset algorithms on 32-wide DelaunayNN-shaped, 3-wide Delaunay and variable-width tables and on real Delaunay and DelaunayNN mappers (both NumPy routes), JAX vs NumPy on wide rows, NaN-in-padding ignored, and the size-0 failed-row case (constructed by overflowing the Watson arithmetic) for the reference, numba and JAX Sibson paths plus its NaN reaching signals and unique weights. Closes #629 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QBeyBLN7ntDChx9v1fmt6M
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
DelaunayNN (Sibson) mapping tables are
SIBSON_MAX_NEIGHBORS= 32 columns wide but hold at most ~13–15 live entries per row. On the NumPy/numba path two steps worked over every padded slot. This PR makes them visit only thepix_sizes[i]live entries and fixes a pre-existing Sibson edge case where a failed row could silently lose its NaN.adaptive_pixel_signals_from(NumPy branch): thenp.add.atscatter over all 32 slots is replaced by a numba kernel,mapper_numba_util.adaptive_pixel_signal_sums_from, that loops only over live entries. When numba is absent ornumba: use_numba: false, it falls back tonp.bincountover the trimmed, compacted live entries. Both are bit-identical to the padded scatter: the same products are summed in the same row-major order from the same0.0, and every skipped entry only ever reached the discarded out-of-bounds slot. The JAX branch is unchanged (it was moved into_pixel_signal_sums_jax).data_slim_to_pixelization_unique_from: it now resets only thepix_checkentries the previous data pixel touched, instead of allpix_pixelsper data pixel, and the mapper passes its tables withnp.asarrayso the 32-wide tables are no longer copied. Outputs and shapes are unchanged and bit-identical.size ≥ 1(reference, numba kernel and JAX path). An overflow or degenerate row with no selected entry used to come out atsize == 0. Every consumer reads only the firstsizeentries, so its NaN weights were skipped and the evidence came out finite and wrong. Such a row is now size 1, mapped to the nearest seed vertex with a NaN weight, so the NaN reaches the curvature matrix, the regularization and the evidence. This deliberately changes behaviour from a finite, wrong evidence to NaN, so the sample is rejected.Correction to the issue's profile: the 0.30 s attributed to
unique_mappingswas mostly the Sibson interpolation, which runs lazily the first timeunique_mappingsreads the mapper's tables. The numba unique-mappings kernel itself took ~22 ms at sub 4.Timing (RAL)
RAL acceptance timing — job 412525,
euclid-ral-compute-10-4,--partition=ral --exclusive, threads=1 (NUMBA/OMP/OPENBLAS/MKL=1),use_jax=False, load 0.4–1.0 (the benchmark process itself). Private checkout (/mnt/ral/jnightin/scratch/dnn_imaging_20261017, shared mirror untouched): PyAutoNerves/Fit/Galaxy/Lens at2026.10.17.1(7f3055b / 4b183e22 / 35100def / c90eceb0); PyAutoArray A =e59d5fa9(2026.10.17.1 = main), B =58a044eb(this PR). HST-scale simulated imaging: 150×150 @ 0.05″, r=3.5″ mask (15,380 pixels), 11×11 PSF, Hilbert 1000 + 30 edge = 1030 vertices, AdaptSplit, sparse operator →InversionImagingSparseNumba. Order A-B-B-A × 2 blocks per case (4 processes per variant), 10 timedlog_likelihood_functioncalls per process after 2 warm-up calls (compile excluded); value = median over processes of per-process medians; step times = wrapped-module medians.34898.34096822383)34840.82970999481)34891.78161640761)34833.08741739975)Spread within each variant ≤ ±2 % (sub 4 NN B 0.380–0.396 s). Table width 32, live entries ≤ 13 (mean 7.2) per DelaunayNN row.
Against the acceptance line: the padded-row overhead is gone (adaptive signals 24× faster, now ~1.6 % of a DelaunayNN likelihood), but DelaunayNN is still 2.20× Delaunay at sub 4, so the draft's ~1.5× is not met (the issue plan's revised ≈2× target is close but also slightly above). What remains is genuine work, not padding: the Sibson interpolation itself (~0.16 s at threads=1, ~41 % of the call) and the curvature build over the wider unique-mapping rows (up to 43 unique pixels per data pixel vs 27 for Delaunay). Neither is in this PR's scope.
API Changes
None to the public API. Behaviour change: a failed Sibson row (overflow or degenerate) with no selected natural neighbour now has
size == 1and a NaN weight, where it used to havesize == 0. A model hitting it now gets a NaN evidence (rejected) instead of a finite, wrong one. The Delaunay and DelaunayNN likelihoods are otherwise bit-identical.See full details below.
Test Plan
test_autoarray/inversion/pixelization/mappers/test_mapper_util_wide_rows.pychecks bit-parity of both functions against the padded scatter and the full-reset algorithm. It covers 32-wide DelaunayNN-shaped, 3-wide Delaunay and variable-width tables, both NumPy routes (numba kernel and bincount fallback) and realDelaunay/DelaunayNNmappers (the Delaunay-unchanged invariant). It also checks JAX against NumPy on wide rows, that NaN in padding only is still ignored, and that the NaN of a size-1 failed row reaches the signals and the unique weights (and was dropped at size 0).test_autoarray/inversion/pixelization/interpolator/test_sibson_failed_rows.pybuilds a size-0 failed row by overflowing the Watson arithmetic (tables at 1e150 scale) and checks it for the reference, numba (exact parity) and JAX Sibson paths.main.find_specreturns None):test_autoarray/inversion/pixelization/256 passed / 20 skipped / 3 xfailed.Heart at ship time: STALE, with one reason: "release validation incomplete: no rehearsal for current source". It is not RED. Merge mode: human
/prm(tier judge).Full API Changes (for automation & release notes)
Added
autoarray.inversion.mappers.mapper_numba_util.adaptive_pixel_signal_sums_from(pixels, pixel_weights, pix_indexes_for_sub_slim_index, pix_size_for_sub_slim_index, slim_index_for_sub_slim_index, adapt_data, exclude_zero_weight_mappings): numba per-pixel signal sums and counts over live entries only (internal helper).mapper_util:_use_numba,_pixel_signal_sums_jax,_pixel_signal_sums_numpy.Changed Behaviour
sibson._sibson_single_from_tables/_sibson_numba._sibson_from_tables_parallel: a failed row with no selected entry now hassize == 1,mappings[0]set to the nearest seed vertex and a NaN weight (previouslysize == 0).Closes #629
🤖 Generated with Claude Code
https://claude.ai/code/session_01QBeyBLN7ntDChx9v1fmt6M