Skip to content

Add ContourletOperators subpackage wrapping Contourlets.jl - #46

Draft
hakkelt wants to merge 81 commits into
kul-optec:masterfrom
hakkelt:add-contourlet-operators
Draft

hakkelt wants to merge 81 commits into
kul-optec:masterfrom
hakkelt:add-contourlet-operators

Conversation

@hakkelt

@hakkelt hakkelt commented Aug 25, 2026 •

Copy link
Copy Markdown
Collaborator

Adds ContourletOperators subpackage: ContourletOp/NSCTOp wrapping Contourlets.jl (Contourlet Transform and NSCT) as LinearOperators.

@github-actions

github-actions Bot commented Aug 25, 2026 •

Copy link
Copy Markdown

Benchmark Results (Julia v1.12.7)

No significant performance or memory regressions detected.

Time benchmarks
Benchmark base head Ratio (base/head)
batching/SimpleBatchOp/adjoint-single 45.5 ± 2.4 μs 45 ± 2.3 μs 1.01 ± 0.15
batching/SimpleBatchOp/forward-single 41.7 ± 2.9 μs 40.2 ± 2.9 μs 1.04 ± 0.21
batching/SpreadingBatchOp/adjoint-single 23.4 ± 2.3 μs 24.6 ± 1.6 μs 0.951 ± 0.22
batching/SpreadingBatchOp/forward-single 20.8 ± 2.4 μs 23.3 ± 2.5 μs 0.891 ± 0.28
calculus/AffineAdd/adjoint 6.71 ± 0.4 μs 6.59 ± 0.5 μs 1.02 ± 0.2
calculus/AffineAdd/forward 20.8 ± 1.5 μs 18.8 ± 1.2 μs 1.11 ± 0.21
calculus/Ax_mul_Bx/forward 49.7 ± 0.41 μs 49.6 ± 0.4 μs 1 ± 0.023
calculus/Ax_mul_Bx/jacobian-adjoint 74.1 ± 3 μs 70 ± 1.8 μs 1.06 ± 0.1
calculus/Ax_mul_Bxt/forward 49.9 ± 0.39 μs 49.7 ± 0.65 μs 1 ± 0.031
calculus/Ax_mul_Bxt/jacobian-adjoint 72.9 ± 1.3 μs 67.6 ± 0.97 μs 1.08 ± 0.049
calculus/Axt_mul_Bx/forward 49.8 ± 0.32 μs 49.9 ± 0.71 μs 0.997 ± 0.031
calculus/Axt_mul_Bx/jacobian-adjoint 69.4 ± 1.1 μs 71 ± 1.4 μs 0.976 ± 0.048
calculus/BroadCast/identity-single 63.4 ± 3.9 μs 72.6 ± 8.5 μs 0.872 ± 0.23
calculus/BroadCast/operator-single-adjoint 1.77 ± 0.09 μs 1.83 ± 0.09 μs 0.967 ± 0.14
calculus/BroadCast/operator-single-forward 330 ± 50 ns 391 ± 40 ns 0.844 ± 0.31
calculus/Compose/adjoint 14.6 ± 1 μs 12.8 ± 0.87 μs 1.15 ± 0.22
calculus/Compose/forward 17.5 ± 1.3 μs 15.7 ± 0.94 μs 1.11 ± 0.22
calculus/DCAT/adjoint 27.7 ± 1.8 μs 24.7 ± 0.69 μs 1.12 ± 0.16
calculus/DCAT/forward 28.8 ± 1.9 μs 26.2 ± 0.72 μs 1.1 ± 0.16
calculus/HCAT/adjoint 27.8 ± 1.7 μs 27.6 ± 1.6 μs 1.01 ± 0.17
calculus/HCAT/forward 41.2 ± 2.5 μs 43.2 ± 1.9 μs 0.955 ± 0.14
calculus/HadamardProd/forward 822 ± 4.2 μs 815 ± 5.1 μs 1.01 ± 0.016
calculus/HadamardProd/jacobian-adjoint 871 ± 7.8 μs 879 ± 7.6 μs 0.992 ± 0.025
calculus/Jacobian/sigmoid-adjoint 162 ± 3.5 μs 161 ± 2.8 μs 1.01 ± 0.056
calculus/Reshape/adjoint 6.41 ± 0.17 μs 6.55 ± 0.32 μs 0.979 ± 0.11
calculus/Reshape/forward 9.69 ± 0.64 μs 9.44 ± 0.54 μs 1.03 ± 0.18
calculus/Scale/adjoint 9.84 ± 0.36 μs 10.1 ± 0.51 μs 0.97 ± 0.12
calculus/Scale/forward 13.4 ± 0.58 μs 14.7 ± 1 μs 0.912 ± 0.15
calculus/Sum/adjoint 33.8 ± 2.4 μs 35.6 ± 3.3 μs 0.947 ± 0.22
calculus/Sum/forward 36.3 ± 2.8 μs 38.2 ± 3.3 μs 0.951 ± 0.22
calculus/VCAT/adjoint 36.7 ± 2.9 μs 39.1 ± 2.8 μs 0.94 ± 0.2
calculus/VCAT/forward 28 ± 1.7 μs 25.8 ± 0.77 μs 1.08 ± 0.15
contourletoperators/ContourletOp/adjoint — 3.38 ± 0.052 ms —
contourletoperators/ContourletOp/forward — 3.66 ± 0.059 ms —
contourletoperators/NSCTOp/adjoint — 6.38 ± 0.082 ms —
contourletoperators/NSCTOp/forward — 10.5 ± 0.26 ms —
dspoperators/Filt/adjoint 222 ± 5.3 μs 221 ± 5.2 μs 1.01 ± 0.068
dspoperators/Filt/forward 230 ± 5.1 μs 228 ± 5.3 μs 1.01 ± 0.065
dspoperators/MIMOFilt/adjoint 180 ± 2.2 μs 180 ± 1.6 μs 1 ± 0.031
dspoperators/MIMOFilt/forward 187 ± 2.2 μs 187 ± 2.2 μs 0.999 ± 0.033
dspoperators/Xcorr/adjoint 51.2 ± 1.7 μs 52 ± 1.9 μs 0.985 ± 0.097
dspoperators/Xcorr/forward 483 ± 60 μs 478 ± 91 μs 1.01 ± 0.46
fftwoperators/DFT/adjoint 172 ± 6.8 μs 172 ± 7.3 μs 0.995 ± 0.12
fftwoperators/DFT/forward 144 ± 7.4 μs 150 ± 8 μs 0.961 ± 0.14
linearoperators/DiagOp/adjoint-single 243 ± 24 μs 257 ± 11 μs 0.946 ± 0.2
linearoperators/DiagOp/adjoint-threaded 252 ± 17 μs 240 ± 6.8 μs 1.05 ± 0.16
linearoperators/DiagOp/forward-single 250 ± 19 μs 254 ± 8.9 μs 0.986 ± 0.17
linearoperators/DiagOp/forward-threaded 252 ± 16 μs 243 ± 8.8 μs 1.04 ± 0.15
linearoperators/Eye/forward 428 ± 24 μs 452 ± 37 μs 0.946 ± 0.19
linearoperators/FiniteDiff/adjoint 361 ± 18 μs 418 ± 39 μs 0.864 ± 0.18
linearoperators/FiniteDiff/forward 365 ± 20 μs 430 ± 38 μs 0.847 ± 0.18
linearoperators/GetIndex/adjoint 783 ± 24 μs 878 ± 56 μs 0.892 ± 0.13
linearoperators/GetIndex/forward 506 ± 22 μs 557 ± 40 μs 0.91 ± 0.15
linearoperators/LBFGS/mul 52.9 ± 1.9 μs 54.3 ± 2.2 μs 0.974 ± 0.11
linearoperators/LBFGS/update 13 ± 0.88 μs 12.4 ± 0.69 μs 1.04 ± 0.18
linearoperators/LMatrixOp/adjoint 227 ± 12 μs 269 ± 22 μs 0.845 ± 0.17
linearoperators/LMatrixOp/forward 258 ± 27 μs 250 ± 22 μs 1.03 ± 0.28
linearoperators/MatrixOp/adjoint 184 ± 5.6 μs 186 ± 6.3 μs 0.99 ± 0.09
linearoperators/MatrixOp/forward 190 ± 5.5 μs 194 ± 7.5 μs 0.982 ± 0.095
linearoperators/MyLinOp/adjoint 264 ± 19 μs 277 ± 13 μs 0.954 ± 0.17
linearoperators/MyLinOp/forward 269 ± 20 μs 279 ± 16 μs 0.965 ± 0.18
linearoperators/Variation/adjoint-single 864 ± 1.8 μs 865 ± 2.7 μs 0.998 ± 0.0075
linearoperators/Variation/forward-single 86.5 ± 6.1 μs 92.8 ± 8.2 μs 0.932 ± 0.21
linearoperators/ZeroPad/adjoint 164 ± 1 μs 165 ± 1.7 μs 0.998 ± 0.024
linearoperators/ZeroPad/forward 81.9 ± 8.2 μs 80.4 ± 12 μs 1.02 ± 0.37
linearoperators/Zeros/forward 782 ± 19 μs 787 ± 57 μs 0.993 ± 0.15
nfftoperators/NFFTOp/adjoint 257 ± 7.2 μs 260 ± 7.9 μs 0.988 ± 0.082
nfftoperators/NFFTOp/forward 213 ± 3.9 μs 212 ± 6.8 μs 1 ± 0.074
nonlinearoperators/Atan/forward 449 ± 6.2 μs 449 ± 6.5 μs 1 ± 0.04
nonlinearoperators/Atan/jacobian-adjoint 14.7 ± 1.1 μs 13.8 ± 0.85 μs 1.07 ± 0.21
nonlinearoperators/Cos/forward 393 ± 5.9 μs 395 ± 5.8 μs 0.995 ± 0.042
nonlinearoperators/Cos/jacobian-adjoint 410 ± 7.1 μs 407 ± 7.8 μs 1.01 ± 0.052
nonlinearoperators/Exp/forward 514 ± 6 μs 527 ± 5.9 μs 0.975 ± 0.032
nonlinearoperators/Exp/jacobian-adjoint 598 ± 6.1 μs 598 ± 6.3 μs 0.999 ± 0.029
nonlinearoperators/Pow/forward 452 ± 5.7 μs 453 ± 5.7 μs 0.999 ± 0.036
nonlinearoperators/Pow/jacobian-adjoint 389 ± 5.8 μs 384 ± 6.1 μs 1.01 ± 0.044
nonlinearoperators/Sech/forward 240 ± 4.5 μs 240 ± 4.8 μs 1 ± 0.056
nonlinearoperators/Sech/jacobian-adjoint 645 ± 6.7 μs 650 ± 6.2 μs 0.991 ± 0.028
nonlinearoperators/Sigmoid/forward 341 ± 5.3 μs 349 ± 5.4 μs 0.976 ± 0.043
nonlinearoperators/Sigmoid/jacobian-adjoint 331 ± 7.1 μs 337 ± 5.4 μs 0.984 ± 0.053
nonlinearoperators/Sin/forward 388 ± 5.5 μs 383 ± 5.8 μs 1.01 ± 0.042
nonlinearoperators/Sin/jacobian-adjoint 412 ± 5.7 μs 409 ± 6.9 μs 1.01 ± 0.044
nonlinearoperators/SoftMax/forward 335 ± 6 μs 326 ± 5.7 μs 1.03 ± 0.051
nonlinearoperators/SoftMax/jacobian-adjoint 373 ± 8.9 μs 373 ± 7 μs 1 ± 0.061
nonlinearoperators/SoftPlus/forward 916 ± 1.4 μs 916 ± 1.4 μs 1 ± 0.0043
nonlinearoperators/SoftPlus/jacobian-adjoint 411 ± 6.7 μs 416 ± 7.2 μs 0.988 ± 0.047
nonlinearoperators/Tanh/forward 331 ± 5.6 μs 332 ± 5.3 μs 0.998 ± 0.046
nonlinearoperators/Tanh/jacobian-adjoint 267 ± 5.2 μs 276 ± 5.1 μs 0.966 ± 0.052
normaloperators/DFT/mul 5 ± 0.18 μs 4.96 ± 0.26 μs 1.01 ± 0.13
normaloperators/DiagOp/mul 274 ± 14 μs 256 ± 6.7 μs 1.07 ± 0.12
normaloperators/NFFTOp/mul 163 ± 2.1 μs 163 ± 8.5 μs 1 ± 0.11
waveletoperators/WaveletOp/adjoint 3.14 ± 0.042 ms 3.09 ± 0.088 ms 1.02 ± 0.064
waveletoperators/WaveletOp/forward 1.24 ± 0.015 ms 1.22 ± 0.018 ms 1.01 ± 0.039
Memory benchmarks
Benchmark base head Ratio (base/head)
batching/SimpleBatchOp/adjoint-single 768 allocs (277.00 KiB) 768 allocs (277.00 KiB) 1
batching/SimpleBatchOp/forward-single 384 allocs (265.00 KiB) 384 allocs (265.00 KiB) 1
batching/SpreadingBatchOp/adjoint-single 418 allocs (140.09 KiB) 418 allocs (140.09 KiB) 1
batching/SpreadingBatchOp/forward-single 224 allocs (134.00 KiB) 224 allocs (134.00 KiB) 1
calculus/AffineAdd/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/AffineAdd/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Ax_mul_Bx/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Ax_mul_Bx/jacobian-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Ax_mul_Bxt/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Ax_mul_Bxt/jacobian-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Axt_mul_Bx/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Axt_mul_Bx/jacobian-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/BroadCast/identity-single 1 allocs (48 bytes) 1 allocs (48 bytes) 1
calculus/BroadCast/operator-single-adjoint 31 allocs (1.08 KiB) 31 allocs (1.08 KiB) 1
calculus/BroadCast/operator-single-forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Compose/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Compose/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/DCAT/adjoint 1 allocs (32 bytes) 1 allocs (32 bytes) 1
calculus/DCAT/forward 1 allocs (32 bytes) 1 allocs (32 bytes) 1
calculus/HCAT/adjoint 131 allocs (4.70 KiB) 131 allocs (4.70 KiB) 1
calculus/HCAT/forward 70 allocs (2.52 KiB) 70 allocs (2.52 KiB) 1
calculus/HadamardProd/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/HadamardProd/jacobian-adjoint 6 allocs (512.14 KiB) 6 allocs (512.14 KiB) 1
calculus/Jacobian/sigmoid-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Reshape/adjoint 1 allocs (32 bytes) 1 allocs (32 bytes) 1
calculus/Reshape/forward 1 allocs (32 bytes) 1 allocs (32 bytes) 1
calculus/Scale/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Scale/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Sum/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/Sum/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
calculus/VCAT/adjoint 130 allocs (4.67 KiB) 130 allocs (4.67 KiB) 1
calculus/VCAT/forward 71 allocs (2.55 KiB) 71 allocs (2.55 KiB) 1
contourletoperators/ContourletOp/adjoint — 6 allocs (256 bytes) —
contourletoperators/ContourletOp/forward — 7 allocs (336 bytes) —
contourletoperators/NSCTOp/adjoint — 462 allocs (24.75 KiB) —
contourletoperators/NSCTOp/forward — 463 allocs (24.83 KiB) —
dspoperators/Filt/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
dspoperators/Filt/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
dspoperators/MIMOFilt/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
dspoperators/MIMOFilt/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
dspoperators/Xcorr/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
dspoperators/Xcorr/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
fftwoperators/DFT/adjoint 45 allocs (258.34 KiB) 45 allocs (258.34 KiB) 1
fftwoperators/DFT/forward 45 allocs (258.34 KiB) 45 allocs (258.34 KiB) 1
linearoperators/DiagOp/adjoint-single 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/DiagOp/adjoint-threaded 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/DiagOp/forward-single 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/DiagOp/forward-threaded 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/Eye/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/FiniteDiff/adjoint 12 allocs (4.00 MiB) 12 allocs (4.00 MiB) 1
linearoperators/FiniteDiff/forward 6 allocs (4.00 MiB) 6 allocs (4.00 MiB) 1
linearoperators/GetIndex/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/GetIndex/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/LBFGS/mul 65 allocs (1.33 KiB) 65 allocs (1.33 KiB) 1
linearoperators/LBFGS/update 3 allocs (48 bytes) 3 allocs (48 bytes) 1
linearoperators/LMatrixOp/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/LMatrixOp/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/MatrixOp/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/MatrixOp/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/MyLinOp/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/MyLinOp/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/Variation/adjoint-single 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/Variation/forward-single 512 allocs (16.00 KiB) 512 allocs (16.00 KiB) 1
linearoperators/ZeroPad/adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/ZeroPad/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
linearoperators/Zeros/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nfftoperators/NFFTOp/adjoint 152 allocs (5.28 KiB) 152 allocs (5.28 KiB) 1
nfftoperators/NFFTOp/forward 149 allocs (5.17 KiB) 149 allocs (5.17 KiB) 1
nonlinearoperators/Atan/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Atan/jacobian-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Cos/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Cos/jacobian-adjoint 6 allocs (512.14 KiB) 6 allocs (512.14 KiB) 1
nonlinearoperators/Exp/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Exp/jacobian-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Pow/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Pow/jacobian-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Sech/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Sech/jacobian-adjoint 6 allocs (512.14 KiB) 6 allocs (512.14 KiB) 1
nonlinearoperators/Sigmoid/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Sigmoid/jacobian-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Sin/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Sin/jacobian-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/SoftMax/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/SoftMax/jacobian-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/SoftPlus/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/SoftPlus/jacobian-adjoint 3 allocs (512.07 KiB) 3 allocs (512.07 KiB) 1
nonlinearoperators/Tanh/forward 0 allocs (0 bytes) 0 allocs (0 bytes) 1
nonlinearoperators/Tanh/jacobian-adjoint 0 allocs (0 bytes) 0 allocs (0 bytes) 1
normaloperators/DFT/mul 0 allocs (0 bytes) 0 allocs (0 bytes) 1
normaloperators/DiagOp/mul 0 allocs (0 bytes) 0 allocs (0 bytes) 1
normaloperators/NFFTOp/mul 7 allocs (208 bytes) 7 allocs (208 bytes) 1
waveletoperators/WaveletOp/adjoint 12 allocs (512.46 KiB) 12 allocs (512.46 KiB) 1
waveletoperators/WaveletOp/forward 12 allocs (512.46 KiB) 12 allocs (512.46 KiB) 1

Ratio interpretation: values > 1 mean the PR is faster; values < 1 mean slower.
🚀 significant speedup · 🐢 significant slowdown

@codecov

codecov Bot commented Aug 25, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.68646% with 70 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.07%. Comparing base (907d673) to head (4f1141f).

Files with missing lines Patch % Lines
src/batching/SpreadingBatchOp.jl 82.35% 9 Missing ⚠️
src/threading_policy.jl 89.74% 8 Missing ⚠️
src/batching/SimpleBatchOp.jl 72.00% 7 Missing ⚠️
src/nonlinearoperators/Pow.jl 71.42% 6 Missing ⚠️
src/nonlinearoperators/SoftMax.jl 91.07% 5 Missing ⚠️
src/nonlinearoperators/SoftPlus.jl 68.75% 5 Missing ⚠️
src/properties.jl 80.00% 4 Missing ⚠️
src/calculus/BroadCast.jl 76.92% 3 Missing ⚠️
ext/GpuExt/linearoperators/getindex.jl 50.00% 2 Missing ⚠️
src/calculus/DCAT.jl 97.59% 2 Missing ⚠️
... and 17 more
Additional details and impacted files
@@            Coverage Diff             @@
##           master      #46      +/-   ##
==========================================
- Coverage   90.13%   90.07%   -0.06%     
==========================================
  Files          51       52       +1     
  Lines        3678     4142     +464     
==========================================
+ Hits         3315     3731     +416     
- Misses        363      411      +48     

☔ 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.

@hakkelt
hakkelt force-pushed the add-contourlet-operators branch from c9c9783 to ce27237 Compare August 25, 2026 19:15
hakkelt added a commit to hakkelt/Ristretto.jl that referenced this pull request Sep 10, 2026
…gularizer

Pull ContourletOperators (ContourletOp/NSCTOp wrapping Contourlets.jl) from
kul-optec/AbstractOperators.jl#46 (hakkelt/AbstractOperators.jl@add-contourlet-operators)
into the vendored subtree at deps/AbstractOperators, mirroring the existing
WaveletOperators subpackage: new ContourletOperators/ subpackage, test/contourlets/,
and workspace/README/docs/benchmark wiring (benchmark/suites/contourletoperators.jl
follows this repo's bench_common.jl/suites split rather than upstream's monolithic
benchmarks.jl).

Add L1Contourlet, an L1 sparsity regularizer wrapping NSCTOp (not the ragged
ContourletOp): the NSCT's shift-invariance means every subband shares the image's
spatial size, so StackedNSCTOp (src/encoding/contourlet_stack_op.jl) stacks the
transform's multi-component ArrayPartition codomain into a plain
(dim_in..., n_bands) array. That is what lets it reuse AbstractOperators.BatchOp
for extra batch/time dimensions and StructuredOptimization.Term/NormL1, neither of
which accept an operator with a multi-component codomain -- discovered when the
naive ContourletOp/ArrayPartition wiring failed with "Cannot create expression
with LinearOperator with `ndoms(L,1) > 1`". StackedNSCTOp also bridges precision
(Contourlets.jl's default filters are Float64; the reported domain/codomain type
is whatever the caller's Variable uses, e.g. ComplexF32) via copyto! conversion
in its staging buffers.

Tests: test/test_reg_contourlet.jl (2D, batched, precision bridging, materialize)
and deps/AbstractOperators/test/contourlets/ (vendored operator round-trip/JET/Aqua).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@hakkelt
hakkelt force-pushed the add-contourlet-operators branch from 878c4d7 to e69ca6d Compare September 19, 2026 10:04
hakkelt added a commit to hakkelt/Ristretto.jl that referenced this pull request Sep 30, 2026
…gularizer

Pull ContourletOperators (ContourletOp/NSCTOp wrapping Contourlets.jl) from
kul-optec/AbstractOperators.jl#46 (hakkelt/AbstractOperators.jl@add-contourlet-operators)
into the vendored subtree at deps/AbstractOperators, mirroring the existing
WaveletOperators subpackage: new ContourletOperators/ subpackage, test/contourlets/,
and workspace/README/docs/benchmark wiring (benchmark/suites/contourletoperators.jl
follows this repo's bench_common.jl/suites split rather than upstream's monolithic
benchmarks.jl).

Add L1Contourlet, an L1 sparsity regularizer wrapping NSCTOp (not the ragged
ContourletOp): the NSCT's shift-invariance means every subband shares the image's
spatial size, so StackedNSCTOp (src/encoding/contourlet_stack_op.jl) stacks the
transform's multi-component ArrayPartition codomain into a plain
(dim_in..., n_bands) array. That is what lets it reuse AbstractOperators.BatchOp
for extra batch/time dimensions and StructuredOptimization.Term/NormL1, neither of
which accept an operator with a multi-component codomain -- discovered when the
naive ContourletOp/ArrayPartition wiring failed with "Cannot create expression
with LinearOperator with `ndoms(L,1) > 1`". StackedNSCTOp also bridges precision
(Contourlets.jl's default filters are Float64; the reported domain/codomain type
is whatever the caller's Variable uses, e.g. ComplexF32) via copyto! conversion
in its staging buffers.

Tests: test/test_reg_contourlet.jl (2D, batched, precision bridging, materialize)
and deps/AbstractOperators/test/contourlets/ (vendored operator round-trip/JET/Aqua).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
hakkelt and others added 24 commits November 2, 2026 19:35
…t merge

Adds AcceleratedDCTs test dependency, disables Aqua's persistent_tasks
check, and carries forward the test coverage expansion, test refactors,
and small dead-code/API cleanups developed on gpu-refactor after the
GPU-support work was squash-merged upstream as 861af22.
It requires Julia >= 1.11 and is already added dynamically at runtime
for that case in test/utils.jl; listing it statically forced dependency
resolution to fail on the Julia 1.10 LTS CI job.
The private thread_count_functions registry and the @restrict_threading /
@enable_full_threading macros in src/utils.jl are replaced by the new
standalone NestedThreading.jl package.

Beyond relocating the mechanism this fixes a latent correctness bug: the
old save/restore was per scope, so two concurrent batched mul! calls
captured each other's already-restricted thread counts and left BLAS
permanently pinned at 1 with no scope active (reproducible before this
commit, fixed after). NestedThreading refcounts budgets process-wide,
snapshotting once and restoring once, and applies the minimum over all
active scopes so nesting can only narrow a budget, never widen it.

- src/utils.jl: delete the threading machinery.
- SpreadingBatchOp/SimpleBatchOp: @restrict_threading @threads -> the
  @budgeted_threads macro; the LOCKING @sync/@Spawn sites use the
  function form with_restricted_threads.
- FFTWOperators: drop the __init__ that reached into AbstractOperators'
  private registry; the FFTW pool now registers via NestedThreading's own
  package extension.
- NFFTOperators: delete set_nfft_threading_expr and its two macros in
  favour of a with_nfft_threading helper over NestedThreading's scoped
  API, at all six call sites.
- Inner libraries now get nthreads() / trip_count threads instead of
  always 1, so a 2-element batch on 8 threads runs BLAS at 4.
- Add a regression test for the concurrency bug and for budget nesting.

NestedThreading is wired in via [sources] pending registration in
General, so these changes are not ready to push yet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHYpJyvm5XwhtMXLE1N8i9
BroadCast.jl's threaded adjoint mul! runs an outer @threads loop whose
body calls mul! on arbitrary sub-operators, which may themselves use
BLAS/FFTW. It never went through the old @restrict_threading, so it was a
genuine oversubscription source that the migration had left in place.

This was the last raw @threads in src/, so @threads is dropped from the
imports; @Spawn and nthreads are still used.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHYpJyvm5XwhtMXLE1N8i9
`TestUtils` is the setup module of nearly every testitem, and it unconditionally ran
`GPUEnv.activate(; persist=true)` plus the FFTW-specific `AcceleratedDCTs` install. Any
filtered run paid full GPU environment activation even when it selected no GPU test.

Move both into a new `GpuEnvSetup` @testmodule (test/gpu_env_setup.jl). TestItemRunner
evaluates setup modules lazily, only for testitems that survive the filter, so activation
now happens iff a `:gpu` testitem is selected. All 47 `:gpu` testitems gain
`GpuEnvSetup` in their setup list.

`runtests.jl` publishes `ABSTRACTOPERATORS_TEST_GPU` from the filter closure, set from the
tags of the items it actually accepts. Deriving it from the filter *string* would be wrong:
GPU testitems also carry their category tags, so `:linearoperator` selects `Eye (GPU)` too.
`GpuEnvSetup` errors when the flag is false, which can only mean a testitem missing its
`:gpu` tag pulled the module in.

Audit results:
- `:gpu` testitems keep their inline `using GPUEnv`; the import is a name binding and costs
  nothing, only `activate` was expensive.
- test_gpu_quality.jl's two items are tagged `:gpu` but exercise GpuExt through JLArrays
  alone, a direct test dep, so they deliberately do not use `GpuEnvSetup`.

Measured: single non-GPU testitem 3m20s -> 8.7s. Pass counts unchanged --
`:jet` 224/224 and `:misc,:quality` 185/185 with zero GPU activation, `:calculus`
1557/1557 (still activates, correctly: it selects the `*(GPU)` items by their
`:calculus` tag), `"Eye (GPU)"` 27/27 through the new setup module.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHYpJyvm5XwhtMXLE1N8i9
Phases 1-4 of THREADING_PLAN.md. Threading was previously expressed three different ways
with four scattered thresholds, and `copy_operator`'s deepcopy fallback silently ignored
both of its keywords.

Infrastructure (src/threading_policy.jl):
- `is_threaded` trait, `supports_threading`, `default_threaded`/`threading_threshold`
  per-operator policy, and `adapt_operator`.
- `copy_operator` now always copies; the "share when thread-safe" short-circuit moved to
  `adapt_operator`. The deepcopy fallback refuses `storage_type`/`threaded` instead of
  ignoring them, naming the type that needs a `_copy_operator_impl`.
- Thresholds are transcriptions of benchmark/threading_sweep.jl (new), not guesses; each
  carries a PROVENANCE line. The sweep contradicted the plan's hypothesis that
  FastBroadcast wins or ties everywhere: for a pure copy `@.. thread=true` is
  time-identical to serial at every swept size, while `@batch` crosses over at 2^15.

Nesting safety (the bug the plan called out):
- create_BatchOp, create_threaded_SpreadingBatchOp and OperatorBroadCast adapted only the
  per-thread *copies*, leaving instance 1 threaded. All three now go through
  `_per_thread_operators`, which also branches on `is_thread_safe` to decide share vs copy
  -- `require_thread_safe` cannot manufacture thread safety, so it must not pretend to.
- `opType` had to move after the adaptation: threading is a type parameter.

Elementwise threading: all 10 nonlinear operators (SoftMax excluded, with the reduction
reason recorded), FiniteDiff, and forwarding traits for the calculus operators.

FiniteDiff `mul!` no longer allocates: `b[idx_1] .- b[idx_2]` materialised two temporaries
per call. With `@views` it is allocation-free and 45x faster at n=1e6 (2581us -> 57us).

Two problems the tests surfaced, both fixed in source rather than in the assertion:
- `adapt_operator(op; threaded=false)` silently no-opped on forwarders, which lacked
  `is_threaded` and so inherited the `false` default while their children stayed threaded.
- JET flagged runtime dispatch in `FiniteDiff(T, dims)`, which fell through to the
  `dir::Int` method and its `Val(dir)`. Split out a D=1 literal method.

The two batching perf tests needed wider margins, not weaker ones: the FiniteDiff
allocation fix sped the serial baseline up ~3x, shrinking an honest 2.5x speedup to 1.7x
and letting runner noise flip a bare `<`. Both now take a minimum across repetitions at a
workload size measured to favour batch parallelism (smaller-items/more-items was measured
to hurt).

Tests: :batching 283, :jet 245 (incl. new @test_opt/@test_call threading coverage),
:quality 61, :calculus+:linearoperator+:nonlinearoperator+:misc 2698, :Threading+:Syntax
276 -- all passing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHYpJyvm5XwhtMXLE1N8i9
Phase 5 of THREADING_PLAN.md, for DCAT: its blocks are fully independent in both
directions, with no accumulation and no shared output.

The policy needed a second condition the plan did not anticipate. Aggregate work alone
does not predict a win: 2 blocks x 2^18 elements is a 2^19 aggregate but measured only
1.03x, because two blocks cap the gain at 2x and the threading overhead eats it, while the
same 2^19 aggregate across 8 blocks measured 1.9x. So `default_block_threaded` requires
both `THRESHOLD_BLOCK_PARALLEL` (2^18, measured) and `MIN_BLOCKS_FOR_PARALLEL` (4). That
pair admits every measured win and excludes every measured loss:

  nb=8 bs=2^16  1.90x  threaded      nb=8 bs=2^12  0.26x  not threaded
  nb=4 bs=2^16  1.18x  threaded      nb=2 bs=2^18  1.03x  not threaded
  nb=8 bs=2^18  9.67x  threaded

The threaded `mul!` is deliberately not `@generated`: a generated body must be pure and the
threading macros expand to closures. The parallel loop is therefore a plain function and
only the single-block body is generated, selected by `Val(i)`. That costs one dynamic
dispatch per block -- nanoseconds against a whole child `mul!` -- and keeps the per-block
type stability a closure over a heterogeneous tuple would have lost. `@budgeted_threads`
rather than `@batch` because each body may itself reach BLAS/FFTW.

Nesting safety applies here too: when the block loop threads, the constructor adapts the
blocks to `threaded = false`.

VCAT-forward and HCAT-adjoint from the plan are not included; they need their own
measurement before shipping, per the plan's own rule that unmeasured variants are dropped.

Tests: :Threading,:DCAT,:calculus 1769; :jet,:quality,:misc 462 -- all passing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHYpJyvm5XwhtMXLE1N8i9
…ression

Phase 6 of THREADING_PLAN.md, plus a regression this phase uncovered.

The regression: Phase 1's strict `copy_operator` fallback made batching *any* FFTW/DSP
operator raise. Those operators own scratch buffers, so a threaded batch needs one private
copy per thread; but they have no `_copy_operator_impl`, and the fallback refused the
`threaded = false` request the batch layer makes. The fix belongs in the fallback's rule,
not in per-operator patches: an operator with no threaded path cannot have a `threaded`
request violated, so deepcopy is a correct answer for it. That is the same rule
`_satisfies_constraints` already used, now applied consistently. `storage_type` is still
refused, since deepcopy genuinely cannot honour it.

Per subpackage:
- NFFTOp already carried a `threaded` field; it gains the traits and a `_copy_operator_impl`
  that shares the immutable plan and dcf while giving the copy its own scratch buffer.
  Requests that would need a different plan (thread count, storage backend) raise rather
  than being silently ignored.
- DFT/IDFT gain `threaded` as the package-wide spelling of the existing `num_threads`, and
  record the plan-time count so `is_threaded` can report it. FFTW is a counted pool, so this
  is fixed at construction; switching it replans.
- DSP and Wavelet operators declare `supports_threading = false` explicitly rather than
  inheriting the default, which is what lets a threaded batch wrap them.

Two bugs caught while writing this, both mine:
- A "cleanup" of a trailing `'` deleted the adjoint postfix from five IDFT constructors,
  turning IDFT into a plain DFT and silently disabling the DFT/IDFT combination rules. The
  FFTW combination tests caught it; there is now a test asserting IDFT stays adjoint-wrapped.
- The DFT copy replanned from the codomain element type, which is wrong for a real-input
  DFT (`C == Complex{D}`) and produced an operator with a complex domain.

DCT/RDFT/IRDFT are not wired for plan-time thread counts -- they have no `num_threads`
plumbing at all today -- so they are declared unthreaded rather than half-wired. Recorded in
the plan's "Not done" section along with VCAT/HCAT block parallelism and the four legacy
thresholds that still gate the pre-existing operators.

THREADING_PLAN.md gains an execution record: measured-vs-guessed thresholds, the three plan
hypotheses that measurement contradicted, and what was deliberately left out.

Tests: full suite 3856 passed, 0 failed, 0 errored (21m47s).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHYpJyvm5XwhtMXLE1N8i9
…all operators

Follow-up round covering VCAT/HCAT block threading, the remaining FFTW transforms, the
legacy thresholds, per-operator thresholds, and one semantics change that ties them
together.

`threaded` is now `Bool` only, with one meaning everywhere, resolved in one place
(`_resolve_threaded`):

  false            veto -- never threads. The only hard directive, and it must be: nesting
                   safety depends on a block/batch loop switching its children off and
                   being able to rely on it.
  true (default)   permission -- threads only if the policy also agrees.

Three families previously disagreed. Elementwise and block operators treated `true` as an
absolute override, batch operators as a permission, and the FFTW transforms bypassed the
policy entirely -- so a 256-point threaded DFT reported is_threaded == true while measuring
0.02x. `is_threaded` now reports what the operator will actually do, never what was asked
for; consequently a single-threaded session or GPU storage reports false without any
special-casing. `copy_operator`/`adapt_operator` keep accepting `nothing`, since there
`threaded` is a *constraint* ("no constraint, preserve") rather than a setting -- without it
a plain copy could not preserve an explicitly serial operator.

VCAT-forward and HCAT-adjoint now thread their block loops, and measuring them forced two
corrections to the Phase 5 policy:
- per-block, not aggregate: at a fixed 2^18 aggregate VCAT measures 1.32x with 4 blocks of
  2^16 but 0.85x with 16 blocks of 2^14;
- per-operator: at 4 blocks of 2^16 VCAT measures 1.32x while HCAT measures 0.76x, because
  HCAT's adjoint carries per-block ArrayPartition indexing. One shared constant would have
  shipped that regression.

FFTW does thread r2r and r2c (measured 3.08x r2c, 2.25x DCT, 1.91x IDCT at n=2^22), so
DCT/IDCT/RDFT/IRDFT gained plan-time thread counts, and all six transforms are now gated on
measured crossovers (c2c 2^13, r2r 2^15, r2c 2^15). `num_threads` stays an explicit command
rather than a permission -- the escape hatch for callers who know their workload.

The four legacy thresholds are gone, which fixed two live defects:
- Variation's two constructors thresholded in different *units* (bytes vs elements), so the
  same 10000-element input produced opposite threading;
- `_should_thread(::AbstractOperator)` had no size component at all, so batch operators
  threaded four-element work.
Scale's legacy 1e4 cutoff was ~400x below its measured 2^22 crossover.

Thresholds are now per-operator transcriptions from benchmark/operator_thresholds.jl (new),
which measures each operator's real `mul!`. The cost classes proved too coarse: within
"transcendental" the crossovers span 2^8 (SoftPlus) to 2^11 (integer Pow), and Pow splits
internally by exponent kind since `x^0.5` lowers to `exp(p*log(x))`.

Also unified: SignAlternation had its own `threaded && nthreads() > 1` with no size policy
and no `is_threaded` method, so its trait always reported false regardless of what it did.

JET caught a regression that no functional test could: reassigning the captured `A` inside
the DCAT/VCAT/HCAT constructors boxed it to `Any`, cascading into 11 runtime-dispatch sites
in a constructor that should infer fully. Results were correct throughout; only @test_opt
saw it.

Tests: full suite 3971 passed, 0 failed. Three batching tests needed updating because they
encoded the old "threaded=true forces threading" assumption -- one of them was reading
`batch_op.operator[1]`, which on the single-threaded struct silently *slices* the wrapped
operator instead of erroring; it now goes through the `_wrapped_operator` accessor.

Known, unfixed, pre-existing: Variation's adjoint throws BoundsError when the trailing
dimension is exactly 2. Present on master; recorded in THREADING_PLAN.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHYpJyvm5XwhtMXLE1N8i9
The adjoint's per-dimension branch chain tested `i == 2` before
`i == size(y, d)`, so the last column of a dimension of length exactly 2
took the interior formula and read one element past the end. This hit
*any* dimension of size 2, not only the trailing one -- (2,8) and
(8,2,3) failed alongside (8,2) -- and the result was mathematically
wrong there, not merely out of bounds: column n of the transpose is
`y_1 + y_n` when n == 2, while the interior formula gives
`y_1 + y_2 - y_3`.

Rewrite the contribution as three independent terms
(`_variation_adjoint_term`) rather than mutually exclusive branches, so
the n == 2 case correctly takes both the `i == 2` and `i == n` terms.
Both mul! methods now share one body so the threaded and serial paths
cannot drift apart.

`N` is passed to the shared body as a plain Int, deliberately not a Val:
inside Polyester's `@batch` the loop body becomes a closure that does not
carry the enclosing method's static parameters, so a `Val(N)` argument is
built from a runtime value and every element dispatches dynamically. That
allocated 293 MB at n = 2^22 and turned a 4.5x speedup into 0.25x, while
leaving every correctness test green -- hence the new allocation testitem,
timings being too noisy to assert on.

Also reject singleton dimensions at construction. They have no finite
difference to take and already failed with a BoundsError inside the
*forward* kernel, so an ArgumentError naming the cause is strictly
better; the array-based constructor now delegates to the tuple one so the
guard and the threading policy each exist in one place.

Verified against the dense forward matrix's exact transpose over 16
shapes x 2 element types x 2 threading modes. Threaded adjoint is back to
baseline (12527 us vs 12668 us at n = 2^22, zero allocations) and the
serial path is ~1.75x faster at mid sizes (3528 vs 6195 us at 2^18), the
three-term form replacing unpredictable branches with straight-line
arithmetic. Measured crossover is unchanged at 2^10.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHYpJyvm5XwhtMXLE1N8i9
The plan is fully executed and its content is now recorded where it is
actually used: threshold provenance in `threading_threshold` docstrings,
policy rationale in `src/threading_policy.jl`, and the measurement
methodology in `benchmark/operator_thresholds.jl`. Nothing references the
file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHYpJyvm5XwhtMXLE1N8i9
Threaded coverage went from 15 entries to 45. Every operator whose policy
can grant threading now has a matched `-single` / `-threaded` pair:
FiniteDiff, Scale, HCAT/VCAT/DCAT, DFT, NFFTOp and the nine threading-
capable non-linear operators join the five that were already covered.

Three defects surfaced while doing it:

1. FiniteDiff and all nine non-linear operators were *implicitly*
   threaded. Their constructors default to `threaded = true` and every
   benchmark size clears the operator's threshold, so a single entry
   measured the threaded path on a dev box and the serial path on a
   one-core runner under the same name. Base entries are now pinned
   `threaded = false` and renamed to `-single`, matching the convention
   DiagOp/Variation/BroadCast already used.

2. `threaded = true` is a permission, not a command, so a mis-sized
   benchmark silently measures the serial path -- and would keep
   reporting a healthy "threaded" number straight through a threading
   regression. Every threaded entry now routes through `check_threaded`
   (or `check_block_threaded` for the *CATs, where `is_threaded` is also
   true when merely the blocks thread). Sizes that the policy rejects are
   given their own constants: Scale needs 2^22, and the *CATs need four
   blocks rather than the two the serial states use.

3. At `evals = 1` the threaded entries were dominated by first-touch page
   faults on the several MB `setup` allocates per sample. DiagOp's
   threaded forward measured a median 4.15x its own minimum and came out
   *slower* than the serial path it beats by 8x once warm. Threaded
   entries now use `evals = 50`, which drops the spread to 1.5x and makes
   the minimum agree with a standalone hot-loop measurement. Serial
   entries keep `evals = 1` so their baselines stay comparable.

Multithreaded entries are skipped whenever `CI` is set, and when Julia has
one thread. `ABSTRACTOPERATORS_BENCH_THREADED=true|false` overrides both,
for a dedicated runner with a pinned core count. On CI the suite is 87
deterministic entries; locally with threads it is 134.

benchmarks.jl is now an entry point over `bench_common.jl` (probes, gate,
constants, shared fixtures) plus one file per suite in `suites/`, each
runnable on its own:

    julia --project=benchmark benchmark/suites/linearoperators.jl

SUITE keys and grouping are unchanged by the split: 134 entries across 9
groups, and the per-suite counts sum to 134.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHYpJyvm5XwhtMXLE1N8i9
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHYpJyvm5XwhtMXLE1N8i9
_fbthread(b::Bool) dispatched through Val(b) across two singleton methods, which
Julia LTS's inference could not resolve at compile time (unlike current stable,
which constant-folds the whole chain) -- JET flagged it as runtime dispatch in
DiagOp/Scale @test_opt. Replaced with a direct ternary, which is a single method
returning a small Union instead of a multi-method dispatch.

Documenter's checkdocs=:exports also failed: is_threaded, adapt_operator, and
supports_threading are exported but had no @docs entry. Added a Threading section
to docs/src/properties.md. That exposed two @ref links (_resolve_threaded, used
twice) pointing at an internal, non-exported helper with no rendered docstring;
unlinked them to plain code spans since they're implementation details, not
public API.

Verified locally: JET/threading test suite (787 tests) and `docs/make.jl` both
pass.
…-loss bug

Codecov flagged this PR's patch coverage at 68.65% (211 lines missing). Most of
the gap was the new `is_threaded`/`supports_threading`/`_children` one-liners
and `_copy_operator_impl` paths added across AffineAdd, Ax_mul_Bx, Ax_mul_Bxt,
Axt_mul_Bx, HadamardProd, BroadCast, SimpleBatchOp, and SpreadingBatchOp, none
of which any existing test called directly -- plus HCAT's threaded adjoint
block loop, whose only exercising test used a block size below HCAT's own
(higher) threshold and so never actually took the threaded path.

Writing the DiagOp/SpreadingBatchOp coverage surfaced a real bug: DiagOp's
`_copy_operator_impl` ran `op.d` (the operator's actual diagonal data, not a
scratch buffer) through `_convert_buffer`, which is `similar`-based and
intentionally does not copy values -- correct for `mul!` scratch buffers that
get overwritten every call, silently wrong for the one field that isn't. Any
`copy_operator(diag_op; storage_type = ...)` was replacing the diagonal with
uninitialized memory, and a scalar `d` MethodError'd outright since
`_convert_buffer` has no `Number` method. Fixed with a `copyto!`-based
`_copy_diag` mirroring AffineAdd's existing `_copy_displacement`, and
strengthened the existing DiagOp copy_operator test (which asserted `!==` but
never checked the copied values were still correct) to catch this class of
regression going forward.

Also fixed two test-writing bugs surfaced while adding this coverage: the new
SpreadingBatchOp/HCAT threading tests initially used per-item/per-block sizes
below the relevant threshold, so `threaded = true` was correctly declined by
the policy and the assertions were exercising the serial path while believing
they were exercising the threaded one.

Verified locally (JULIA_NUM_THREADS=2, matching CI): full :batching/:Threading
tag run (1375 tests) and the full :jet suite (245 tests), both passing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHYpJyvm5XwhtMXLE1N8i9
…dingBatchOp gap

Two more codecov-flagged gaps from the threading migration:

- Scale and AdjointOperator gained the same is_threaded/supports_threading/
  _children/_copy_operator_impl additions as the other operators, with zero
  test coverage. Scale also needed its ArrayPartition-codomain branch exercised
  (codomain_type_for_policy/codomain_array_type_for_policy), covered here via a
  Scale wrapping a DCAT.

- Bigger issue: nearly every existing SpreadingBatchOp test that names a
  specific threading strategy (COPYING, LOCKING, FIXED_OPERATOR, AUTO) used a
  per-item domain size of 5-15 elements. This PR's threading policy gates
  `threaded = true` on MIN_BATCH_WORK_FOR_PARALLEL (2^10 elements) before it
  ever reaches strategy selection, so every one of those tests was silently
  falling back to the single-threaded branch regardless of which strategy it
  named -- `test_failing_nonthreadsafe_spreading_batch_op` already carried a
  comment explaining this exact trap for its own workload. Bumped the shared
  `test_nonthreadsafe_spreading_batch_op` helper and the four named-strategy
  testitems to sizes that actually cross the gate, and added `isa` assertions
  so a future regression back to the wrong strategy fails loudly instead of
  silently. One power-iteration opnorm assertion needed its tolerance loosened
  to match the larger workload (same relaxation already used one line below
  it for the same estimate).

Verified locally (JULIA_NUM_THREADS=2): 1082 tests passing across
:batching/:Threading/:Scale/:AdjointOperator/:jet tags.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHYpJyvm5XwhtMXLE1N8i9
…nd BroadCast's threaded construction

Third round closing the codecov gap on the threading-migration diff, plus a
cleanup found along the way:

- OperatorWrapper: same is_threaded/supports_threading gap as the other
  operators covered in the previous two commits.

- HCAT: every mul!/adjoint mul!/_hcat_block_adj! branch that handles a
  multi-element ("tuple-idxs") sub-operator was untested -- all existing
  coverage used single-index HCATs, so the `Pi <: Integer` false branch never
  ran anywhere, in either the natural or the permuted (indexed) form, serial
  or threaded. Added one operator built from a HCAT-of-DiagOp wrapped in
  Compose(FiniteDiff, ...) to keep it non-diagonal (diagonal wrappers get
  simplified straight back into a flat HCAT by the package's own combination
  rules) and cheap enough to push past the block-threading size threshold.
  Verified with `test_op`'s forward/adjoint dot-product identity rather than
  hand-derived expected values, which is what caught two dimension bugs in
  earlier drafts of this test before they were committed.

- BroadCast: OperatorBroadCast's threaded-construction branch (per-thread
  domain buffers, per-thread operator copies) needs a broadcast sized above
  THRESHOLD_MEMORY_BOUND; the existing "non-compact threaded" test used ~60
  elements, so `threaded = true` was always declined by the policy.

- Removed a stray `>>>>>>> d84c552 (...)` conflict marker left at the end of
  test/calculus/test_hcat.jl from the branch's original rebase, before any of
  this session's work. Harmless to test execution (outside any @testitem) but
  should not have been committed.

Verified locally (JULIA_NUM_THREADS=2): instrumented coverage now shows zero
missed executable lines in HCAT.jl for the :HCAT tag scope (was 8), and
BroadCast.jl's only remaining misses are two pre-existing lines outside this
PR's diff. Full :HCAT/:BroadCast/:OperatorWrapper/:jet run: 618 tests passing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHYpJyvm5XwhtMXLE1N8i9
Same is_threaded/supports_threading/_children gap as the operators covered in
the previous three commits: SimpleBatchOp.jl and VCAT's threaded-forward mul!
path were already fully exercised (SimpleBatchOp by existing tests, VCAT by
the "VCAT-forward and HCAT-adjoint block loops" threading-contract test at
the right block size) but neither test asserted is_threaded/supports_threading
themselves, and Sum had no threading-trait coverage at all.

Verified locally (JULIA_NUM_THREADS=2): instrumented coverage now shows zero
missed executable lines in SimpleBatchOp.jl and VCAT.jl for the :batching/
:Threading tag scope. Sum.jl's remaining local misses are pre-existing
@generated-function lines outside this PR's diff. Full :Sum/:VCAT/:Threading
run: 496 tests passing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHYpJyvm5XwhtMXLE1N8i9
Both gained a _copy_operator_impl in this PR (the whole reason for it is
honouring storage_type, since neither has a threaded path) with no test
exercising it.

Verified locally: instrumented coverage now shows zero missed executable
lines in both files. Full :LMatrixOp/:MyLinOp/:jet run: 353 tests passing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHYpJyvm5XwhtMXLE1N8i9
A review comment on this branch claimed "None of the DSP operators has a
Julia-level threaded path: the work happens inside DSP.jl/FFTW, which manage
their own parallelism." That's wrong on two counts: DSPOperators does not
depend on DSP.jl at all (only AbstractFFTs/FFTW), and Conv/Xcorr plan their
own FFTW transforms directly -- they were simply never wired into FFTW's
thread count the way FFTWOperators' DFT/DCT/RDFT already are, so they were
hard-coded to `supports_threading = false` alongside Filt/MIMOFilt, which
genuinely have no threaded path (IIR filtering is a sequential recursion, not
an FFT).

Gave Conv and Xcorr the same `num_threads`/`threaded` constructor keywords,
`is_threaded`/`supports_threading`, and `_copy_operator_impl` (replan on a
threaded-flag change, share plans and just re-copy scratch buffers otherwise)
as FFTWOperators' DFT, via a small `_dsp_fftw_num_threads`/
`_dsp_with_fftw_threads` pair mirroring `FFTWOperators._fftw_num_threads`
(DSPOperators does not depend on FFTWOperators, so this isn't reused directly;
the threshold is marked provisional, borrowed from FFTWOperators' measured
:c2c class since both plan a c2c/r2c FFT of the padded convolution length).

Two bugs surfaced while getting this working:
- The first draft wrapped each constructor's *whole* plan-building block
  (including buffer allocation and `if domain_type <: Real` branching) in the
  `_dsp_with_fftw_threads` closure. That runs correctly but JET's `@test_call`
  couldn't infer the closure's return type through the branching and reported
  spurious "local variable not defined" errors. Every existing FFTW-based
  operator in this codebase (RDFT, DCT) only wraps the bare `plan_*` calls
  themselves -- matching that convention fixed the JET report, not just the
  symptom.
- Xcorr's `_copy_operator_impl` replan path called the `(dim_in, h)`
  constructor Xcorr never actually defines (unlike Conv, which does); fixed
  to call the `(domain_type, dim_in, h)` form it does have.

Also updated the pre-existing "subpackage operators declare their threading"
and "batching an operator that has no threaded path" tests, which had used
Conv/Xcorr as their canonical no-threaded-path example -- replaced with Filt,
which is the operator that actually fits that description.

Verified locally (JULIA_NUM_THREADS=2): full :dsp/:batching/:Threading/:jet/
:quality run (1057 tests) and a full Documenter doctest build, both passing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHYpJyvm5XwhtMXLE1N8i9
…eturn-before-loop

_copy_operator_impl's purpose is to produce a new operator satisfying either a
storage_type or threaded request (or both) -- refusing storage_type outright,
which the previous commit did by copying FFTWOperators' DFT.jl pattern
verbatim, defeats that. Unlike DFT (whose plans are the only per-instance
state), Conv/Xcorr can genuinely support it: `h` is copied to the requested
array type with `copyto!` (it's the operator's actual filter data, not a
scratch buffer, so uninitialized allocation would silently corrupt it -- the
same class of bug fixed for DiagOp earlier in this branch) and the operator
is rebuilt through the public constructor, which replans FFTW against
whatever backend the copied `h` now lives on.

Also fixed a Runic formatting artifact: `_xcorr_fir_adj!`'s tiled FIR adjoint
loop ended in `return @inbounds while ... end`, wrapping a loop (whose value
is always `nothing`) in `return` rather than following it with a bare
`return`. Grepped the full branch diff against upstream master for the same
`return for`/`return while` pattern; this was the only occurrence.

Verified locally (JULIA_NUM_THREADS=2): storage_type round-trips correctly
for both operators (copied array is a distinct object with equal values,
`copy * x` matches the original), combined with threaded=true, and the full
:dsp/:jet/:Threading run (433 tests) passes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHYpJyvm5XwhtMXLE1N8i9
…sed it

Same issue as the previous Conv/Xcorr fix, found across the rest of the
branch: DFT, RDFT, IRDFT, DCT/IDCT (FFTWOperators) and NFFTOp (NFFTOperators)
all threw ArgumentError on a storage_type request instead of honouring it.
_copy_operator_impl's contract is to produce a new operator satisfying either
kind of request; refusing one outright isn't a valid implementation of that
contract regardless of how the operator is built internally.

FFTWOperators (DFT/RDFT/IRDFT/DCT/IDCT): none of these hold persistent input
data, only plans (and scratch, for RDFT/DCT) built from a prototype array's
shape and element type -- unlike Conv's `h`, there's nothing to carry values
for. A storage-type change just means replanning against an uninitialized
prototype on the requested backend, exactly like the existing threaded-change
path already did for `Array`; the fix widens that path to any storage_type
and folds the two conditions (unchanged storage AND unchanged threading) into
one guard for the "just share the existing plan" fast path.

NFFTOp: the harder case, since it plans against a `trajectory` array the
operator doesn't retain as its own field, and rebuilding needs it back. It
turns out the NFFT plan already carries it (`plan.k`, in the flattened 2D
form `create_plan` reshapes every trajectory into before planning) -- this
codebase's own `NFFTPlan` override already depends on that exact internal
field layout to *construct* NFFT.jl's plan type in the first place, so
reading `.k` back out is no more fragile than what was already there. The
image size comes from the public `NFFT.size_in`, and `dcf`'s own shape is
what determines how to un-flatten `plan.k` back into a paddable trajectory
(reshaping to `plan`'s own flattened shape would have been wrong whenever the
original trajectory had more than one non-node dimension). This also
incidentally fixes the same wrong-refusal bug for `threaded` on NFFTOp, which
carried an identical "cannot change after construction, rebuild instead"
argument even though the same trajectory-recovery path resolves it too.

Updated the two tests that asserted the old refusals (RDFT/NFFTOp in
"Threading contract: batching an operator that has no threaded path" and
"...NFFTOp reports its plan-time threading") to instead verify the request is
now honoured and numerically correct.

Verified locally (JULIA_NUM_THREADS=2): full :fftw/:nfft/:Threading/:jet run
(906 tests) and a full Documenter doctest build, both passing. Manually
verified NFFTOp's storage_type copy reproduces identical forward/adjoint
results from a distinct plan object.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHYpJyvm5XwhtMXLE1N8i9
hakkelt and others added 29 commits November 2, 2026 19:35
This reverts commit e7753ac.

The "failed to optimize due to recursion" reports it worked around were
downstream of the non-concrete `size`/`ndims`/`ndoms` chain, now fixed at the
root. With that fix in place the `@generated size` methods infer fine while
binding only the type parameters they use, and the full `:jet` slice passes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An explanatory comment between the docstring and `ndims(L::AbstractOperator)`
detached the docstring from the method, so Documenter failed the build with
`no docs found for 'ndims' in @docs block in docs/src/properties.md`. Move the
comment above the docstring.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`_fbbool(::Type{FastBroadcast.True})` accepted the bare type, so an operator
that stored the type instead of the singleton instance still reported the right
`is_threaded` trait and only failed later, inside `@.. thread = Th`, with
`MethodError: no method matching DataType()`. No caller passes a type any more;
removing the methods turns that mistake into an immediate `MethodError` at the
trait instead of a silent mis-encoding.

Replace the two tests that pinned the type form with a check that the operators
which carry the flag really store an instance.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AGENTS.md allows only a fixed set of lowercase type tags and CamelCase
operator names, so :Threading was neither. Rename it to :threading and
add that tag to the documented list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Four review findings on the block-parallel loops:

- Data race: a threaded block loop called `mul!` on the same operator from
  several tasks when one non-thread-safe operator filled several blocks,
  directly (`DCAT(C, C)`) or wrapped (`DCAT(C, 2 * C)`), so the blocks
  shared its scratch buffers and the output was wrong on every call.
  Each non-thread-safe block now gets its own copy (`_block_loop_operator`).
- Storage: only the first block's storage was checked, so a CPU block in
  front of device blocks threaded the loop. Every block is checked now; a
  multi-domain block counts as CPU when all its components are.
- Lost flag: `copy_operator(dcat; threaded = true)` forced the blocks
  serial even when the constructor then left the block loop serial, and
  `get_normal_op(::DCAT)`, `VCAT.remove_slicing`, `getindex`, `Jacobian`
  and the combination rules rebuilt block operators without their
  block-loop flag, so a `threaded = false` veto was lost. `HCAT(A, buf)`
  and `VCAT(A, buf)` take `threaded` for that.
- `check`: HCAT's `Tuple` `mul!` methods (forward, and serial and threaded
  adjoint) did not call `check`, so a wrong size surfaced as a block's
  error, or a `TaskFailedException` from the threaded loop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- `copy_operator(DiagOp(T, dims, 2.0))` threw a `MethodError` from
  `_array_wrapper(::Float64)`. A scalar diagonal now keeps the operator's
  own storage type.
- Copying an `LBFGS` built a fresh operator and dropped the curvature
  history (`s_M`, `y_M`, `ys_M`, `currmem`, `curridx`, `H`), so the copy
  acted as the identity. The history is the operator's state: the copy now
  starts from it, with its own buffers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- FFTW and DSP operators on device storage reported `is_threaded == true`
  above the size gate: `_fftw_num_threads` and `_dsp_fftw_num_threads`
  never looked at storage, so `RDFT`, `IRDFT`, `Conv` and `Xcorr` on a
  `CuArray` recorded `Threads.nthreads()` and `adapt_operator(op;
  threaded = false)` replanned them on every call. Both now return 1 for
  device storage, whatever was asked.
- Device-backed `MatrixOp`/`LMatrixOp` entered the restricted BLAS scope
  (a process-wide lock plus a setter round trip for every pool) on every
  `mul!`, although a device `gemm` uses none of those pools. The scope is
  now skipped for non-CPU storage: 2x2 `JLArray` `MatrixOp` `mul!` 5.44 us
  and 64 B before, 0.073 us and 0 B after (bare `mul!`: 0.064 us); the
  CPU `threaded = false` path is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`==` on SimpleBatchOp and SpreadingBatchOp compared the wrapped operators
and the sizes but not the domain/codomain batch masks, so two batch
operators batching along different axes compared equal while giving
different results.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`threaded = true` only permits threading, and with `-t 1` it correctly
resolves to serial, but the threading contract helper and the threading
traits tests asserted `is_threaded(...) == true` unconditionally. CI only
passed because it sets `JULIA_NUM_THREADS: 2`. Those assertions now
expect `Threads.nthreads() > 1`; none is removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It was the one :gpu testitem still on `setup = [TestUtils]` only. Without
`GpuEnvSetup`, `gpu_backends()` returns nothing and the testitem passes
without asserting anything, or not, depending on test order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
copy_operator, get_normal_op, permute, remove_displacement and Jacobian
passed blocks that share no scratch through the constructor, which copied
the non-thread-safe ones again. copy_operator(D; threaded = true) also
built threaded block copies (FFTW plans) only to replace them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The JLArray gemm allocates by itself, so the test failed on CI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Batch operators fell back to `supports_threading == false`, so
`adapt_operator(b; threaded = false)` returned a threaded batch op unchanged.
Their copies also dropped `threaded` for the wrapped operator, while a serial
batch loop reported `is_threaded == false` even around a threaded operator.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copying every block that is not thread-safe detached the loop's blocks from the
caller's objects, so e.g. `update!` on an `LBFGS` block no longer reached a
threaded DCAT. Copy a block only when it shares a non-thread-safe operator with
another block.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`is_threaded` and `supports_threading` now default to asking the operator's
children, so the forwarders no longer repeat the same two methods and a new
wrapper cannot forget them. Also:

- `_block_loop_operators` walks each block's tree once.
- `_children(::SpreadingBatchOpCopying)` iterates instead of allocating.
- `_should_thread(op, storage_type)` takes `storage_type` as a default argument.
- `DiagOp` copy uses `_fbbool`.
- `DFT`/`IRDFT` copies with nothing to change return the operator itself.
- Batch operator docstrings no longer mention a `nothing` default for `threaded`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- `is_threaded` and `supports_threading` referred to the internal `_children` with an
  `@ref` that Documenter cannot resolve, which fails the docs build.
- The `_children` docstring still promised a tuple and described the old `false`
  default.
- The `SimpleBatchOp` comment described a removed method.
- The `DFT`/`IRDFT` copy comments now say why returning `op` is a copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The `update!` call used random gradients, so the curvature check could reject
the pair and leave the operator an identity map that any stale copy would also
match. Use `g = 2x`, assert that the update changed the operator, and compare
with `≈`: inside the block loop BLAS runs single-threaded and rounds the LBFGS
dot products differently, which failed the exact comparison on Julia 1.10.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…wdown

- A literal power in a threaded `@..` broadcast passes `^` and the exponent in
  `Ref`s, which cost 2 allocations per call in Sigmoid forward and the Tanh and
  Atan Jacobian-adjoints. Use `inv` or a scalar helper instead.
- The fused Sigmoid Jacobian-adjoint was ~1.3x slower than master on one thread.
  Computing `exp` in its own pass first is faster both serial and threaded
  (32768 elements: 264 -> 187 us serial, 146 -> 102 us on 2 threads).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds ContourletOp and NSCTOp linear operators wrapping the discrete
Contourlet Transform and Nonsubsampled Contourlet Transform from
Contourlets.jl, following the existing subpackage pattern (WaveletOperators,
FFTWOperators, DSPOperators, NFFTOperators). Coefficients (coarse band +
directional subbands) are exposed as a flat RecursiveArrayTools.ArrayPartition
codomain, matching this repo's HCAT/DCAT multi-component convention.

Each operator owns a preallocated Contourlets.ContourletWorkspace and
coefficient buffer, so forward and adjoint mul! reuse scratch memory across
calls instead of allocating per call; a threading keyword exposes
Contourlets.jl's ThreadingPolicy (Auto/Enabled/Disabled) to control the
directional filter bank stage.

With the default biorthogonal (CDF97/Q2345) filters neither transform is
self-adjoint, so the adjoint operator is wired to the declared inverse
transform (a perfect-reconstruction left inverse) rather than the literal
linear-algebra transpose; documented on both operator types, and covered
by round-trip/in-place tests instead of the usual adjoint dot-product
invariant test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SUAy2tC1kyndjCrhNfZdoc
Fixes 'expected package ContourletOperators to be registered' failure
on the LTS matrix job — it wasn't in the rm/develop lists alongside
the other local subpackages.
similar_coefficients was using the raw requested T instead of the
promoted Td that make_workspace already computes internally, causing
a MethodError on the first mul! whenever T was narrower than the
params' filter precision. Also unify ContourletOp/NSCTOp into a
single ContourletTransformOp struct distinguished by a Kind trait,
removing the ~110-line duplication between the two operators.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016vzNfSioXkuA1P84Y7BzzP
…IDCT convention

The runtime Kind-field + const-alias approach diverged from the codebase's
established pattern for near-duplicate transform pairs (FFTWOperators'
DCT/IDCT: one abstract supertype for shared traits, two separate concrete
structs for the rest). Switch to that pattern instead: ContourletTransform
carries size/domain_type/codomain_type/is_invertible/is_thread_safe once,
while ContourletOp/NSCTOp keep their own constructors, dispatch barriers,
mul! pairs, and fun_name. Drops the fun_name/kind-field indirection and the
confusing ContourletTransformOp name (it backed NSCT too). Behavior and
public API unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016vzNfSioXkuA1P84Y7BzzP
Adds an array_type constructor keyword (mirrors WaveletOp) so domain_array_type/
codomain_array_type report GPU storage. Contourlets.jl itself stays CPU-only (FFTW
plans, scalar filter-bank loops), so mul! stages x/y through an internal CPU buffer
(copyto! in, run the CPU transform, copyto! out) instead of executing on-device.

Verified on real CUDA hardware (Quadro RTX 6000): exact forward/adjoint round-trip
with array_type = CuArray{Float64}. Extended test_jet.jl's @test_call coverage for
the new array_type kwarg.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016vzNfSioXkuA1P84Y7BzzP
The branch listed `contourletoperators.jl` in the suite index but never carried the file, which
only became visible once the branch moved onto the restructured `benchmark/suites/` layout. The
file is the one MriReconstructionToolbox has been vendoring.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@hakkelt
hakkelt force-pushed the add-contourlet-operators branch from e69ca6d to 4f1141f Compare November 3, 2026 16:36

This branch has not been deployed

No deployments
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.

1 participant