You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Reinstate a numba CPU curvature path for the interferometer inversion: a new package autoarray/inversion/inversion/interferometer_numba/ mirroring the live imaging_numba/, holding one kernel — the extent-grid direct convolution prototyped as direct_conv in autolens_profiling/scripts/misc/numba_interferometer/kernels.py. For each source column of A it convolves over the (Ny, Nx) unmasked-extent rectangle with contiguous W~ preload rows, then projects with A^T; cost O(nnz*M + S*nnz).
Do not reinstate the deleted kernel. The recovered O(N^2 P^2) pair loop beats a NumPy rfft2 convolution at only one of six measured cells; direct_conv beats it by 2.9-4.8x on the real likelihood's F row.
The path is selected, not defaulted to: it is gated on geometry (non-zeros per source column) and only ever runs when xp is np, numba imports, and the preconditions hold. Above the crossover the FFT path wins, so a blanket default would be a regression.
Scope of this issue: the PyAutoArray library phase only. The prompt also lists autolens_profiling; that is the follow-on in-situ phase — re-measuring delaunay_numba.py armed through the library dispatch rather than through the profiling pack — and it is filed separately via /intake at ship time as draft/research/autolens_profiling/interferometer_numba_library_dispatch_insitu.md. It is not done here.
Stacked on
This is stacked on #542 (perf: NumPy/scipy application path for InterferometerSparseOperator), which stores the raw real-space (2y, 2x) preload that this kernel indexes and establishes the xp=np application path this class specialises.
Branch: feature/interferometer-numba-cpu-direct-conv, cut from feature/interferometer-sparse-operator-numpy-cpu-path (not from main).
Single thread, i9-10885H, autolens 2026.8.17.1. In-situ F: mapper x mapper row, real likelihood, arms interleaved with dgemm controls:
instrument
mesh
recovered kernel
direct_conv
JAX/FFT (jit-warm)
direct_conv vs JAX
sma
Delaunay 1500
0.2475 s
0.0844 s
0.5917 s
7.01x
sma
rect 32^2
0.3967 s
0.0998 s
0.4101 s
4.11x
alma
Delaunay 1500
4.0710 s
1.2250 s
2.4297 s
1.98x
alma
rect 32^2
7.0205 s
1.4631 s
1.6110 s
1.10x
Whole evaluation, with the jit-warm F substituted into the measured non-F cost: 2.43x (sma Delaunay), 2.76x (sma rect), 1.77x (alma Delaunay), 1.08x (alma rect) faster than JAX-CPU.
The gate
The controlling variable is non-zeros per source column, nnz/S = N_pix*P/S, with nnz = mapper.pix_sizes_for_sub_slim_index.sum() and S = mapper.params. Measured crossover against a NumPy rfft2 convolution: ~60 (Delaunay) to ~77 (rectangular). Below it the numba kernel wins (up to 5.93x); above it the FFT wins — by 1.6x at alma_high Delaunay and 3.7x at alma_high rectangular. The default cap is therefore 60.0, the conservative of the two, and the constant is machine-dependent and documented as such.
New package autoarray/inversion/inversion/interferometer_numba/ mirroring imaging_numba/: a util module holding the direct_conv kernel (plus its lazily-built prange sibling) and its input builder, and a sparse.py with InversionInterferometerSparseNumba.
Port the kernel verbatim from the profiling prototype, but take the mask's extent_index_for_masked_pixel for the extent geometry so nothing new has to be stored on the dataset.
InversionInterferometerSparseNumba raises InversionException on every precondition it cannot honour — non-NumPy xp, missing numba, linear-function lists, more than one mapper, over-sampling — rather than working around them.
Add a Settings key for the geometry gate (interferometer_numba_nnz_per_source_max, config-backed, default 60.0, 0 disables) and dispatch on it in inversion/factory.py; preconditions failing at dispatch fall through silently to the existing sparse path, which is a routing decision, not an error.
New test package pinning F, D, reconstruction and log_evidence against InversionInterferometerSparse(xp=np) at rtol=1e-10, atol=1e-10*max|F|, with a control that a 1 % scale of F fails the pin, parallel == serial, builder parity with the prototype, factory routing, and each precondition's message.
The in-situ re-measurement on autolens_profiling is a separate follow-up prompt filed at ship time, not part of this issue.
Detailed implementation plan
Work Classification
Library
Affected Repositories
PyAutoArray (primary, and the only repo this issue touches)
Branch Survey
Repository
Current Branch
Dirty?
./PyAutoArray
main (9bd76799)
clean
Suggested branch:feature/interferometer-numba-cpu-direct-conv (cut from feature/interferometer-sparse-operator-numpy-cpu-path, PR base the same until #542 merges)
curvature_direct_conv(preload, iy, ix, flat, indptr, col, val, cscptr, csc_row, csc_val, ny, nx, pix_pixels) ported verbatim from the prototype under @numba_util.jit().
direct_conv_parallel_kernel() lazily built with numba.njit(cache=True, parallel=True, nogil=True) exactly as the prototype does (numba_util.jit cannot express parallel=True), selected when conf["general"]["numba"]["parallel"] is true — the same flag the shared decorator reads.
kernel_inputs_from(pix_indexes, pix_sizes, pix_weights, extent_index_for_masked_pixel, extent_shape, pix_pixels) — the prototype's builder, but taking the mask's extent_index_for_masked_pixel (iy = flat // nx, ix = flat % nx) instead of native_index_for_slim_index, so nothing new has to be stored on the dataset.
__init__ raises InversionException (messages ported from the prototype) on: xp is not np, missing numba, any AbstractLinearObjFuncList, more than one mapper, or over-sampling (sub_fraction != 1). Preconditions are raised, not worked around.
data_vector inherited. curvature_matrix keeps the base class's regularization / diag handling — the kernel returns a complete symmetric F, so the mirroring pass is a no-op; verify that rather than skipping it.
3. autoarray/settings.py
interferometer_numba_nnz_per_source_max: Optional[float] = None -> conf["general"]["inversion"]["interferometer_numba_nnz_per_source_max"] with a try/except KeyError fallback of 60.0 (the Delaunay crossover, the conservative of the two measured); 0 disables the path. Documented on the property with the measured crossovers and the note that the constant is machine-dependent.
After the sparse-operator check: if xp is npandsettings.interferometer_numba_nnz_per_source_max > 0and the preconditions hold andnnz_per_source_column <= maxandnumba imports -> InversionInterferometerSparseNumba; otherwise InversionInterferometerSparse as today.
Preconditions failing at dispatch fall through silently to the sparse path (a routing decision, not an error); the numba class itself still raises if constructed directly with bad inputs.
Mirrors how the imaging branch already selects by operator type (SparseLinAlgImagingNumba -> InversionImagingSparseNumba); the interferometer branch has no numba case today.
5. Tests — new test_autoarray/inversion/inversion/interferometer_numba/
F, D, reconstruction, log_evidence vs InversionInterferometerSparse(xp=np) at rtol=1e-10, atol=1e-10*max|F| on the DFT Delaunay and rectangular fixtures from test_interferometer.py.
A control that a 1 % scale of F fails the pin.
Parallel kernel == serial kernel.
kernel_inputs_from matches the prototype's on a hand-built mapper.
Factory routing: gate above / below, xp=jnp never routes numba, func-list and multi-mapper fall through.
Each precondition raises with its message.
Run the existing interferometer test tree to prove the default sparse path is unchanged.
Note: the conftest nufftax-skip is path-keyed and a new interferometer_numba/ test path is not on the list, so the DFT fixtures run unskipped.
6. Workspace / in-situ phase (not this issue)
The in-situ re-measurement on autolens_profiling — delaunay_numba.py arming through the library dispatch instead of through the prototype pack — is filed as a follow-up prompt via /intake at ship time (draft/research/autolens_profiling/interferometer_numba_library_dispatch_insitu.md).
autolens_profiling/scripts/misc/numba_interferometer/kernels.py — curvature_direct_conv, _curvature_direct_conv_parallel, kernel_inputs_from; inversion.py for the precondition messages.
test_autoarray/inversion/inversion/interferometer/test_interferometer.py — Delaunay / rectangular DFT fixtures to reuse.
Verification
Parity + control + parallel == serial + routing + precondition tests green.
The default interferometer path unchanged (existing interferometer tree green).
pytest test_autoarray/inversion test_autoarray/dataset test_autoarray/operators -q green; black clean.
Reinstate a numba CPU interferometer curvature path — the extent-grid convolution, geometry-gated
Type: feature
Target: autoarray
Repos:
PyAutoArray
autolens_profiling
Themes:
numba-cpu
interferometer
likelihood-profiling
Difficulty: large
Autonomy: supervised
Priority: high
Epic: numba-interferometer-revisit
Filed: 2026-09-07
Follow-up from autolens_profiling#226 (phase 2 of numba-interferometer-revisit). The
verdict, the bake-off and the in-situ arms are in autolens_profiling/results/notes/numba_interferometer_verdict.md.
What
Add autoarray/inversion/inversion/interferometer_numba/ mirroring the live imaging_numba/ package, with one kernel: the extent-grid direct convolution prototyped
as direct_conv in autolens_profiling/scripts/misc/numba_interferometer/kernels.py. For
each source column of A, convolve it over the (Ny, Nx) unmasked-extent rectangle with
contiguous W~ preload rows, then project with Aᵀ. Cost O(nnz·M + S·nnz).
Do not reinstate the deleted kernel. The recovered O(N² P²) pair loop beats a NumPy rfft2 convolution at only one of six measured cells; direct_conv beats it by 2.9-4.8× on
the real likelihood's F row.
In-situ F: mapper×mapper row, real likelihood, arms interleaved with dgemm controls:
instrument
mesh
recovered kernel
direct_conv
JAX/FFT (jit-warm)
direct_conv vs JAX
sma
Delaunay 1500
0.2475 s
0.0844 s
0.5917 s
7.01×
sma
rect 32²
0.3967 s
0.0998 s
0.4101 s
4.11×
alma
Delaunay 1500
4.0710 s
1.2250 s
2.4297 s
1.98×
alma
rect 32²
7.0205 s
1.4631 s
1.6110 s
1.10×
Whole evaluation, with the jit-warm F substituted into the measured non-F cost:
2.43× (sma Delaunay), 2.76× (sma rect), 1.77× (alma Delaunay), 1.08× (alma rect) faster than
JAX-CPU.
The gate this needs
The controlling variable is non-zeros per source column, nnz/S = N_pix·P/S. Measured
crossover against a NumPy rfft2 convolution: ≈60 (Delaunay) to ≈77 (rectangular). Below
it the numba kernel wins (up to 5.93×); above it the FFT wins (by 1.6× at alma_high Delaunay,
3.7× at alma_high rectangular). So the path must be selected, not defaulted to — a
dispatch rule on nnz/S with the constant measured on the target machine, or an explicit
user setting with the rule documented.
Scope
interferometer_numba/inversion_interferometer_numba_util.py — the direct_conv kernel
and its prange sibling (3.5× at sma, 4.6× at alma on 8 threads; see the pool caveat).
interferometer_numba/sparse.py — InversionInterferometerSparseNumba, mirroring imaging_numba/sparse.py, dispatched from inversion/factory.py.
The mapper must emit the CSR/CSC + extent-flat layout directly (as _sparse_triplets_curvature_from already emits COO), so the kernel does not marshal its
own inputs per evaluation.
Preconditions to raise on, not work around (the profiling pack already does): linear-
function lists, multiple mappers, over_sample_size != 1, non-NumPy xp.
Parity gate: F, D, reconstruction and log evidence against InversionInterferometerSparse at rtol=1e-10 on F with atol scaled by max|F|,
plus a control that a 1 % scale fails the pin.
The real-space W~ preload must be kept on the dataset: InterferometerSparseOperator
currently stores only Khat = fft2(preload) and discards the array this kernel indexes.
Overview
Reinstate a numba CPU curvature path for the interferometer inversion: a new package
autoarray/inversion/inversion/interferometer_numba/mirroring the liveimaging_numba/, holding one kernel — the extent-grid direct convolution prototyped asdirect_convinautolens_profiling/scripts/misc/numba_interferometer/kernels.py. For each source column ofAit convolves over the(Ny, Nx)unmasked-extent rectangle with contiguousW~preload rows, then projects withA^T; costO(nnz*M + S*nnz).Do not reinstate the deleted kernel. The recovered
O(N^2 P^2)pair loop beats a NumPyrfft2convolution at only one of six measured cells;direct_convbeats it by 2.9-4.8x on the real likelihood'sFrow.The path is selected, not defaulted to: it is gated on geometry (non-zeros per source column) and only ever runs when
xp is np, numba imports, and the preconditions hold. Above the crossover the FFT path wins, so a blanket default would be a regression.Scope of this issue: the PyAutoArray library phase only. The prompt also lists
autolens_profiling; that is the follow-on in-situ phase — re-measuringdelaunay_numba.pyarmed through the library dispatch rather than through the profiling pack — and it is filed separately via/intakeat ship time asdraft/research/autolens_profiling/interferometer_numba_library_dispatch_insitu.md. It is not done here.Stacked on
This is stacked on #542 (
perf: NumPy/scipy application path for InterferometerSparseOperator), which stores the raw real-space(2y, 2x)preload that this kernel indexes and establishes thexp=npapplication path this class specialises.feature/interferometer-numba-cpu-direct-conv, cut fromfeature/interferometer-sparse-operator-numpy-cpu-path(not frommain).feature/interferometer-sparse-operator-numpy-cpu-pathuntil perf: NumPy/scipy application path for InterferometerSparseOperator (xp=np never imports JAX) #542 merges; GitHub retargets the PR tomainon that merge.Measured (from the prompt)
Single thread, i9-10885H, autolens 2026.8.17.1. In-situ
F: mapper x mapperrow, real likelihood, arms interleaved withdgemmcontrols:direct_convWhole evaluation, with the jit-warm
Fsubstituted into the measured non-Fcost: 2.43x (sma Delaunay), 2.76x (sma rect), 1.77x (alma Delaunay), 1.08x (alma rect) faster than JAX-CPU.The gate
The controlling variable is non-zeros per source column,
nnz/S = N_pix*P/S, withnnz = mapper.pix_sizes_for_sub_slim_index.sum()andS = mapper.params. Measured crossover against a NumPyrfft2convolution: ~60 (Delaunay) to ~77 (rectangular). Below it the numba kernel wins (up to 5.93x); above it the FFT wins — by 1.6x at alma_high Delaunay and 3.7x at alma_high rectangular. The default cap is therefore 60.0, the conservative of the two, and the constant is machine-dependent and documented as such.Links
PyAutoMind/complete/archive/epics/numba_interferometer_likelihood_revisit.mdautolens_profiling/results/notes/numba_interferometer_verdict.md)rfft2inapply_operator), PyAutoArray#541 (NUFFT type-1 preload)Plan
autoarray/inversion/inversion/interferometer_numba/mirroringimaging_numba/: a util module holding thedirect_convkernel (plus its lazily-builtprangesibling) and its input builder, and asparse.pywithInversionInterferometerSparseNumba.extent_index_for_masked_pixelfor the extent geometry so nothing new has to be stored on the dataset.InversionInterferometerSparseNumbaraisesInversionExceptionon every precondition it cannot honour — non-NumPyxp, missing numba, linear-function lists, more than one mapper, over-sampling — rather than working around them.Settingskey for the geometry gate (interferometer_numba_nnz_per_source_max, config-backed, default 60.0,0disables) and dispatch on it ininversion/factory.py; preconditions failing at dispatch fall through silently to the existing sparse path, which is a routing decision, not an error.F,D,reconstructionandlog_evidenceagainstInversionInterferometerSparse(xp=np)atrtol=1e-10, atol=1e-10*max|F|, with a control that a 1 % scale ofFfails the pin, parallel == serial, builder parity with the prototype, factory routing, and each precondition's message.autolens_profilingis a separate follow-up prompt filed at ship time, not part of this issue.Detailed implementation plan
Work Classification
Library
Affected Repositories
Branch Survey
9bd76799)Suggested branch:
feature/interferometer-numba-cpu-direct-conv(cut fromfeature/interferometer-sparse-operator-numpy-cpu-path, PR base the same until #542 merges)Worktree root:
~/Code/PyAutoLabs-wt/interferometer-numba-cpu-direct-conv/Implementation Steps
1.
interferometer_numba/inversion_interferometer_numba_util.pycurvature_direct_conv(preload, iy, ix, flat, indptr, col, val, cscptr, csc_row, csc_val, ny, nx, pix_pixels)ported verbatim from the prototype under@numba_util.jit().direct_conv_parallel_kernel()lazily built withnumba.njit(cache=True, parallel=True, nogil=True)exactly as the prototype does (numba_util.jitcannot expressparallel=True), selected whenconf["general"]["numba"]["parallel"]is true — the same flag the shared decorator reads.kernel_inputs_from(pix_indexes, pix_sizes, pix_weights, extent_index_for_masked_pixel, extent_shape, pix_pixels)— the prototype's builder, but taking the mask'sextent_index_for_masked_pixel(iy = flat // nx,ix = flat % nx) instead ofnative_index_for_slim_index, so nothing new has to be stored on the dataset.nnz_per_source_column_from(mapper)=pix_sizes_for_sub_slim_index.sum() / params.2.
interferometer_numba/sparse.py::InversionInterferometerSparseNumba(InversionInterferometerSparse)__init__raisesInversionException(messages ported from the prototype) on:xp is not np, missing numba, anyAbstractLinearObjFuncList, more than one mapper, or over-sampling (sub_fraction != 1). Preconditions are raised, not worked around.kernel_index_arrayscached per mapper.curvature_matrix_diagcalls the kernel withself.dataset.sparse_operator.nufft_precision_operator(the preload stored by perf: NumPy/scipy application path for InterferometerSparseOperator (xp=np never imports JAX) #542; assert(2ny, 2nx) == preload.shape).data_vectorinherited.curvature_matrixkeeps the base class's regularization / diag handling — the kernel returns a complete symmetricF, so the mirroring pass is a no-op; verify that rather than skipping it.3.
autoarray/settings.pyinterferometer_numba_nnz_per_source_max: Optional[float] = None->conf["general"]["inversion"]["interferometer_numba_nnz_per_source_max"]with atry/except KeyErrorfallback of 60.0 (the Delaunay crossover, the conservative of the two measured);0disables the path. Documented on the property with the measured crossovers and the note that the constant is machine-dependent.4.
autoarray/inversion/inversion/factory.py— interferometer branchxp is npandsettings.interferometer_numba_nnz_per_source_max > 0and the preconditions hold andnnz_per_source_column <= maxandnumbaimports ->InversionInterferometerSparseNumba; otherwiseInversionInterferometerSparseas today.SparseLinAlgImagingNumba->InversionImagingSparseNumba); the interferometer branch has no numba case today.5. Tests — new
test_autoarray/inversion/inversion/interferometer_numba/F,D,reconstruction,log_evidencevsInversionInterferometerSparse(xp=np)atrtol=1e-10, atol=1e-10*max|F|on the DFT Delaunay and rectangular fixtures fromtest_interferometer.py.Ffails the pin.kernel_inputs_frommatches the prototype's on a hand-built mapper.xp=jnpnever routes numba, func-list and multi-mapper fall through.interferometer_numba/test path is not on the list, so the DFT fixtures run unskipped.6. Workspace / in-situ phase (not this issue)
autolens_profiling—delaunay_numba.pyarming through the library dispatch instead of through the prototype pack — is filed as a follow-up prompt via/intakeat ship time (draft/research/autolens_profiling/interferometer_numba_library_dispatch_insitu.md).Key Files
autoarray/inversion/inversion/interferometer_numba/— new package (inversion_interferometer_numba_util.py,sparse.py), mirroringautoarray/inversion/inversion/imaging_numba/(two modules,@numba_util.jit()kernels +sparse.py).autoarray/inversion/inversion/factory.py— interferometer dispatch branch.autoarray/settings.py— the new gate key (None->conf["general"]["inversion"][key]withtry/except KeyError).autoarray/inversion/inversion/interferometer/sparse.py— base classInversionInterferometerSparse.autoarray/inversion/inversion/interferometer/inversion_interferometer_util.py—nufft_precision_operator(stored by perf: NumPy/scipy application path for InterferometerSparseOperator (xp=np never imports JAX) #542).autolens_profiling/scripts/misc/numba_interferometer/kernels.py—curvature_direct_conv,_curvature_direct_conv_parallel,kernel_inputs_from;inversion.pyfor the precondition messages.test_autoarray/inversion/inversion/interferometer/test_interferometer.py— Delaunay / rectangular DFT fixtures to reuse.Verification
pytest test_autoarray/inversion test_autoarray/dataset test_autoarray/operators -qgreen;blackclean.feature/interferometer-sparse-operator-numpy-cpu-pathuntil perf: NumPy/scipy application path for InterferometerSparseOperator (xp=np never imports JAX) #542 merges.Original Prompt
Click to expand starting prompt
Reinstate a numba CPU interferometer curvature path — the extent-grid convolution, geometry-gated
Type: feature
Target: autoarray
Repos:
Themes:
Difficulty: large
Autonomy: supervised
Priority: high
Epic: numba-interferometer-revisit
Filed: 2026-09-07
Follow-up from
autolens_profiling#226(phase 2 ofnumba-interferometer-revisit). Theverdict, the bake-off and the in-situ arms are in
autolens_profiling/results/notes/numba_interferometer_verdict.md.What
Add
autoarray/inversion/inversion/interferometer_numba/mirroring the liveimaging_numba/package, with one kernel: the extent-grid direct convolution prototypedas
direct_convinautolens_profiling/scripts/misc/numba_interferometer/kernels.py. Foreach source column of
A, convolve it over the(Ny, Nx)unmasked-extent rectangle withcontiguous
W~preload rows, then project withAᵀ. CostO(nnz·M + S·nnz).Do not reinstate the deleted kernel. The recovered
O(N² P²)pair loop beats a NumPyrfft2convolution at only one of six measured cells;direct_convbeats it by 2.9-4.8× onthe real likelihood's
Frow.Measured (single thread, i9-10885H, autolens 2026.8.17.1)
In-situ
F: mapper×mapperrow, real likelihood, arms interleaved withdgemmcontrols:direct_convWhole evaluation, with the jit-warm
Fsubstituted into the measured non-Fcost:2.43× (sma Delaunay), 2.76× (sma rect), 1.77× (alma Delaunay), 1.08× (alma rect) faster than
JAX-CPU.
The gate this needs
The controlling variable is non-zeros per source column,
nnz/S = N_pix·P/S. Measuredcrossover against a NumPy
rfft2convolution: ≈60 (Delaunay) to ≈77 (rectangular). Belowit the numba kernel wins (up to 5.93×); above it the FFT wins (by 1.6× at alma_high Delaunay,
3.7× at alma_high rectangular). So the path must be selected, not defaulted to — a
dispatch rule on
nnz/Swith the constant measured on the target machine, or an explicituser setting with the rule documented.
Scope
interferometer_numba/inversion_interferometer_numba_util.py— thedirect_convkerneland its
prangesibling (3.5× at sma, 4.6× at alma on 8 threads; see the pool caveat).interferometer_numba/sparse.py—InversionInterferometerSparseNumba, mirroringimaging_numba/sparse.py, dispatched frominversion/factory.py._sparse_triplets_curvature_fromalready emits COO), so the kernel does not marshal itsown inputs per evaluation.
function lists, multiple mappers,
over_sample_size != 1, non-NumPyxp.F,D, reconstruction and log evidence againstInversionInterferometerSparseatrtol=1e-10onFwithatolscaled bymax|F|,plus a control that a 1 % scale fails the pin.
W~preload must be kept on the dataset:InterferometerSparseOperatorcurrently stores only
Khat = fft2(preload)and discards the array this kernel indexes.