Repository navigation
Add convolve_weights() to fold a point-spread function into a set of weights - #67
Merged
Merged
Conversation
… a point-spread function, into a set of weights. The convolved weights are the sparse matrix product P W, so applying them resamples and blurs in one step, and `transpose_weights_conservative()` transposes the whole operation without any change. The kernel may vary along the orthogonal axes (per wavelength, say) or across the output grid. Each run of weights sharing an input cell gathers its convolved footprint in order, so weights ordered by input come out ordered and merged, with no sort. The algorithm is shared source compiled for the host and for a CUDA device, as the clipping kernel is, and weights built on the device are convolved there and stay there. `on_device()` and the prefix sum move into `regridding._cuda` so that both device kernels can use them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GqceVSp8s2msxj1Dr9MrGU
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #67 +/- ##
==========================================
+ Coverage 99.62% 99.68% +0.06%
==========================================
Files 55 62 +7
Lines 3192 3835 +643
==========================================
+ Hits 3180 3823 +643
Misses 12 12
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
… that cannot be reached. The empty slots are only skipped on a device, where coverage cannot follow the compiled kernel, so a test runs the shared body as plain Python on weights with an empty slot on each side. A run's output cells lie inside the grid, so its footprint is never clipped away and that branch is gone. Also tests a kernel with zeros in it and both outcomes of trimming the kernel's placeholder axes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GqceVSp8s2msxj1Dr9MrGU
…el bodies and the tests. The module docstring of `_shared.py` already said the array arguments were annotated as `numpy.ndarray`; now they are. The device kernel's arrays are annotated as `Any`, as device arrays are elsewhere in the package. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GqceVSp8s2msxj1Dr9MrGU
The tests are excluded from the type check in `pyproject.toml`, and pyright skips an excluded file even when it is named on the command line, so these went unnoticed. Keyword arguments collected with `dict()` are annotated as `dict[str, Any]`, the device result of `regrid_from_weights()` as `Any`, and a triple is unpacked rather than indexed out of an object array. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GqceVSp8s2msxj1Dr9MrGU
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GqceVSp8s2msxj1Dr9MrGU
…x, check the axes, and broadcast shapes. Performance: - Each run of weights sharing an input cell now scatters through the kernel into a scratch box covering its footprint, which is then read out in order. That costs one multiply-add per weight and kernel element plus one visit per box cell, where gathering cost every weight for every cell. An 8x8 grid upsampled onto 400x400 with a 7x7 kernel goes from 1.4 s to 0.007 s on the host and from 12.9 s to 0.09 s on the device. The host keeps one growing box per thread; the device measures every run's box in a first pass and gives each a slice of one allocation. - A kernel which varies along some of the resampled axes keeps one row per cell along those axes only, instead of being copied out to one row per output cell (822 MB for a 7x7 kernel varying along x on 2048x1024). Correctness: - An `axis_output` which does not describe the weights' grid raises, rather than convolving along the wrong axes: the weights may not have more orthogonal axes than it leaves, and every output index has to lie in the grid it selects (checked on the device with a flag read once). - A kernel which varies along an orthogonal axis the weights are broadcast along now broadcasts `shape_input` and `shape_output` too, using the new `axis_input` argument, and `transpose_weights_conservative()` broadcasts the grids to weights with more orthogonal elements than they have. Device: - `_cuda.prefix_sum()` writes into memory numba owns, on the default stream, so a kernel reading the offsets after it returns cannot race torch reusing the memory. The clipping kernel shares the fix. - The device scatter skips a slot whose `-1` is on the output side, as it is in transposed device weights, instead of adding to the cell before. The device tests reuse the host tests' grids. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GqceVSp8s2msxj1Dr9MrGU
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.
Adds
regridding.convolve_weights(weights, kernel, axis_input=None, axis_output=None). It folds a convolution of the output, such as a point-spread function, into a saved set of weights.The weights
Wfromweights()map input cells onto output cells. This returns the sparse productP W, whereP[i', i] = K_i(i' - i)spreads whatever lands in output celliover its neighbors. Applying the result resamples and blurs in oneregrid_from_weights()call.transpose_weights_conservative()transposes the whole operation without any change, which is what ctis needs to model a PSF in MART: its backprojection is the transpose of the forward model.Kernel
len(axis_output)axes are the kernel itself, in the order ofaxis_output, centered atn // 2asscipy.ndimage.convolvecenters it. It must be dimensionless.optika.sensors.kernel_diffusionalready follows.Algorithm
Weights are divided into runs: consecutive weights sharing an input cell. Each run scatters its weights through the kernel into a scratch box covering its footprint, which is the bounding box of its output cells grown by the kernel. The box is then read out in order.
weights()returns them, come out ordered and with each pair once, with no sort.The algorithm is shared source compiled for the host with
numba.njitand for a CUDA device withnumba.cuda.jit, as the clipping kernel is.device="cuda"are convolved on the device and the result stays there.-1empty slots are dropped, on either side.Checks against misuse
axis_output: if it doesn't describe the grid the weights were built for, the call raises rather than convolving along the wrong axes. The weights can't have more orthogonal axes than it leaves, and every output index must lie in the grid it selects. On the device this is a flag read once at the end.axis_inputto place the input grid's orthogonal axes.transpose_weights_conservative()now broadcasts the grids to weights with more orthogonal elements than the grids have.Related device fixes
regridding._cuda.prefix_sum()(moved here from the clipping kernel, withon_device()) now writes into memory numba owns, on the default stream. A kernel reading the offsets after it returns can't race torch reusing the memory. The clipping kernel shares the fix.regrid_from_weights()now skips a slot whose-1is on the output side, as it is in transposed device weights. Before, it added zero times an input value to the cell just before its element of the output, which shows up as NaN when the input has one. There is a test for it.Verification
Tests cover:
scipy.ndimage.convolve(mode="constant")of the resampled array, for odd, even, 1×1 and zero-containing kernels.axis_outputgiven in reverse order.scipy.sparseP @ W.transpose_weights_conservativeofP Wequals the geometric transpose after correlating with the kernel.axis_outputandaxis_inputerrors.Device tests (
-m cuda) check that the device result:-1slots on either side;axis_output;A further test checks the transposed-scatter fix; it fails without the fix.
Locally:
NUMBA_DISABLE_JIT=1.Cost
Measured warm. The first call with a new dtype compiles.
Not in this PR
transpose_weights_conservative()still has no device path: it brings the values to the host. That is needed before a whole iterative inversion can stay on the device, and is left for a separate PR.🤖 Generated with Claude Code
https://claude.ai/code/session_01GqceVSp8s2msxj1Dr9MrGU