Repository navigation
Description
Overview
DelaunayNN is ~40x slower than Delaunay on the NumPy (use_jax=False) path (Aris SMA interferometer: 18.1 s vs 0.43 s per likelihood at 1405 vertices) because the Sibson natural-neighbour interpolation in sibson_mappings_weights_from_tables is a pure-Python per-query loop (~11k queries × ~1.5 ms). This task adds a numba kernel for that path and then makes sure the numba imaging and interferometer likelihood paths actually take DelaunayNN mappers, so DelaunayNN costs about the same as Delaunay on CPU.
Heart RED override (development only) — authorised by the human on 2026-10-15 under AUTONOMY.md "Human override for Heart RED". Reasons at the time, verbatim: "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] CI wall-clock: 26 gates · slowest PyAutoBrain Nightly Release 63m · 5 hang events"; "[red] Release readiness: release validation FAILED (stage integrate)"; "[red] autofit_test: red"; "[red] Release validation: NOT release_ready — v2026.10.15.1.dev81701 profile=release (2026-10-15T07:04:34+00:00)"; "[red] validation_report/stages/integrate: fail"; "[red] PyAutoArray: red"; "[red] autolens_inference: red"; "[red] autolens_profiling: red"; "[red] Worktree drift: 5 orphan / 0 missing / 4 dirty"; "[red] euclid_strong_lens_modeling_pipeline: red". Human note: PyAutoArray tests will be handled once the epic refactor elsewhere is sorted.
Plan
Phase 1 — fast kernel
- Replace the pure-Python NumPy loop in Sibson interpolation with a parallel numba kernel that reproduces every edge case and diagnostic exactly.
- Prove it with parity tests (random meshes + constructed edge cases) against the current per-query reference.
- Re-time on the Aris setup — Sibson step from ~18 s to tens of ms.
Phase 2 — routing
- Measure which inversion class DelaunayNN mappers reach on interferometer and imaging data at both vertex counts; the key risk is Sibson's wider rows tripping the interferometer numba gate (60 nnz/source column) onto the slower FFT route.
- Fix the routing if it misses the numba path, and guard against NaN (overflow/degenerate) weights silently entering the numba kernels.
- Add DelaunayNN routing tests and likelihood parity vs the FFT/reference route; report a before/after routing + timing table for both datasets.
Tier: glance — merge mode: auto-merge on green if the Witness passes, post-merge summary. (PyAutoArray/Tests is already red on main, so this likely falls back to human /prm.) Witness checked at end of phase 2; phase 1 gates on parity + Sibson-step timing.
Detailed implementation plan
Affected Repositories
- PyAutoArray (primary)
Branch Survey
| Repository | Current Branch | Dirty? |
|---|---|---|
| ./array/PyAutoArray | main (ccddfba) | clean |
No active.md claim on PyAutoArray (worktree_check_conflict exit 0). Note: an unregistered sparse-operator-oversampling-cache PyAutoArray worktree has 11 uncommitted files and may touch factory.py / sparse code — check before phase 2.
Suggested branch: feature/delaunay-nn-numba · worktree ~/Code/PyAutoLabs-wt/delaunay-nn-numba/
Implementation Steps
Phase 1 — autoarray/inversion/mesh/interpolator/sibson.py
- New
_sibson_from_tables_numba(queries, seeds, fallbacks, points, simplices_padded, simplex_neighbors, centres, r2, valid, C, M)built lazily withnumba.njit(cache=True, parallel=True, nogil=True)+prangeover queries (pattern:interferometer_numba/inversion_interferometer_numba_util.py:109-135). Per query, fixed-size scratch: BFS cavity walk oversimplex_neighborswith the_contains_querytolerance64*eps*max(r²,1)(cap C =SIBSON_MAX_CAVITY_TRIANGLES= 32, overflow flag); Watson circumcircle contributions (≤3C); group/compact by vertex id via insertion sort (replacing argsort +np.add.at); normalise. - Keep every edge case of
_sibson_single_from_tables(L343-556): outside-mesh fallback (seed<0→[outside_fallback_index], w=1, size 1), on-vertex fallback, on-edge barycentric row (128*eps, size 3), degenerate (invalid inserted circle / non-finite or|sum|≤tiny), neighbour overflow (unique_count > M, M =SIBSON_MAX_NEIGHBORS= 32) → NaN weights; all six outputs (mappings (Q,M) int32, sizes, weights (Q,M), cavity_sizes, overflow, degenerate) unchanged. - NumPy branch of
sibson_mappings_weights_from_tables(L629-636) dispatches to the kernel when numba is importable andgeneral.yaml numba.use_numbais true; else keeps the list comprehension, retained as the reference. JAX branch untouched. - Tests: new
test_autoarray/inversion/pixelization/interpolator/test_sibson_numba.py— kernel vs reference on random meshes and constructed on-vertex / on-edge / outside-hull / degenerate / cavity-overflow cases; mappings, sizes and diagnostics identical, weights to ~1e-12. Existingtest_sibson.py(incl.scipy_delaunay_nnfixed-seed regression L231),test_delaunay_nn.py,test_numerics_audit.pystay green. - Re-time with the Aris scripts (local scratchpad, collaborator data — never committed); numbers in the PR body.
Phase 2 — routing (autoarray/inversion/inversion/factory.py)
6. Interferometer: measure nnz_per_source_column_from(mapper) for DelaunayNN on the Aris mask (1405, 5526 vertices). If > 60, time direct_conv vs FFT for DelaunayNN to locate the real crossover; retune interferometer_numba_nnz_per_source_max (settings.py:377-412, general.yaml:21) or make the gate mesh-aware based on data. kernel_inputs_from already handles variable widths (CSR). Add a NaN-weight check in _use_interferometer_numba (L242-321) or the sparse.py:97-145 preconditions.
7. Imaging: confirm InversionImagingSparseNumba (imaging_numba/sparse.py:55, chosen at factory.py:89-163) is taken with a DelaunayNN mapper and that mapper_numba_util.data_slim_to_pixelization_unique_from handles widths up to 32; time Delaunay vs DelaunayNN on HST-scale imaging.
8. Tests: DelaunayNN cases in interferometer_numba/test_interferometer_numba.py routing tests (L309-459) and imaging in test_factory.py; likelihood parity numba vs FFT/mapping route.
9. Report table: inversion class per mesh × dataset, before/after, with Delaunay vs DelaunayNN timings.
Key Files
autoarray/inversion/mesh/interpolator/sibson.py— Sibson tables, NumPy/JAX branches,scipy_delaunay_nnautoarray/numba_util.py,autoarray/config/general.yaml— numba conventions/configautoarray/inversion/inversion/factory.py,autoarray/settings.py— inversion routing + nnz gateautoarray/inversion/inversion/interferometer_numba/— direct_conv kernel +kernel_inputs_fromautoarray/inversion/inversion/imaging_numba/sparse.py— imaging numba inversion
Risks
- Kernel summation order differs from argsort grouping → weights compared to 1e-12, not bit-for-bit.
- numba compile time on first call (cached).
Original Prompt
Click to expand starting prompt
Make DelaunayNN fast on the numba/NumPy path
Type: feature
Target: PyAutoArray
Repos:
- PyAutoArray
Difficulty: large
Autonomy: supervised
Priority: normal
Memory: wiki/galaxies/sources/massive-ellipticals.md; wiki/lensing/sources/dark-matter-substructure.md; reading-queue.md
Status: formalised
Consequence: glance
Witness: on the Aris interferometer setup (1405 vertices, use_jax=False) DelaunayNN steady log_likelihood_function time <= 2x Delaunay, with numba-vs-reference Sibson parity tests green
Review-minutes: 3
Unattended: ready
DelaunayNN is ~40x slower than Delaunay on the NumPy (use_jax=False) path because the
Sibson natural-neighbour interpolation is a pure-Python per-query loop. Make it fast with
a numba kernel so DelaunayNN costs about the same as Delaunay on CPU.
Evidence (measured 2026-10-15, laptop, PyAutoArray ccddfba)
Collaborator (Aris) SMA-scale interferometer data: 5,516,930 visibilities, 5,496 masked
pixels, sparse operator applied, AdaptSplit, use_positive_only_solver=False, threads
pinned to 1. Bare analysis.log_likelihood_function steady seconds per call:
| mesh | 1405 vertices | 5526 vertices |
|---|---|---|
| Delaunay | 0.43 s | 12.7 s |
| DelaunayNN | 18.1 s | 56 s |
Profile: almost all DelaunayNN time is in sibson_mappings_weights_from_tables ->
single() (autoarray/inversion/mesh/interpolator/sibson.py ~L613-644), reached via
scipy_delaunay_nn -> mappings_weights_for (~11k calls per likelihood: data points +
split points). The NumPy branch is a Python list comprehension over queries; each
_sibson_single_from_tables call runs ~50-100 tiny NumPy ops (argsort, np.add.at,
broadcast_to, where, stack, pad...) for at most ~32 cavity triangles / ~96 contributions,
~1.5 ms per query. The JAX branch is vectorised (vmap + lax.map) and is not affected.
Scripts/logs: scratchpad .../50c8c6cd-6ac2-47c2-9290-7ee73f9c4315/scratchpad/aris/step6
and PROGRESS2.md (temporary; numbers are reproduced above).
Fix
- Add an
@njit(parallel=True /prangeover queries) kernel for the NumPy path of
sibson_mappings_weights_from_tables: cavity walk, Watson circumcircle contributions,
compaction/dedup of vertex ids into fixed-sizemax_neighborsrows, normalisation. - Reproduce every edge case of
_sibson_single_from_tablesexactly: outside-mesh
fallback (seed_simplex < 0), on-vertex fallback, on-edge barycentric row, degenerate
Watson circles and cavity/neighbour overflow -> NaN weights, plus the returned
diagnostics (cavity_sizes,overflow,degenerate). - Follow the existing numba conventions in
interpolator/delaunay.py/
rectangular.py(confignumba:block, caching). JAX path untouched.
Numba likelihood paths must use it (imaging + interferometer)
The faster interpolation alone is not enough: the numba likelihood paths must actually
take DelaunayNN mappers.
- Interferometer:
factory.pyroutes a NumPy inversion to the numbadirect_conv
curvature path (InversionInterferometerSparseNumba) only when mean non-zeros per
source column <=interferometer_numba_nnz_per_source_max(60, tuned on Delaunay).
DelaunayNN stencils carry more neighbours per data point (up to
SIBSON_MAX_NEIGHBORS= 32 vs 3), so DelaunayNN may fall through to the slower FFT
route. Measure nnz/column for DelaunayNN on the Aris mask; confirm which route it takes;
if it missesdirect_conv, make it take it (or re-measure the crossover) — and check the
kernel handles variable-size Sibson rows correctly. - Imaging: do the same audit for the numba imaging likelihood path — confirm DelaunayNN
mappers are routed to it and handled correctly, and time imaging Delaunay vs DelaunayNN. - Report, for both datasets, which inversion class each mesh is routed to before/after.
Acceptance
- Parity unit tests: numba kernel vs the current NumPy reference on random meshes and on
constructed edge cases (query on vertex, on edge, outside hull, degenerate) — mappings
and sizes identical, weights to ~1e-12. - Re-time on the Aris setup: Sibson step from ~18 s to tens of ms; DelaunayNN per-call
cost close to Delaunay (report both vertex counts). - Imaging and interferometer numba paths are taken by DelaunayNN mappers (asserted in a test
of the routing), with likelihood parity vs the FFT/reference route. - Existing DelaunayNN tests and the workspace_test parity scripts still pass.
Activity
Jammy2211 commentedon Oct 15, 2026
Library PR Created — phase 1
pending-release), commit 57e0b51stale("release validation incomplete: no rehearsal for current source"); feed RED reasons recorded verbatim in the PR body. Permits commit/push/PR-open only — merge is a separate human/prm.InversionInterferometerSparseNumba(DelaunayNN nnz/source column 17.8 @1405, 1.0 @5526, gate 60). Remaining phase 2: NaN-weight guard, DelaunayNN routing tests + likelihood parity, imaging audit + timing.Next:
/prmon #624, then phase 2.Jammy2211 commentedon Oct 15, 2026
Library PR Created — phase 2
pending-release), commit 4505daeInversionInterferometerSparseNumba(Aris 1405/5526) andInversionImagingSparseNumba(HST imaging, sub 2/4); NaN Sibson weights give NaN evidence on every route (mapping / FFT / numba). PR adds routing, parity and NaN tests + docstrings.unique_mappings/adaptive_pixel_signals_from, filed as Mind follow-updraft/feature/autoarray/delaunaynn_wide_row_imaging_overhead.md(with a pre-existing size==0 failed-row edge case from review)./prm.Next:
/prm#624, then #625.Merge pull request #624 from PyAutoLabs/feature/delaunay-nn-numba
Merge pull request #625 from PyAutoLabs/feature/delaunay-nn-numba-rou…
Jammy2211 commentedon Oct 15, 2026
Shipped
53d94b98) and test: pin DelaunayNN routing to the numba likelihood paths (#623 phase 2) #625 (DelaunayNN routing tests,a84f724c) intomain; bothpending-release.use_jax=False— DelaunayNN 0.57 s vs Delaunay 0.41 s per likelihood (≤2×), Sibson numba-vs-reference parity tests green.draft/feature/autoarray/delaunaynn_wide_row_imaging_overhead.md(imaging sub-4 wide-row overhead + pre-existing size==0 failed-row edge case)./prm; Heart remains RED for release purposes.