Skip to content

Add Adapt reconstruction for FFT, NFFT, and Toeplitz normal operators - #43

Merged
nHackel merged 7 commits into
JuliaImageRecon:mainfrom
shukebeta:issue-10-fft-nfft-adapt
Nov 1, 2026
Merged

nHackel merged 7 commits into
JuliaImageRecon:mainfrom
shukebeta:issue-10-fft-nfft-adapt

Conversation

@shukebeta

@shukebeta shukebeta commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

What this PR fixes

FFTOpImpl, NFFTOpImpl, and NFFTToeplitzNormalOp contain backend-bound plans, workspaces, and closures. Generic Adapt.adapt does not rebuild those captured resources, so an adapted operator can remain tied to the source storage/backend. Unsupported Adapt targets must not silently fall back to a host operator.

How it fixes it

This PR adds explicit Adapt.adapt_structure implementations that rebuild fresh target-side plans, workspaces, and closures while preserving operator semantics:

  • FFTOp retains the public FFT planner kwargs by default and replays them with a target-backed work array, preserving shape, shift, unitary normalization, and planner configuration.
  • NFFTOp retains the explicit backend, original node matrix, shape, and all public arguments passed to plan_nfft. Adapt transfers array-valued arguments to the target and replays the same constructor call, preserving the NFFT algorithm family, exact oversampled grid, kernel/settings, and NonuniformFFTs custom point_transform without inspecting backend-specific plan internals. The default cache=true retains a defensive copy of the node matrix and constructor arguments, which has an intentional additional memory cost; cache=false opts out of that retained state and reconstruction support.
  • NFFTToeplitzNormalOp transfers λ and weights to the target, retains its FFT planner kwargs, and rebuilds fresh FFT plans and doubled-shape workspaces there. Workspaces are allocated directly from target storage; adapted values are copied only when Adapt returns the source array itself, avoiding redundant target buffers.
  • cache=false is available on these constructors for callers who do not want to retain reconstruction arguments; adapting such an operator fails explicitly because faithful replay is unavailable.
  • Unsupported or abstract no-op targets raise ArgumentError instead of silently rebuilding on host Array; supported concrete targets, including same-backend Array, remain valid.
  • Base.copy(::FFTOpImpl) preserves an explicit cache=false opt-out while keeping its existing default-planner behavior; copy does not unexpectedly replay cached FFT planner kwargs.

No Metal capability claim is made by this PR.

Validation

The Julia 1.10 package test suite passed on the preceding head 850cf9c:

  • 707 passed, 1 explicitly broken, 708 total.
  • The single broken case is the known upstream NonuniformFFTs + JLArray CPU-kernel gap in AcceleratedKernels and is explicitly marked broken.
  • Regression tests construct CPU FFT and Toeplitz plans with non-default flags=FFTW.MEASURE, assert the cached kwargs, and assert that rebuilt FFT plans carry the requested flags.
  • The suite covers Array and JLArray target storage, FFT forward/adjoint behavior, compound operators and round trips, exact NFFT.jl grid preservation for (12, 16), m=2, σ=1.25, NFFT algorithm-family preservation, NonuniformFFTs custom point_transform with fftshift=false, Toeplitz λ/weights, source immutability, unsupported targets, and explicit cache=false behavior.

GPU CI results will be reported separately when the refreshed head completes. No Metal lane is claimed.

@shukebeta shukebeta changed the title issue 10 fft nfft adapt Add Adapt reconstruction for FFT, NFFT, and Toeplitz normal operators Oct 24, 2026
@codecov

codecov Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.33333% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 73.01%. Comparing base (31906da) to head (454257e).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
ext/LinearOperatorNFFTExt/NFFTOp.jl 91.54% 6 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #43      +/-   ##
==========================================
+ Coverage   69.23%   73.01%   +3.78%     
==========================================
  Files          15       15              
  Lines         520      593      +73     
==========================================
+ Hits          360      433      +73     
  Misses        160      160              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@nHackel nHackel left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit worried that the code for the NFFT adapt will be fairly brittle and will be need frequent changes if NFFT.jl or NonuniformFFTs.jl changes.

An alternative approach would be caching/storing the arguments which are given to the (N)FFT plan constructor call inside the operator and then replaying those with the approriate array types. We could then also have an additional kwarg argument which allows callers to disable caching in case that is performance critical. Adapt shouldn't be used in a hot loop, so probably a Dict{Symbol, Any} is fine

@shukebeta shukebeta left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed review 5664445957 in 325cb8e.

The Adapt reconstruction no longer infers state from NFFT.jl or NonuniformFFTs plan internals. FFTOp caches its FFT planner kwargs; NFFTOp caches the explicit backend, original nodes, shape, and every plan_nfft kwarg; and Toeplitz operators cache their FFT planner kwargs. Adapt transfers array-valued cached arguments to the target and replays the public constructor call, preserving exact NFFT grids and custom point transforms. cache=false opts out of retention and makes Adapt fail explicitly rather than guessing.

The PR description now documents this design. Julia 1.10.12 Pkg.test() passes with 699 passed, 1 explicitly broken, 700 total; the broken case is the known NonuniformFFTs + JLArray upstream CPU-kernel gap. The refreshed head is pushed and ready for re-review.

@shukebeta

Copy link
Copy Markdown
Contributor Author

Follow-up commit 454257e is pushed with the requested review fixes:

  • use AbstractNFFTs.active_backend() when constructing NFFT plans;
  • preserve cache=false through copy(::FFTOpImpl) without replaying cached planner kwargs, with regression coverage;
  • only copy cached NFFT array arguments when Adapt.adapt returns the original array, avoiding redundant copies;
  • move the shared Adapt target-storage validation helper into the main module;
  • document the additional memory retained by the default NFFT cache.

Validation on Julia 1.10 in Docker: the Adapt-focused suite reports 402 passed, 1 explicitly broken, 403 total, plus 5/5 explicit cache-disabled checks. The broken case is the known NonuniformFFTs + JLArray CPU-kernel gap and remains explicitly marked broken. The earlier package-suite result was 707 passed, 1 explicitly broken, 708 total.

The PR description now documents the reconstruction contract, cache tradeoff, unsupported-target behavior, and validation details for the refreshed head.

@nHackel
nHackel merged commit e34a8b0 into JuliaImageRecon:main Nov 1, 2026
6 checks passed
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.

2 participants