Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
c3b89d4
Update dependencies and compatibility versions
JaredW40 Dec 12, 2025
7103463
Add LuxCUDA dependency to Project.toml
JaredW40 Dec 12, 2025
f7a008e
Refactor CUDA.cu functions for FFTW plans
JaredW40 Dec 18, 2025
25cb1cc
Update Project.toml dependencies
JaredW40 Dec 18, 2025
9021c10
Merge remote changes and update Project.toml
JaredW40 Dec 18, 2025
d079932
Replace gradient function with Flux.gradient
JaredW40 Dec 18, 2025
25a5bb6
Replace gradient with Flux.gradient in tests
JaredW40 Dec 18, 2025
d7f8485
Replace gradient with Flux.gradient in tests
JaredW40 Dec 18, 2025
0bd4c0d
Update test assertions for CUDA FFT plans
JaredW40 Dec 18, 2025
dfe813a
Update Flux parameter checks in ConvFFT tests
JaredW40 Dec 18, 2025
a686602
Refactor ConvFFT tests for CPU and GPU support
JaredW40 Dec 18, 2025
70986b9
Update tests to use 'all' instead of 'minimum'
JaredW40 Dec 18, 2025
65739ef
Replace minimum tests with add function tests
JaredW40 Dec 18, 2025
f54ed3c
Fix argument format in effectiveSize call
JaredW40 Dec 18, 2025
911550f
Replace 'add' with 'all' in boundary tests
JaredW40 Dec 18, 2025
2c024bc
Merge pull request #3 from JaredW40/master
BoundaryValueProblems Dec 20, 2025
55b75f0
Bump version to 0.3.6
BoundaryValueProblems Dec 20, 2025
96ea78c
Change Flux.@functor to Flux.@layer for ConvFFT
JaredW40 Dec 20, 2025
816fb8c
Fix type annotation for Pad function in boundaries.jl
JaredW40 Dec 20, 2025
42c78d8
Fix syntax in convFFTConstructors.jl
JaredW40 Dec 20, 2025
227918b
Add CUDA import for GPU compatibility
JaredW40 Dec 20, 2025
4d9fdb4
Import CUDA for GPU and CPU conversion
JaredW40 Dec 20, 2025
6559274
Add CUDA initialization to FourierFilterFlux module
JaredW40 Dec 20, 2025
ed3b41a
Add conditional CUDA initialization
JaredW40 Dec 21, 2025
8d3dbd0
Enable CUDA usage in FourierFilterFlux.jl
JaredW40 Dec 21, 2025
c7b433c
Merge pull request #4 from JaredW40/master
BoundaryValueProblems Dec 21, 2025
17e8003
Bump version to 0.3.7
BoundaryValueProblems Dec 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "FourierFilterFlux"
uuid = "3d7dfd45-6c90-4c9b-b697-194a05757159"
authors = ["dsweber2"]
version = "0.3.5"
version = "0.3.7"

[deps]
AbstractFFTs = "621f4979-c628-5d54-868e-fcf4e3e8185c"
Expand Down
6 changes: 4 additions & 2 deletions src/FourierFilterFlux.jl
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
module FourierFilterFlux
using Reexport
# @reexport using CUDA
using CUDA
using Zygote, Flux, Adapt, LinearAlgebra
using AbstractFFTs, FFTW # TODO: check the license on FFTW and such
using ContinuousWavelets
using RecipesBase
using CUDA

const use_cuda = Ref(false)
if CUDA.functional()
use_cuda[] = true
end

import Adapt: adapt
export pad, originalDomain, formatJLD, getBatchSize
Expand Down
31 changes: 19 additions & 12 deletions src/Utils.jl
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import NNlib.relu
# just a little bit of type piracy used internally TODO maybe don't...
relu(x::C) where {C<:Complex} = real(x) > 0 ? x : C(0)

import CUDA: CuArray
# ways to convert between gpu and cpu
import Adapt.adapt
function adapt(to, cft::ConvFFT{D,OT,F,A,V,PD,P,T,An}) where {D,OT,F,A,V,PD,P,T,An}
Expand Down Expand Up @@ -38,16 +38,23 @@ function cu(cft::ConvFFT{D,OT,F,A,V,PD,P,T,An}) where {D,OT,F,A,V,PD,P,T,An}
cft.analytic)
end

# TODO this is somewhat kludgy, not sure why cu was converting these back
#function CUDA.cu(P::FFTW.rFFTWPlan)
# return plan_rfft(cu(zeros(real(eltype(P)), P.sz)), P.region)
#end
#CUDA.cu(P::CUFFT.rCuFFTPlan) = P
# Only convert FFTW plans to CUFFT plans if CUDA is actually functional
function CUDA.cu(P::FFTW.rFFTWPlan)
if CUDA.functional()
return plan_rfft(CUDA.cu(zeros(real(eltype(P)), P.sz)), P.region)
else
return P # fallback to CPU FFTW plan
end
end

#function CUDA.cu(P::FFTW.cFFTWPlan)
# return plan_fft(cu(zeros(eltype(P), P.sz)), P.region)
#end
#CUDA.cu(P::CUFFT.cCuFFTPlan) = P
function CUDA.cu(P::FFTW.cFFTWPlan)
if CUDA.functional()
return plan_fft(CUDA.cu(zeros(eltype(P), P.sz)), P.region)
else
return P # fallback to CPU FFTW plan
end
end
CUDA.cu(P::CUDA.CUFFT.Plan) = P

Adapt.adapt(::Type{Array{T}}, P::FFTW.FFTWPlan{T}) where {T} = P
function Adapt.adapt(::Type{Array{T}}, P::FFTW.rFFTWPlan) where {T}
Expand All @@ -62,8 +69,8 @@ adapt(::Type{<:CuArray}, x::T) where {T<:CUDA.CUFFT.CuFFTPlan} = x
# is actually converting
function adapt(::Union{Type{<:Array},Flux.FluxCPUAdaptor},
x::T) where {T<:CUDA.CUFFT.CuFFTPlan}
transformSize = x.osz
dataSize = x.sz
transformSize = x.output_size
dataSize = x.input_size
if dataSize != transformSize
# this is an rfft, since the dimension isn't preserved
return plan_rfft(zeros(dataSize), x.region)
Expand Down
2 changes: 1 addition & 1 deletion src/boundaries.jl
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ end

N gives the number of dimensions of convolution, while `x` gives the specific amount to pad in each dimension (done on both sides). If the values in `x` are negative, then the support of the filters will be determined automataically
"""
Pad(x::Vararg{<:Integer,N}) where {N} = Pad{N}(x)
Pad(x::Vararg{Integer,N}) where {N} = Pad{N}(x)
import Base.ndims
ndims(p::Pad{N}) where {N} = N

Expand Down
2 changes: 1 addition & 1 deletion src/convFFTConstructors.jl
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ function waveletLayer(inputSize::Union{T,NTuple{N,T}};
averagingStyle = RealWaveletComplexSignal
end
An = map(ii -> ((ii in An) ? averagingStyle() :
AnalyticWavelet()), (1:size(wavelets, 2)[end]...,))
AnalyticWavelet()), 1:size(wavelets, 2))
end
if bias
bias = dType.(init(inputSize[2:end-1]..., size(wavelets, 2)))
Expand Down
2 changes: 1 addition & 1 deletion src/paramCollection.jl
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Flux.@functor ConvFFT
Flux.@layer ConvFFT

function Flux.trainable(CFT::ConvFFT{A,B,C,D,E,F,G,true}) where {A,B,C,D,E,F,G}
(CFT.weight, CFT.bias)
Expand Down
2 changes: 1 addition & 1 deletion src/transforms.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# TODO: version that doesn't have an fft built in

import CUDA: CuArray
function (shears::ConvFFT)(x)
if typeof(shears.weight) <: CuArray && !(typeof(x) <: CuArray)
error("don't try to apply a gpu transform to a non-CuArray")
Expand Down
14 changes: 7 additions & 7 deletions test/CUDATests.jl
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
if CUDA.functional()
@testset "CUDA methods" begin
w = ConvFFT((100,), nConvDims = 1)
@test cu(w.fftPlan) isa CUFFT.rCuFFTPlan # does cu work on the fft plans when applied directly?
@test cu(w.fftPlan) isa CUDA.CUFFT.CuFFTPlan # does cu work on the fft plans when applied directly?
cw = cu(w)
@test cw.weight isa NTuple{N,CuArray} where {N} # does cu work on the weights?
@test cw.fftPlan isa CUFFT.rCuFFTPlan # does cu work on the fftPlan?
@test cw.fftPlan isa CUDA.CUFFT.CuFFTPlan # does cu work on the fftPlan?
cw1 = gpu(w)
@test cw1.weight isa NTuple{N,CuArray} where {N} # does gpu work on the weights?
@test cw1.fftPlan isa CUFFT.rCuFFTPlan # does gpu work on the fftPlan?
@test cw1.fftPlan isa CUDA.CUFFT.CuFFTPlan # does gpu work on the fftPlan?
w1 = cpu(cw)
@test w1.weight isa NTuple{N,Array} where {N} # does cpu work on the weights?
@test w1.fftPlan isa FFTW.rFFTWPlan # does cpu work on the fftPlan?
Expand All @@ -16,15 +16,15 @@ if CUDA.functional()
@test cw(cx) isa CuArray
@test cw(cx) ≈ cu(w(x)) # CUDA and cpu version get the same result approximately
cw(cx)
∇cu = gradient(t -> sum(cw(t)), cx)[1]
∇ = gradient(t -> sum(w(t)), x)[1]
∇cu = Flux.gradient(t -> sum(cw(t)), cx)[1]
∇ = Flux.gradient(t -> sum(w(t)), x)[1]
@test ∇ ≈ cpu(∇cu)
w1 = waveletLayer((100, 1, 1))
cw1 = cu(w1)
@test cw1(cx) ≈ cu(w1(x))

CUDA.@allowscalar ∇cu = gradient(t -> abs(cw1(t)[1]), cx)[1]
CUDA.@allowscalar ∇ = gradient(t -> abs(w1(t)[1]), x)[1]
CUDA.@allowscalar ∇cu = Flux.gradient(t -> abs(cw1(t)[1]), cx)[1]
CUDA.@allowscalar ∇ = Flux.gradient(t -> abs(w1(t)[1]), x)[1]
@test ∇ ≈ cpu(∇cu)
end
end
83 changes: 45 additions & 38 deletions test/ConvFFTConstructors.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# TODO: add some checks for different boundary conditions
# TODO: add checks for analytic wavelets
# ConvFFT constructor tests
using FourierFilterFlux: applyWeight, applyBC, internalConvFFT
@testset "ConvFFT constructors" begin
@testset "Utils" begin
explicit = [1 0 0; 0 1 0; 0 0 1; zeros(2, 3)]
Expand All @@ -22,16 +23,17 @@

shears = ConvFFT(weightMatrix, nothing, originalSize, abs,
plan = true, boundary = Pad(padding), trainable = true)
@test Flux.params(shears).order[1] == shears.weight[1]
@test length(Flux.params(shears).order) == 1
trn = Flux.trainable(shears)
@test trn[1][1] == shears.weight[1]
@test length(trn) >= 1

shears = ConvFFT(weightMatrix, nothing, originalSize, abs,
boundary = Pad(padding), trainable = false)
@test isempty(Flux.params(shears))
@test isempty(Flux.trainable(shears))

x = randn(21, 11, 1, 10)
∇ = gradient((x) -> shears(x)[1, 1, 1, 1, 3], x)
@test minimum(∇[1][:, :, :, [1:2..., 4:10...]] .≈ 0)
∇ = Flux.gradient((x) -> shears(x)[1, 1, 1, 1, 3], x)
@test all(∇[1][:, :, :, [1:2..., 4:10...]] .≈ 0)

# check that the identity ConvFFT is, in fact, an identity
weightMatrix = ones(Float32, (21 + 10) >> 1 + 1, 11 + 10, 1)
Expand Down Expand Up @@ -59,7 +61,7 @@
nextLayer = FourierFilterFlux.internalConvFFT(x̂, shears.weight, usedInds,
shears.fftPlan, shears.bias,
shears.analytic)
∇ = gradient((x̂) -> FourierFilterFlux.internalConvFFT(x̂,
∇ = Flux.gradient((x̂) -> FourierFilterFlux.internalConvFFT(x̂,
shears.weight,
usedInds,
shears.fftPlan,
Expand All @@ -69,20 +71,20 @@
1,
1],
x̂)
@test minimum(abs.(diag(∇[1][:, :, 1, 1])) .≈ 2.0f0 / 31 / 21)
@test all(abs.(diag(∇[1][:, :, 1, 1])) .≈ 2.0f0 / 31 / 21)

ax = axes(x̂)[3:end-1]
∇ = gradient((x̂) -> FourierFilterFlux.applyWeight(x̂, shears.weight[1], usedInds,
∇ = Flux.gradient((x̂) -> FourierFilterFlux.applyWeight(x̂, shears.weight[1], usedInds,
shears.fftPlan,
shears.bias, FourierFilterFlux.NonAnalyticMatching())[1,
1,
1,
1,
1], x̂)
@test minimum(abs.(diag(∇[1][:, :, 1, 1])) .≈ 2.0f0 / 31 / 21)
@test all(abs.(diag(∇[1][:, :, 1, 1])) .≈ 2.0f0 / 31 / 21)

∇ = gradient((x̂) -> (shears.fftPlan\(x̂.*shears.weight[1]))[1, 1, 1, 1], x̂)
@test minimum(abs.(diag(∇[1][:, :, 1, 1])) .≈ 1.0f0 / 31 * 2 / 21)
∇ = Flux.gradient((x̂) -> (shears.fftPlan\(x̂.*shears.weight[1]))[1, 1, 1, 1], x̂)
@test all(abs.(diag(∇[1][:, :, 1, 1])) .≈ 1.0f0 / 31 * 2 / 21)
sheared = shears(x)
@test size(sheared) == (21, 11, 1, 1, 10)

Expand All @@ -97,11 +99,11 @@
if CUDA.functional()
gpuVer = shears |> gpu
@test gpuVer.weight[1] isa CuArray
@test gpuVer.fftPlan isa CUFFT.rCuFFTPlan
@test gpuVer.fftPlan isa CUDA.CUFFT.CuFFTPlan
if !(gpuVer.weight[1] isa CuArray)
println("gpuVer.weight is of type $(typeof(gpuVer.weight))")
end
if !(gpuVer.fftPlan isa CUFFT.rCuFFTPlan)
if !(gpuVer.fftPlan isa CUDA.CUFFT.CuFFTPlan)
println("gpuVer.fftPlan is of type $(typeof(gpuVer.fftPlan))")
end
end
Expand Down Expand Up @@ -147,15 +149,17 @@
@test shears.σ == abs
@test shears.bias == nothing
@test shears.bc.padBy == (5,)
@test Flux.params(shears).order[1] == shears.weight[1]
trn = Flux.trainable(shears)
@test trn[1][1] == shears.weight[1]
@test length(trn) >= 1

shears = ConvFFT(weightMatrix, nothing, originalSize, abs,
plan = true, boundary = Pad(padding), trainable = false)
@test isempty(Flux.params(shears))
@test isempty(Flux.trainable(shears))

x = randn(21, 1, 10)
∇ = gradient((x) -> shears(x)[1, 1, 1, 3], x)
@test minimum(∇[1][:, :, [1:2..., 4:10...]] .≈ 0)
∇ = Flux.gradient((x) -> shears(x)[1, 1, 1, 3], x)
@test all(∇[1][:, :, [1:2..., 4:10...]] .≈ 0)

# Sym test
weightMatrix = randn(Float32, (21 + 1), 1)
Expand All @@ -165,23 +169,27 @@
@test shears.σ == abs
@test shears.bias == nothing
@test typeof(shears.bc) <: Sym
@test Flux.params(shears).order[1] == shears.weight[1]
trn = Flux.trainable(shears)
@test trn[1][1] == shears.weight[1]
@test length(trn) >= 1
x = randn(21, 1, 10)
∇ = gradient((x) -> shears(x)[1, 1, 1, 3], x)
@test minimum(∇[1][:, :, [1:2..., 4:10...]] .≈ 0)
@test minimum(abs.(∇[1][:, 1, 3])) > 0
∇ = Flux.gradient((x) -> shears(x)[1, 1, 1, 3], x)
@test all(∇[1][:, :, [1:2..., 4:10...]] .≈ 0)
@test all(abs.(∇[1][:, 1, 3]) .> 0)
weightMatrix = randn(Float32, 21 >> 1 + 1, 1)
shears = ConvFFT(weightMatrix, nothing, originalSize, abs,
plan = true, boundary = FourierFilterFlux.Periodic())
@test size(shears.fftPlan) == originalSize
@test shears.σ == abs
@test shears.bias == nothing
@test typeof(shears.bc) <: FourierFilterFlux.Periodic
@test Flux.params(shears).order[1] == shears.weight[1]
trn = Flux.trainable(shears)
@test trn[1][1] == shears.weight[1]
@test length(trn) >= 1
x = randn(21, 1, 10)
∇ = gradient((x) -> shears(x)[1, 1, 1, 3], x)
@test minimum(∇[1][:, :, [1:2..., 4:10...]] .≈ 0)
@test minimum(abs.(∇[1][:, 1, 3])) > 0
∇ = Flux.gradient((x) -> shears(x)[1, 1, 1, 3], x)
@test all(∇[1][:, :, [1:2..., 4:10...]] .≈ 0)
@test all(abs.(∇[1][:, 1, 3]) .> 0)
end

# check that the identity ConvFFT is, in fact, an identity
Expand Down Expand Up @@ -235,7 +243,6 @@
end


using FourierFilterFlux: applyWeight, applyBC, internalConvFFT
weight = (2 .* ones(Complex{Float32}, (21 + 10) >> 1 + 1),)
bc = Pad(5)
x = randn(Float32, 21, 1, 10)
Expand All @@ -244,13 +251,13 @@
fftPlan = plan_rfft(xbc, (1,))
An = map(x -> FourierFilterFlux.NonAnalyticMatching(), (1:length(weight)...,))
nextLayer = internalConvFFT(x̂, weight, usedInds, fftPlan, nothing, An)
∇ = gradient((x̂) -> internalConvFFT(x̂, weight, usedInds, fftPlan, nothing, An)[1,
∇ = Flux.gradient((x̂) -> internalConvFFT(x̂, weight, usedInds, fftPlan, nothing, An)[1,
1,
1,
1,
1],
x̂)
y, ∂ = pullback((x̂) -> internalConvFFT(x̂, weight, usedInds, fftPlan, nothing, An)[1,
y, ∂ = Zygote.pullback((x̂) -> internalConvFFT(x̂, weight, usedInds, fftPlan, nothing, An)[1,
1,
1,
1,
Expand All @@ -259,12 +266,12 @@
∂(y)
∂(y) # repeated calls to the derivative were causing errors while argWrapper
# was in use
@test minimum(abs.(∇[1][:, 1, 1]) .≈ 2.0f0 / 31)
@test all(abs.(∇[1][:, 1, 1]) .≈ 2.0f0 / 31)
# no bias, not analytic and real valued output

# no bias, analytic (so complex valued)
fftPlan = plan_fft(xbc, (1,))
∇ = gradient((x̂) -> abs(applyWeight(x̂,
∇ = Flux.gradient((x̂) -> abs(applyWeight(x̂,
weight[1],
usedInds,
fftPlan,
Expand All @@ -274,7 +281,7 @@
1,
1]),
x̂)
@test minimum(abs.(∇[1][:, 1, 1]) .≈ 2.0f0 / 31)
@test all(abs.(∇[1][:, 1, 1]) .≈ 2.0f0 / 31)

# no bias, not analytic, complex valued, but still symmetric
real(applyWeight(x̂,
Expand All @@ -284,7 +291,7 @@
nothing,
FourierFilterFlux.RealWaveletRealSignal()))
fftPlan = plan_fft(xbc, (1,))
∇ = gradient((x̂) -> real(applyWeight(x̂,
∇ = Flux.gradient((x̂) -> real(applyWeight(x̂,
weight[1],
usedInds,
fftPlan,
Expand All @@ -294,7 +301,7 @@
1,
1]),
x̂)
@test minimum(abs.(∇[1][2:end, 1, 1]) .≈ 2 * 2.0f0 / 31)
@test all(abs.(∇[1][2:end, 1, 1]) .≈ 2 * 2.0f0 / 31)
@test abs(∇[1][1, 1, 1]) ≈ 2.0f0 / 31

# internal methods tests
Expand All @@ -309,7 +316,7 @@
nextLayer = FourierFilterFlux.internalConvFFT(x̂, shears.weight, usedInds,
shears.fftPlan,
shears.bias, shears.analytic)
∇ = gradient((x̂) -> FourierFilterFlux.internalConvFFT(x̂,
∇ = Flux.gradient((x̂) -> FourierFilterFlux.internalConvFFT(x̂,
shears.weight,
usedInds,
shears.fftPlan,
Expand All @@ -319,24 +326,24 @@
1,
1],
x̂)
@test minimum(abs.(∇[1][:, 1, 1]) .≈ 2.0f0 / 31)
@test all(abs.(∇[1][:, 1, 1]) .≈ 2.0f0 / 31)
#

# no bias, not analytic and real valued output
# no bias, analytic (so complex valued)
# no bias, not analytic, complex valued, but still symmetric
# biased (and one of the others, doesn't matter which)

∇ = gradient((x̂) -> (shears.fftPlan\(x̂.*shears.weight[1]))[1, 1, 1, 1], x̂)
@test minimum(abs.(∇[1][:, :, 1, 1]) .≈ 1.0f0 / 31 * 2)
∇ = Flux.gradient((x̂) -> (shears.fftPlan\(x̂.*shears.weight[1]))[1, 1, 1, 1], x̂)
@test all(abs.(∇[1][:, :, 1, 1]) .≈ 1.0f0 / 31 * 2)
sheared = shears(x)
@test size(sheared) == (21, 1, 1, 10)

# convert to a gpu version
if CUDA.functional()
gpuVer = shears |> gpu
@test gpuVer.weight[1] isa CuArray
@test gpuVer.fftPlan isa CUFFT.rCuFFTPlan
@test gpuVer.fftPlan isa CUDA.CUFFT.CuFFTPlan
end
# extra channel dimension
originalSize = (20, 16, 1, 10)
Expand Down
Loading
Loading