From 1c69f6c9576fb6adef0606b28f5c4f050c6b7dac Mon Sep 17 00:00:00 2001 From: Gaurav Arya Date: Sat, 8 Jul 2023 21:10:24 -0400 Subject: [PATCH 01/18] Add TestUtils submodule/extension --- Project.toml | 3 + ext/AbstractFFTsTestUtilsExt.jl | 234 ++++++++++++++++++++++++++++ src/AbstractFFTs.jl | 2 + src/TestUtils.jl | 15 ++ test/{testplans.jl => TestPlans.jl} | 60 +++++-- test/runtests.jl | 141 ++--------------- 6 files changed, 309 insertions(+), 146 deletions(-) create mode 100644 ext/AbstractFFTsTestUtilsExt.jl create mode 100644 src/TestUtils.jl rename test/{testplans.jl => TestPlans.jl} (90%) diff --git a/Project.toml b/Project.toml index 3a6a88b8..f069e4f9 100644 --- a/Project.toml +++ b/Project.toml @@ -5,12 +5,15 @@ version = "1.4.0" [deps] ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [weakdeps] ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [extensions] AbstractFFTsChainRulesCoreExt = "ChainRulesCore" +AbstractFFTsTestUtilsExt = "Test" [compat] ChainRulesCore = "1" diff --git a/ext/AbstractFFTsTestUtilsExt.jl b/ext/AbstractFFTsTestUtilsExt.jl new file mode 100644 index 00000000..819a532f --- /dev/null +++ b/ext/AbstractFFTsTestUtilsExt.jl @@ -0,0 +1,234 @@ +# This file contains code that was formerly part of Julia. License is MIT: https://julialang.org/license + +module AbstractFFTsTestUtilsExt + +using AbstractFFTs +using AbstractFFTs: TestUtils +using AbstractFFTs.LinearAlgebra +using Test + +# Ground truth _x_fft computed using FFTW library +const TEST_CASES = ( + (; x = collect(1:7), dims = 1, + x_fft = [28.0 + 0.0im, + -3.5 + 7.267824888003178im, + -3.5 + 2.7911568610884143im, + -3.5 + 0.7988521603655248im, + -3.5 - 0.7988521603655248im, + -3.5 - 2.7911568610884143im, + -3.5 - 7.267824888003178im]), + (; x = collect(1:8), dims = 1, + x_fft = [36.0 + 0.0im, + -4.0 + 9.65685424949238im, + -4.0 + 4.0im, + -4.0 + 1.6568542494923806im, + -4.0 + 0.0im, + -4.0 - 1.6568542494923806im, + -4.0 - 4.0im, + -4.0 - 9.65685424949238im]), + (; x = collect(reshape(1:8, 2, 4)), dims = 2, + x_fft = [16.0+0.0im -4.0+4.0im -4.0+0.0im -4.0-4.0im; + 20.0+0.0im -4.0+4.0im -4.0+0.0im -4.0-4.0im]), + (; x = collect(reshape(1:9, 3, 3)), dims = 2, + x_fft = [12.0+0.0im -4.5+2.598076211353316im -4.5-2.598076211353316im; + 15.0+0.0im -4.5+2.598076211353316im -4.5-2.598076211353316im; + 18.0+0.0im -4.5+2.598076211353316im -4.5-2.598076211353316im]), + (; x = collect(reshape(1:8, 2, 2, 2)), dims = 1:2, + x_fft = cat([10.0 + 0.0im -4.0 + 0.0im; -2.0 + 0.0im 0.0 + 0.0im], + [26.0 + 0.0im -4.0 + 0.0im; -2.0 + 0.0im 0.0 + 0.0im], + dims=3)), + (; x = collect(1:7) + im * collect(8:14), dims = 1, + x_fft = [28.0 + 77.0im, + -10.76782488800318 + 3.767824888003175im, + -6.291156861088416 - 0.7088431389115883im, + -4.298852160365525 - 2.7011478396344746im, + -2.7011478396344764 - 4.298852160365524im, + -0.7088431389115866 - 6.291156861088417im, + 3.767824888003177 - 10.76782488800318im]), + (; x = collect(reshape(1:8, 2, 2, 2)) + im * reshape(9:16, 2, 2, 2), dims = 1:2, + x_fft = cat([10.0 + 42.0im -4.0 - 4.0im; -2.0 - 2.0im 0.0 + 0.0im], + [26.0 + 58.0im -4.0 - 4.0im; -2.0 - 2.0im 0.0 + 0.0im], + dims=3)), + ) + +""" + TestUtils.test_complex_fft(ArrayType=Array; test_real=true, test_inplace=true) + +Run tests to verify correctness of FFT/BFFT/IFFT functionality using a particular backend plan implementation. +The backend implementation is assumed to be loaded prior to calling this function. + +# Arguments + +- `ArrayType`: determines the `AbstractArray` implementation for + which the correctness tests are run. Arrays are constructed via + `convert(ArrayType, ...)`. +- `test_inplace=true`: whether to test in-place plans. +""" +function TestUtils.test_complex_fft(ArrayType=Array; test_inplace=true) + @testset "correctness of fft, bfft, ifft" begin + for test_case in TEST_CASES + _x, dims, _x_fft = test_case.x, test_case.dims, test_case.x_fft + x = convert(ArrayType, _x) # dummy array that will be passed to plans + x_complexf = convert(ArrayType, complex.(float.(x))) # for testing mutating complex FFTs + x_fft = convert(ArrayType, _x_fft) + + # FFT + @test fft(x, dims) ≈ x_fft + if test_inplace + _x_complexf = copy(x_complexf) + @test fft!(_x_complexf, dims) ≈ x_fft + @test _x_complexf ≈ x_fft + end + # test OOP plans, checking plan_fft and also inv of plan_ifft, + # which should give functionally identical plans + for P in (plan_fft(similar(x_complexf), dims), inv(plan_ifft(similar(x_complexf), dims))) + @test eltype(P) <: Complex + @test fftdims(P) == dims + @test P * x ≈ x_fft + @test P \ (P * x) ≈ x + _x_out = similar(x_fft) + @test mul!(_x_out, P, x_complexf) ≈ x_fft + @test _x_out ≈ x_fft + end + if test_inplace + # test IIP plans + for P in (plan_fft!(similar(x_complexf), dims), inv(plan_ifft!(similar(x_complexf), dims))) + @test eltype(P) <: Complex + @test fftdims(P) == dims + _x_complexf = copy(x_complexf) + @test P * _x_complexf ≈ x_fft + @test _x_complexf ≈ x_fft + @test P \ _x_complexf ≈ x + @test _x_complexf ≈ x + end + end + + # BFFT + x_scaled = prod(size(x, d) for d in dims) .* x + @test bfft(x_fft, dims) ≈ x_scaled + if test_inplace + _x_fft = copy(x_fft) + @test bfft!(_x_fft, dims) ≈ x_scaled + @test _x_fft ≈ x_scaled + end + # test OOP plans. Just 1 plan to test, but we use a for loop for consistent style + for P in (plan_bfft(similar(x_fft), dims),) + @test eltype(P) <: Complex + @test fftdims(P) == dims + @test P * x_fft ≈ x_scaled + @test P \ (P * x_fft) ≈ x_fft + _x_complexf = similar(x_complexf) + @test mul!(_x_complexf, P, x_fft) ≈ x_scaled + @test _x_complexf ≈ x_scaled + end + # test IIP plans + for P in (plan_bfft!(similar(x_fft), dims),) + @test eltype(P) <: Complex + @test fftdims(P) == dims + _x_fft = copy(x_fft) + @test P * _x_fft ≈ x_scaled + @test _x_fft ≈ x_scaled + @test P \ _x_fft ≈ x_fft + @test _x_fft ≈ x_fft + end + + # IFFT + @test ifft(x_fft, dims) ≈ x + if test_inplace + _x_fft = copy(x_fft) + @test ifft!(_x_fft, dims) ≈ x + @test _x_fft ≈ x + end + # test OOP plans + for P in (plan_ifft(similar(x_complexf), dims), inv(plan_fft(similar(x_complexf), dims))) + @test eltype(P) <: Complex + @test fftdims(P) == dims + @test P * x_fft ≈ x + @test P \ (P * x_fft) ≈ x_fft + _x_complexf = similar(x_complexf) + @test mul!(_x_complexf, P, x_fft) ≈ x + @test _x_complexf ≈ x + end + # test IIP plans + if test_inplace + for P in (plan_ifft!(similar(x_complexf), dims), inv(plan_fft!(similar(x_complexf), dims))) + @test eltype(P) <: Complex + @test fftdims(P) == dims + _x_fft = copy(x_fft) + @test P * _x_fft ≈ x + @test _x_fft ≈ x + @test P \ _x_fft ≈ x_fft + @test _x_fft ≈ x_fft + end + end + end + end +end + +""" + TestUtils.test_real_fft(ArrayType=Array; test_real=true, test_inplace=true) + +Run tests to verify correctness of RFFT/BRFFT/IRFFT functionality using a particular backend plan implementation. +The backend implementation is assumed to be loaded prior to calling this function. + +# Arguments + +- `ArrayType`: determines the `AbstractArray` implementation for + which the correctness tests are run. Arrays are constructed via + `convert(ArrayType, ...)`. +- `test_inplace=true`: whether to test in-place plans. +""" +function TestUtils.test_real_fft(ArrayType=Array; test_inplace=true) + @testset "correctness of rfft, brfft, irfft" begin + for test_case in TEST_CASES[5:5] + _x, dims, _x_fft = test_case.x, test_case.dims, test_case.x_fft + x = convert(ArrayType, _x) # dummy array that will be passed to plans + x_real = float.(x) # for testing mutating real FFTs + x_fft = convert(ArrayType, _x_fft) + x_rfft = selectdim(x_fft, first(dims), 1:(size(x_fft, first(dims)) ÷ 2 + 1)) + + if !(eltype(x) <: Real) + continue + end + + # RFFT + @test rfft(x, dims) ≈ x_rfft + for P in (plan_rfft(similar(x_real), dims), inv(plan_irfft(similar(x_rfft), size(x, first(dims)), dims))) + @test eltype(P) <: Real + @test fftdims(P) == dims + # Always copy input before application due to FFTW real plans possibly mutating input (AbstractFFTs.jl#101) + @test P * copy(x) ≈ x_rfft + @test P \ (P * copy(x)) ≈ x + _x_rfft = similar(x_rfft) + @test mul!(_x_rfft, P, copy(x_real)) ≈ x_rfft + @test _x_rfft ≈ x_rfft + end + + # BRFFT + x_scaled = prod(size(x, d) for d in dims) .* x + @test brfft(x_rfft, size(x, first(dims)), dims) ≈ x_scaled + for P in (plan_brfft(similar(x_rfft), size(x, first(dims)), dims),) + @test eltype(P) <: Complex + @test fftdims(P) == dims + @test P * copy(x_rfft) ≈ x_scaled + @test P \ (P * copy(x_rfft)) ≈ x_rfft + _x_scaled = similar(x_real) + @test mul!(_x_scaled, P, copy(x_rfft)) ≈ x_scaled + @test _x_scaled ≈ x_scaled + end + + # IRFFT + @test irfft(x_rfft, size(x, first(dims)), dims) ≈ x + for P in (plan_irfft(similar(x_rfft), size(x, first(dims)), dims), inv(plan_rfft(similar(x_real), dims))) + @test eltype(P) <: Complex + @test fftdims(P) == dims + @test P * copy(x_rfft) ≈ x + @test P \ (P * copy(x_rfft)) ≈ x_rfft + _x_real = similar(x_real) + @test mul!(_x_real, P, copy(x_rfft)) ≈ x_real + end + end + end +end + +end \ No newline at end of file diff --git a/src/AbstractFFTs.jl b/src/AbstractFFTs.jl index 00f6dc27..6ace1659 100644 --- a/src/AbstractFFTs.jl +++ b/src/AbstractFFTs.jl @@ -6,9 +6,11 @@ export fft, ifft, bfft, fft!, ifft!, bfft!, fftdims, fftshift, ifftshift, fftshift!, ifftshift!, Frequencies, fftfreq, rfftfreq include("definitions.jl") +include("TestUtils.jl") if !isdefined(Base, :get_extension) include("../ext/AbstractFFTsChainRulesCoreExt.jl") + include("../ext/AbstractFFTsTestUtilsExt.jl") end end # module diff --git a/src/TestUtils.jl b/src/TestUtils.jl new file mode 100644 index 00000000..76c51006 --- /dev/null +++ b/src/TestUtils.jl @@ -0,0 +1,15 @@ +module TestUtils + +function test_complex_fft end +function test_real_fft end + +function __init__() + # Better error message if users forget to load Test + Base.Experimental.register_error_hint(MethodError) do io, exc, _, _ + if exc.f in (test_real_fft, test_complex_fft) + print(io, "\nDid you forget to load Test?") + end + end +end + +end \ No newline at end of file diff --git a/test/testplans.jl b/test/TestPlans.jl similarity index 90% rename from test/testplans.jl rename to test/TestPlans.jl index 09b3f671..0bced74c 100644 --- a/test/testplans.jl +++ b/test/TestPlans.jl @@ -1,3 +1,9 @@ +module TestPlans + +using LinearAlgebra +using AbstractFFTs +using AbstractFFTs: Plan + mutable struct TestPlan{T,N,G} <: Plan{T} region::G sz::NTuple{N,Int} @@ -76,13 +82,13 @@ function dft!( return y end -function mul!( +function LinearAlgebra.mul!( y::AbstractArray{<:Complex,N}, p::TestPlan, x::AbstractArray{<:Union{Complex,Real},N} ) where {N} size(y) == size(p) == size(x) || throw(DimensionMismatch()) dft!(y, x, p.region, -1) end -function mul!( +function LinearAlgebra.mul!( y::AbstractArray{<:Complex,N}, p::InverseTestPlan, x::AbstractArray{<:Union{Complex,Real},N} ) where {N} size(y) == size(p) == size(x) || throw(DimensionMismatch()) @@ -194,22 +200,17 @@ end to_real!(x::AbstractArray) = map!(real, x, x) -function Base.:*(p::TestRPlan, x::AbstractArray) +function LinearAlgebra.mul!(y::AbstractArray{<:Complex, N}, p::TestRPlan, x::AbstractArray{<:Real, N}) where {N} size(p) == size(x) || error("array and plan are not consistent") - # create output array - firstdim = first(p.region)::Int - d = size(x, firstdim) - firstdim_size = d ÷ 2 + 1 - T = complex(float(eltype(x))) - sz = ntuple(i -> i == firstdim ? firstdim_size : size(x, i), Val(ndims(x))) - y = similar(x, T, sz) - # compute DFT dft!(y, x, p.region, -1) # we clean the output a bit to make sure that we return real values # whenever the output is mathematically guaranteed to be a real number + firstdim = first(p.region)::Int + d = size(x, firstdim) + firstdim_size = d ÷ 2 + 1 to_real!(selectdim(y, firstdim, 1)) if iseven(d) to_real!(selectdim(y, firstdim, firstdim_size)) @@ -218,29 +219,50 @@ function Base.:*(p::TestRPlan, x::AbstractArray) return y end -function Base.:*(p::InverseTestRPlan, x::AbstractArray) +function Base.:*(p::TestRPlan, x::AbstractArray) + # create output array + firstdim = first(p.region)::Int + d = size(x, firstdim) + firstdim_size = d ÷ 2 + 1 + T = complex(float(eltype(x))) + sz = ntuple(i -> i == firstdim ? firstdim_size : size(x, i), Val(ndims(x))) + y = similar(x, T, sz) + + # run in-place mul! + mul!(y, p, x) + + return y +end + +function LinearAlgebra.mul!(y::AbstractArray{<:Real, N}, p::InverseTestRPlan, x::AbstractArray{<:Complex, N}) where {N} size(p) == size(x) || error("array and plan are not consistent") + # compute DFT + real_invdft!(y, x, p.region) +end + +function Base.:*(p::InverseTestRPlan, x::AbstractArray) # create output array firstdim = first(p.region)::Int d = p.d sz = ntuple(i -> i == firstdim ? d : size(x, i), Val(ndims(x))) y = similar(x, real(float(eltype(x))), sz) - # compute DFT - real_invdft!(y, x, p.region) + # run in-place mul! + mul!(y, p, x) return y end # In-place plans -# (simple wrapper of out-of-place plans that does not support inverses) +# (simple wrapper of OOP plans) struct InplaceTestPlan{T,P<:Plan{T}} <: Plan{T} plan::P end Base.size(p::InplaceTestPlan) = size(p.plan) Base.ndims(p::InplaceTestPlan) = ndims(p.plan) +AbstractFFTs.fftdims(p::InplaceTestPlan) = fftdims(p.plan) AbstractFFTs.ProjectionStyle(p::InplaceTestPlan) = AbstractFFTs.ProjectionStyle(p.plan) function AbstractFFTs.plan_fft!(x::AbstractArray, region; kwargs...) @@ -251,6 +273,12 @@ function AbstractFFTs.plan_bfft!(x::AbstractArray, region; kwargs...) end function LinearAlgebra.mul!(y::AbstractArray, p::InplaceTestPlan, x::AbstractArray) - return mul!(y, p.plan, x) + return copyto!(y, p.plan * x) end Base.:*(p::InplaceTestPlan, x::AbstractArray) = copyto!(x, p.plan * x) + +AbstractFFTs.plan_inv(p::InplaceTestPlan) = InplaceTestPlan(AbstractFFTs.plan_inv(p.plan)) +# Don't cache inverse of inplace wrapper plan (only inverse of inner plan) +Base.inv(p::InplaceTestPlan) = InplaceTestPlan(inv(p.plan)) + +end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index c5f0659b..4f92b89d 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,20 +1,18 @@ -# This file contains code that was formerly part of Julia. License is MIT: https://julialang.org/license - -using AbstractFFTs -using AbstractFFTs: Plan, ScaledPlan -using ChainRulesTestUtils -using FiniteDifferences -import ChainRulesCore - -using LinearAlgebra using Random using Test - +using AbstractFFTs +using ChainRulesTestUtils import Unitful +using LinearAlgebra Random.seed!(1234) -include("testplans.jl") +# Load example plan implementation. +include("TestPlans.jl") + +# Run interface tests for TestPlans +AbstractFFTs.TestUtils.test_complex_fft(Array) +AbstractFFTs.TestUtils.test_real_fft(Array) @testset "rfft sizes" begin A = rand(11, 10) @@ -26,124 +24,6 @@ include("testplans.jl") @test_throws AssertionError AbstractFFTs.brfft_output_size(A1, 10, 2) end -@testset "Custom Plan" begin - # DFT along last dimension, results computed using FFTW - for (x, fftw_fft) in ( - (collect(1:7), - [28.0 + 0.0im, - -3.5 + 7.267824888003178im, - -3.5 + 2.7911568610884143im, - -3.5 + 0.7988521603655248im, - -3.5 - 0.7988521603655248im, - -3.5 - 2.7911568610884143im, - -3.5 - 7.267824888003178im]), - (collect(1:8), - [36.0 + 0.0im, - -4.0 + 9.65685424949238im, - -4.0 + 4.0im, - -4.0 + 1.6568542494923806im, - -4.0 + 0.0im, - -4.0 - 1.6568542494923806im, - -4.0 - 4.0im, - -4.0 - 9.65685424949238im]), - (collect(reshape(1:8, 2, 4)), - [16.0+0.0im -4.0+4.0im -4.0+0.0im -4.0-4.0im; - 20.0+0.0im -4.0+4.0im -4.0+0.0im -4.0-4.0im]), - (collect(reshape(1:9, 3, 3)), - [12.0+0.0im -4.5+2.598076211353316im -4.5-2.598076211353316im; - 15.0+0.0im -4.5+2.598076211353316im -4.5-2.598076211353316im; - 18.0+0.0im -4.5+2.598076211353316im -4.5-2.598076211353316im]), - ) - # FFT - dims = ndims(x) - y = AbstractFFTs.fft(x, dims) - @test y ≈ fftw_fft - # test plan_fft and also inv and plan_inv of plan_ifft, which should all give - # functionally identical plans - for P in [plan_fft(x, dims), inv(plan_ifft(x, dims)), - AbstractFFTs.plan_inv(plan_ifft(x, dims))] - @test eltype(P) === ComplexF64 - @test P * x ≈ fftw_fft - @test P \ (P * x) ≈ x - @test fftdims(P) == dims - end - - # in-place plan - P = plan_fft!(x, dims) - @test eltype(P) === ComplexF64 - xc64 = ComplexF64.(x) - @test P * xc64 ≈ fftw_fft - @test xc64 ≈ fftw_fft - - fftw_bfft = complex.(size(x, dims) .* x) - @test AbstractFFTs.bfft(y, dims) ≈ fftw_bfft - P = plan_bfft(x, dims) - @test P * y ≈ fftw_bfft - @test P \ (P * y) ≈ y - @test fftdims(P) == dims - - # in-place plan - P = plan_bfft!(x, dims) - @test eltype(P) === ComplexF64 - yc64 = ComplexF64.(y) - @test P * yc64 ≈ fftw_bfft - @test yc64 ≈ fftw_bfft - - fftw_ifft = complex.(x) - @test AbstractFFTs.ifft(y, dims) ≈ fftw_ifft - # test plan_ifft and also inv and plan_inv of plan_fft, which should all give - # functionally identical plans - for P in [plan_ifft(x, dims), inv(plan_fft(x, dims)), - AbstractFFTs.plan_inv(plan_fft(x, dims))] - @test P * y ≈ fftw_ifft - @test P \ (P * y) ≈ y - @test fftdims(P) == dims - end - - # in-place plan - P = plan_ifft!(x, dims) - @test eltype(P) === ComplexF64 - yc64 = ComplexF64.(y) - @test P * yc64 ≈ fftw_ifft - @test yc64 ≈ fftw_ifft - - # real FFT - fftw_rfft = fftw_fft[ - (Colon() for _ in 1:(ndims(fftw_fft) - 1))..., - 1:(size(fftw_fft, ndims(fftw_fft)) ÷ 2 + 1) - ] - ry = AbstractFFTs.rfft(x, dims) - @test ry ≈ fftw_rfft - # test plan_rfft and also inv and plan_inv of plan_irfft, which should all give - # functionally identical plans - for P in [plan_rfft(x, dims), inv(plan_irfft(ry, size(x, dims), dims)), - AbstractFFTs.plan_inv(plan_irfft(ry, size(x, dims), dims))] - @test eltype(P) <: Real - @test P * x ≈ fftw_rfft - @test P \ (P * x) ≈ x - @test fftdims(P) == dims - end - - fftw_brfft = complex.(size(x, dims) .* x) - @test AbstractFFTs.brfft(ry, size(x, dims), dims) ≈ fftw_brfft - P = plan_brfft(ry, size(x, dims), dims) - @test P * ry ≈ fftw_brfft - @test P \ (P * ry) ≈ ry - @test fftdims(P) == dims - - fftw_irfft = complex.(x) - @test AbstractFFTs.irfft(ry, size(x, dims), dims) ≈ fftw_irfft - # test plan_rfft and also inv and plan_inv of plan_irfft, which should all give - # functionally identical plans - for P in [plan_irfft(ry, size(x, dims), dims), inv(plan_rfft(x, dims)), - AbstractFFTs.plan_inv(plan_rfft(x, dims))] - @test P * ry ≈ fftw_irfft - @test P \ (P * ry) ≈ ry - @test fftdims(P) == dims - end - end -end - @testset "Shift functions" begin @test @inferred(AbstractFFTs.fftshift([1 2 3])) == [3 1 2] @test @inferred(AbstractFFTs.fftshift([1, 2, 3])) == [3, 1, 2] @@ -232,7 +112,7 @@ end # normalization should be inferable even if region is only inferred as ::Any, # need to wrap in another function to test this (note that p.region::Any for # p::TestPlan) - f9(p::Plan{T}, sz) where {T} = AbstractFFTs.normalization(real(T), sz, fftdims(p)) + f9(p::AbstractFFTs.Plan{T}, sz) where {T} = AbstractFFTs.normalization(real(T), sz, fftdims(p)) @test @inferred(f9(plan_fft(zeros(10), 1), 10)) == 1/10 end @@ -427,3 +307,4 @@ end end end end + From 68daedb68e3dc79c62557765fbcfc712113f4d6f Mon Sep 17 00:00:00 2001 From: Gaurav Arya Date: Sat, 8 Jul 2023 21:57:33 -0400 Subject: [PATCH 02/18] Fix typo --- test/runtests.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index 4f92b89d..4dbb3a9d 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -249,7 +249,7 @@ end @testset "fft" begin # Overloads to allow ChainRulesTestUtils to test rules w.r.t. ScaledPlan's. See https://github.com/JuliaDiff/ChainRulesTestUtils.jl/issues/256 - InnerPlan = Union{TestPlan, InverseTestPlan, TestRPlan, InverseTestRPlan} + InnerPlan = Union{TestPlans.TestPlan, TestPlans.InverseTestPlan, TestPlans.TestRPlan, TestPlans.InverseTestRPlan} function FiniteDifferences.to_vec(x::InnerPlan) function FFTPlan_from_vec(x_vec::Vector) return x From c7f9b79523852d86554014208aeff67991bc6014 Mon Sep 17 00:00:00 2001 From: Gaurav Arya Date: Sat, 8 Jul 2023 21:59:49 -0400 Subject: [PATCH 03/18] Support Julia 1.0 --- src/TestUtils.jl | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/TestUtils.jl b/src/TestUtils.jl index 76c51006..50ae3461 100644 --- a/src/TestUtils.jl +++ b/src/TestUtils.jl @@ -4,10 +4,12 @@ function test_complex_fft end function test_real_fft end function __init__() - # Better error message if users forget to load Test - Base.Experimental.register_error_hint(MethodError) do io, exc, _, _ - if exc.f in (test_real_fft, test_complex_fft) - print(io, "\nDid you forget to load Test?") + if isdefined(Base, :Experimental) + # Better error message if users forget to load Test + Base.Experimental.register_error_hint(MethodError) do io, exc, _, _ + if exc.f in (test_real_fft, test_complex_fft) + print(io, "\nDid you forget to load Test?") + end end end end From babfca6776c29022da7ef12add596cd6a44b92be Mon Sep 17 00:00:00 2001 From: Gaurav Arya Date: Sat, 8 Jul 2023 22:05:27 -0400 Subject: [PATCH 04/18] Add missing test deps --- test/runtests.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/runtests.jl b/test/runtests.jl index 4dbb3a9d..b1a7a849 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -4,6 +4,8 @@ using AbstractFFTs using ChainRulesTestUtils import Unitful using LinearAlgebra +using ChainRulesCore +using FiniteDifferences Random.seed!(1234) From 09d8383062d9cbacbbaa305c17365830058646f9 Mon Sep 17 00:00:00 2001 From: Gaurav Arya Date: Sat, 8 Jul 2023 22:33:45 -0400 Subject: [PATCH 05/18] Add adjoint testing to test utilities --- ext/AbstractFFTsTestUtilsExt.jl | 40 +++++++++++++++++++++++++-- src/definitions.jl | 1 + test/runtests.jl | 49 --------------------------------- 3 files changed, 38 insertions(+), 52 deletions(-) diff --git a/ext/AbstractFFTsTestUtilsExt.jl b/ext/AbstractFFTsTestUtilsExt.jl index 819a532f..c1b40f6b 100644 --- a/ext/AbstractFFTsTestUtilsExt.jl +++ b/ext/AbstractFFTsTestUtilsExt.jl @@ -51,6 +51,26 @@ const TEST_CASES = ( dims=3)), ) +# Perform generic adjoint plan tests +function _adjoint_test(P, x; real_plan=false) + y = rand(eltype(P * x), size(P * x)) + # test basic properties + @test_broken eltype(P') === typeof(y) # (AbstactFFTs.jl#110) + @test fftdims(P') == fftdims(P) + @test (P')' === P # test adjoint of adjoint + @test size(P') == AbstractFFTs.output_size(P) # test size of adjoint + # test correctness of adjoint and its inverse via the dot test + if !real_plan + @test dot(y, P * x) ≈ dot(P' * y, x) + @test dot(y, P \ x) ≈ dot(P' \ y, x) + else + _component_dot(x, y) = dot(real.(x), real.(y)) + dot(imag.(x), imag.(y)) + @test _component_dot(y, P * copy(x)) ≈ _component_dot(P' * copy(y), x) + @test _component_dot(x, P \ copy(y)) ≈ _component_dot(P' \ copy(x), y) + end + @test_throws MethodError mul!(x, P', y) +end + """ TestUtils.test_complex_fft(ArrayType=Array; test_real=true, test_inplace=true) @@ -63,8 +83,9 @@ The backend implementation is assumed to be loaded prior to calling this functio which the correctness tests are run. Arrays are constructed via `convert(ArrayType, ...)`. - `test_inplace=true`: whether to test in-place plans. +- `test_adjoint=true`: whether to test adjoints of plans. """ -function TestUtils.test_complex_fft(ArrayType=Array; test_inplace=true) +function TestUtils.test_complex_fft(ArrayType=Array; test_inplace=true, test_adjoint=true) @testset "correctness of fft, bfft, ifft" begin for test_case in TEST_CASES _x, dims, _x_fft = test_case.x, test_case.dims, test_case.x_fft @@ -89,6 +110,9 @@ function TestUtils.test_complex_fft(ArrayType=Array; test_inplace=true) _x_out = similar(x_fft) @test mul!(_x_out, P, x_complexf) ≈ x_fft @test _x_out ≈ x_fft + if test_adjoint + _adjoint_test(P, x_complexf) + end end if test_inplace # test IIP plans @@ -120,6 +144,9 @@ function TestUtils.test_complex_fft(ArrayType=Array; test_inplace=true) _x_complexf = similar(x_complexf) @test mul!(_x_complexf, P, x_fft) ≈ x_scaled @test _x_complexf ≈ x_scaled + if test_adjoint + _adjoint_test(P, x_complexf) + end end # test IIP plans for P in (plan_bfft!(similar(x_fft), dims),) @@ -148,6 +175,9 @@ function TestUtils.test_complex_fft(ArrayType=Array; test_inplace=true) _x_complexf = similar(x_complexf) @test mul!(_x_complexf, P, x_fft) ≈ x @test _x_complexf ≈ x + if test_adjoint + _adjoint_test(P, x_complexf) + end end # test IIP plans if test_inplace @@ -177,10 +207,11 @@ The backend implementation is assumed to be loaded prior to calling this functio which the correctness tests are run. Arrays are constructed via `convert(ArrayType, ...)`. - `test_inplace=true`: whether to test in-place plans. +- `test_adjoint=true`: whether to test adjoints of plans. """ -function TestUtils.test_real_fft(ArrayType=Array; test_inplace=true) +function TestUtils.test_real_fft(ArrayType=Array; test_inplace=true, test_adjoint=true) @testset "correctness of rfft, brfft, irfft" begin - for test_case in TEST_CASES[5:5] + for test_case in TEST_CASES _x, dims, _x_fft = test_case.x, test_case.dims, test_case.x_fft x = convert(ArrayType, _x) # dummy array that will be passed to plans x_real = float.(x) # for testing mutating real FFTs @@ -202,6 +233,9 @@ function TestUtils.test_real_fft(ArrayType=Array; test_inplace=true) _x_rfft = similar(x_rfft) @test mul!(_x_rfft, P, copy(x_real)) ≈ x_rfft @test _x_rfft ≈ x_rfft + if test_adjoint + _adjoint_test(P, x_real; real_plan=true) + end end # BRFFT diff --git a/src/definitions.jl b/src/definitions.jl index 4ec176eb..23f4b161 100644 --- a/src/definitions.jl +++ b/src/definitions.jl @@ -619,6 +619,7 @@ Base.adjoint(p::ScaledPlan) = ScaledPlan(p.p', p.scale) size(p::AdjointPlan) = output_size(p.p) output_size(p::AdjointPlan) = size(p.p) +fftdims(p::AdjointPlan) = fftdims(p.p) Base.:*(p::AdjointPlan, x::AbstractArray) = _mul(p, x, ProjectionStyle(p.p)) diff --git a/test/runtests.jl b/test/runtests.jl index b1a7a849..3c5d4c69 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -153,55 +153,6 @@ end end end -@testset "adjoint" begin - @testset "complex fft adjoint" begin - for x_shape in ((3,), (3, 4), (3, 4, 5)) - N = length(x_shape) - real_x = randn(x_shape) - complex_x = randn(ComplexF64, x_shape) - y = randn(ComplexF64, x_shape) - for x in (real_x, complex_x) - for dims in unique((1, 1:N, N)) - P = plan_fft(x, dims) - @test (P')' === P # test adjoint of adjoint - @test size(P') == AbstractFFTs.output_size(P) # test size of adjoint - @test dot(y, P * x) ≈ dot(P' * y, x) # test validity of adjoint - @test dot(y, P \ x) ≈ dot(P' \ y, x) # test inv of adjoint - @test dot(y, P \ x) ≈ dot(AbstractFFTs.plan_inv(P') * y, x) # test plan_inv of adjoint - Pinv = plan_ifft(y) - @test (Pinv')' * y == Pinv * y - @test size(Pinv') == AbstractFFTs.output_size(Pinv) - @test dot(x, Pinv * y) ≈ dot(Pinv' * x, y) - @test dot(x, Pinv \ y) ≈ dot(Pinv' \ x, y) - @test dot(x, Pinv \ y) ≈ dot(AbstractFFTs.plan_inv(Pinv') * x, y) - @test_throws MethodError mul!(x, P', y) - end - end - end - end - @testset "real fft adjoint" begin - for x in (randn(3), randn(4), randn(3, 4), randn(3, 4, 5)) # test odd and even lengths - N = ndims(x) - for dims in unique((1, 1:N, N)) - P = plan_rfft(x, dims) - y = randn(ComplexF64, size(P * x)) - @test (P')' * x == P * x - @test size(P') == AbstractFFTs.output_size(P) - @test dot(real.(y), real.(P * x)) + dot(imag.(y), imag.(P * x)) ≈ dot(P' * y, x) - @test dot(real.(y), real.(P' \ x)) + dot(imag.(y), imag.(P' \ x)) ≈ dot(P \ y, x) - @test dot(real.(y), real.(AbstractFFTs.plan_inv(P') * x)) + - dot(imag.(y), imag.(AbstractFFTs.plan_inv(P') * x)) ≈ dot(P \ y, x) - Pinv = plan_irfft(y, size(x)[first(dims)], dims) - @test (Pinv')' * y == Pinv * y - @test size(Pinv') == AbstractFFTs.output_size(Pinv) - @test dot(x, Pinv * y) ≈ dot(real.(y), real.(Pinv' * x)) + dot(imag.(y), imag.(Pinv' * x)) - @test dot(x, Pinv' \ y) ≈ dot(real.(y), real.(Pinv \ x)) + dot(imag.(y), imag.(Pinv \ x)) - @test dot(x, AbstractFFTs.plan_inv(Pinv') * y) ≈ dot(real.(y), real.(Pinv \ x)) + dot(imag.(y), imag.(Pinv \ x)) - end - end - end -end - # Test that dims defaults to 1:ndims for fft-like functions @testset "Default dims" begin for x in (randn(3), randn(3, 4), randn(3, 4, 5)) From 06f8b91fa37102a3c0b634ed069e67810ba54073 Mon Sep 17 00:00:00 2001 From: Gaurav Arya Date: Sat, 8 Jul 2023 22:36:22 -0400 Subject: [PATCH 06/18] Remove mul! method from inplace test plan (consistent with fftw) --- test/TestPlans.jl | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/TestPlans.jl b/test/TestPlans.jl index 0bced74c..48599d1f 100644 --- a/test/TestPlans.jl +++ b/test/TestPlans.jl @@ -272,9 +272,6 @@ function AbstractFFTs.plan_bfft!(x::AbstractArray, region; kwargs...) return InplaceTestPlan(plan_bfft(x, region; kwargs...)) end -function LinearAlgebra.mul!(y::AbstractArray, p::InplaceTestPlan, x::AbstractArray) - return copyto!(y, p.plan * x) -end Base.:*(p::InplaceTestPlan, x::AbstractArray) = copyto!(x, p.plan * x) AbstractFFTs.plan_inv(p::InplaceTestPlan) = InplaceTestPlan(AbstractFFTs.plan_inv(p.plan)) From fe2793ef65a15e7c5b425bceade520f19e39d387 Mon Sep 17 00:00:00 2001 From: Gaurav Arya Date: Sat, 8 Jul 2023 22:39:20 -0400 Subject: [PATCH 07/18] Fix typo --- ext/AbstractFFTsTestUtilsExt.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/AbstractFFTsTestUtilsExt.jl b/ext/AbstractFFTsTestUtilsExt.jl index c1b40f6b..ea49da13 100644 --- a/ext/AbstractFFTsTestUtilsExt.jl +++ b/ext/AbstractFFTsTestUtilsExt.jl @@ -55,7 +55,7 @@ const TEST_CASES = ( function _adjoint_test(P, x; real_plan=false) y = rand(eltype(P * x), size(P * x)) # test basic properties - @test_broken eltype(P') === typeof(y) # (AbstactFFTs.jl#110) + @test_broken eltype(P') === typeof(y) # (AbstractFFTs.jl#110) @test fftdims(P') == fftdims(P) @test (P')' === P # test adjoint of adjoint @test size(P') == AbstractFFTs.output_size(P) # test size of adjoint From 98fdcdeaf54b040faf433d27f1528367f2e9855c Mon Sep 17 00:00:00 2001 From: Gaurav Arya Date: Sat, 8 Jul 2023 22:54:06 -0400 Subject: [PATCH 08/18] Document test utilities --- docs/src/implementations.md | 10 +++++++++ ext/AbstractFFTsTestUtilsExt.jl | 39 +++++---------------------------- src/TestUtils.jl | 39 +++++++++++++++++++++++++++++++++ src/definitions.jl | 2 +- 4 files changed, 55 insertions(+), 35 deletions(-) diff --git a/docs/src/implementations.md b/docs/src/implementations.md index 7367fd4c..5c8303cd 100644 --- a/docs/src/implementations.md +++ b/docs/src/implementations.md @@ -39,3 +39,13 @@ To define a new FFT implementation in your own module, you should The normalization convention for your FFT should be that it computes ``y_k = \sum_j x_j \exp(-2\pi i j k/n)`` for a transform of length ``n``, and the "backwards" (unnormalized inverse) transform computes the same thing but with ``\exp(+2\pi i jk/n)``. + +## Testing implementations + +`AbstractFFTs.jl` provides a `TestUtils` module to help with testing downstream implementations. + +```@docs +AbstractFFTs.TestUtils.test_complex_fft +AbstractFFTs.TestUtils.test_real_fft +AbstractFFTs.TestUtils.test_plan_adjoint +``` diff --git a/ext/AbstractFFTsTestUtilsExt.jl b/ext/AbstractFFTsTestUtilsExt.jl index ea49da13..1d6efc85 100644 --- a/ext/AbstractFFTsTestUtilsExt.jl +++ b/ext/AbstractFFTsTestUtilsExt.jl @@ -51,8 +51,7 @@ const TEST_CASES = ( dims=3)), ) -# Perform generic adjoint plan tests -function _adjoint_test(P, x; real_plan=false) +function TestUtils.test_plan_adjoint(P::AbstractFFTs.Plan, x::AbstractArray; real_plan=false) y = rand(eltype(P * x), size(P * x)) # test basic properties @test_broken eltype(P') === typeof(y) # (AbstractFFTs.jl#110) @@ -71,20 +70,6 @@ function _adjoint_test(P, x; real_plan=false) @test_throws MethodError mul!(x, P', y) end -""" - TestUtils.test_complex_fft(ArrayType=Array; test_real=true, test_inplace=true) - -Run tests to verify correctness of FFT/BFFT/IFFT functionality using a particular backend plan implementation. -The backend implementation is assumed to be loaded prior to calling this function. - -# Arguments - -- `ArrayType`: determines the `AbstractArray` implementation for - which the correctness tests are run. Arrays are constructed via - `convert(ArrayType, ...)`. -- `test_inplace=true`: whether to test in-place plans. -- `test_adjoint=true`: whether to test adjoints of plans. -""" function TestUtils.test_complex_fft(ArrayType=Array; test_inplace=true, test_adjoint=true) @testset "correctness of fft, bfft, ifft" begin for test_case in TEST_CASES @@ -111,7 +96,7 @@ function TestUtils.test_complex_fft(ArrayType=Array; test_inplace=true, test_adj @test mul!(_x_out, P, x_complexf) ≈ x_fft @test _x_out ≈ x_fft if test_adjoint - _adjoint_test(P, x_complexf) + TestUtils.test_plan_adjoint(P, x_complexf) end end if test_inplace @@ -145,7 +130,7 @@ function TestUtils.test_complex_fft(ArrayType=Array; test_inplace=true, test_adj @test mul!(_x_complexf, P, x_fft) ≈ x_scaled @test _x_complexf ≈ x_scaled if test_adjoint - _adjoint_test(P, x_complexf) + TestUtils.test_plan_adjoint(P, x_complexf) end end # test IIP plans @@ -176,7 +161,7 @@ function TestUtils.test_complex_fft(ArrayType=Array; test_inplace=true, test_adj @test mul!(_x_complexf, P, x_fft) ≈ x @test _x_complexf ≈ x if test_adjoint - _adjoint_test(P, x_complexf) + TestUtils.test_plan_adjoint(P, x_complexf) end end # test IIP plans @@ -195,20 +180,6 @@ function TestUtils.test_complex_fft(ArrayType=Array; test_inplace=true, test_adj end end -""" - TestUtils.test_real_fft(ArrayType=Array; test_real=true, test_inplace=true) - -Run tests to verify correctness of RFFT/BRFFT/IRFFT functionality using a particular backend plan implementation. -The backend implementation is assumed to be loaded prior to calling this function. - -# Arguments - -- `ArrayType`: determines the `AbstractArray` implementation for - which the correctness tests are run. Arrays are constructed via - `convert(ArrayType, ...)`. -- `test_inplace=true`: whether to test in-place plans. -- `test_adjoint=true`: whether to test adjoints of plans. -""" function TestUtils.test_real_fft(ArrayType=Array; test_inplace=true, test_adjoint=true) @testset "correctness of rfft, brfft, irfft" begin for test_case in TEST_CASES @@ -234,7 +205,7 @@ function TestUtils.test_real_fft(ArrayType=Array; test_inplace=true, test_adjoin @test mul!(_x_rfft, P, copy(x_real)) ≈ x_rfft @test _x_rfft ≈ x_rfft if test_adjoint - _adjoint_test(P, x_real; real_plan=true) + TestUtils.test_plan_adjoint(P, x_real; real_plan=true) end end diff --git a/src/TestUtils.jl b/src/TestUtils.jl index 50ae3461..17ac76a6 100644 --- a/src/TestUtils.jl +++ b/src/TestUtils.jl @@ -1,8 +1,47 @@ module TestUtils +""" + TestUtils.test_complex_fft(ArrayType=Array; test_real=true, test_inplace=true) + +Run tests to verify correctness of FFT/BFFT/IFFT functionality using a particular backend plan implementation. +The backend implementation is assumed to be loaded prior to calling this function. + +# Arguments + +- `ArrayType`: determines the `AbstractArray` implementation for + which the correctness tests are run. Arrays are constructed via + `convert(ArrayType, ...)`. +- `test_inplace=true`: whether to test in-place plans. +- `test_adjoint=true`: whether to test [plan adjoints](api.md#Base.adjoint). +""" function test_complex_fft end + +""" + TestUtils.test_real_fft(ArrayType=Array; test_real=true, test_inplace=true) + +Run tests to verify correctness of RFFT/BRFFT/IRFFT functionality using a particular backend plan implementation. +The backend implementation is assumed to be loaded prior to calling this function. + +# Arguments + +- `ArrayType`: determines the `AbstractArray` implementation for + which the correctness tests are run. Arrays are constructed via + `convert(ArrayType, ...)`. +- `test_inplace=true`: whether to test in-place plans. +- `test_adjoint=true`: whether to test [plan adjoints](api.md#Base.adjoint). +""" function test_real_fft end +""" + TestUtils.test_plan_adjoint(P::Plan, x::AbstractArray; real_plan=false) + +Test basic properties of the adjoint `P'` of a particular plan given an input array to the plan `x`, +including its accuracy via the dot test. Real-to-complex and complex-to-real plans require +a slightly modified dot test, in which case `real_plan=true` should be provided. + +""" +function test_plan_adjoint end + function __init__() if isdefined(Base, :Experimental) # Better error message if users forget to load Test diff --git a/src/definitions.jl b/src/definitions.jl index 23f4b161..f349349a 100644 --- a/src/definitions.jl +++ b/src/definitions.jl @@ -608,7 +608,7 @@ Form the adjoint operator of an FFT plan. Returns a plan that performs the adjoi the original plan. Note that this differs from the corresponding backwards plan in the case of real FFTs due to the halving of one of the dimensions of the FFT output, as described in [`rfft`](@ref). -!!! note +!!! warning Adjoint plans do not currently support `LinearAlgebra.mul!`. Further, as a new addition to `AbstractFFTs`, coverage of `Base.adjoint` in downstream implementations may be limited. """ From 3537f76a4043ea20e88107da175c0a731bf13e67 Mon Sep 17 00:00:00 2001 From: Gaurav Arya Date: Sat, 8 Jul 2023 23:43:09 -0400 Subject: [PATCH 09/18] Apply code review suggestions and refactor TestUtils --- Project.toml | 2 +- docs/src/implementations.md | 10 +- ...TestUtilsExt.jl => AbstractFFTsTestExt.jl} | 111 ++++++++---------- src/AbstractFFTs.jl | 2 +- src/TestUtils.jl | 44 ++++--- test/runtests.jl | 4 +- 6 files changed, 87 insertions(+), 86 deletions(-) rename ext/{AbstractFFTsTestUtilsExt.jl => AbstractFFTsTestExt.jl} (69%) diff --git a/Project.toml b/Project.toml index f069e4f9..6404c36e 100644 --- a/Project.toml +++ b/Project.toml @@ -13,7 +13,7 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [extensions] AbstractFFTsChainRulesCoreExt = "ChainRulesCore" -AbstractFFTsTestUtilsExt = "Test" +AbstractFFTsTestExt = "Test" [compat] ChainRulesCore = "1" diff --git a/docs/src/implementations.md b/docs/src/implementations.md index 5c8303cd..6db6ee0e 100644 --- a/docs/src/implementations.md +++ b/docs/src/implementations.md @@ -43,9 +43,13 @@ length ``n``, and the "backwards" (unnormalized inverse) transform computes the ## Testing implementations `AbstractFFTs.jl` provides a `TestUtils` module to help with testing downstream implementations. - +The following functions test that all FFT functionality has been correctly implemented: +```@docs +AbstractFFTs.TestUtils.test_complex_ffts +AbstractFFTs.TestUtils.test_real_ffts +``` +`TestUtils` also exposes lower level functions for generically testing particular plans: ```@docs -AbstractFFTs.TestUtils.test_complex_fft -AbstractFFTs.TestUtils.test_real_fft +AbstractFFTs.TestUtils.test_plan AbstractFFTs.TestUtils.test_plan_adjoint ``` diff --git a/ext/AbstractFFTsTestUtilsExt.jl b/ext/AbstractFFTsTestExt.jl similarity index 69% rename from ext/AbstractFFTsTestUtilsExt.jl rename to ext/AbstractFFTsTestExt.jl index 1d6efc85..6d8fbec1 100644 --- a/ext/AbstractFFTsTestUtilsExt.jl +++ b/ext/AbstractFFTsTestExt.jl @@ -1,13 +1,13 @@ # This file contains code that was formerly part of Julia. License is MIT: https://julialang.org/license -module AbstractFFTsTestUtilsExt +module AbstractFFTsTestExt using AbstractFFTs using AbstractFFTs: TestUtils using AbstractFFTs.LinearAlgebra using Test -# Ground truth _x_fft computed using FFTW library +# Ground truth x_fft computed using FFTW library const TEST_CASES = ( (; x = collect(1:7), dims = 1, x_fft = [28.0 + 0.0im, @@ -51,29 +51,47 @@ const TEST_CASES = ( dims=3)), ) -function TestUtils.test_plan_adjoint(P::AbstractFFTs.Plan, x::AbstractArray; real_plan=false) - y = rand(eltype(P * x), size(P * x)) + +function TestUtils.test_plan(P::AbstractFFTs.Plan, x::AbstractArray, x_transformed::AbstractArray; inplace_plan=false, copy_input=false) + _copy = copy_input ? copy : identity + if !inplace_plan + @test P * _copy(x) ≈ x_transformed + @test P \ (P * _copy(x)) ≈ x + _x_out = similar(P * _copy(x)) + @test mul!(_x_out, P, _copy(x)) ≈ x_transformed + @test _x_out ≈ x_transformed + else + _x = copy(x) + @test P * _copy(_x) ≈ x_transformed + @test _x ≈ x_transformed + @test P \ _copy(_x) ≈ x + @test _x ≈ x + end +end + +function TestUtils.test_plan_adjoint(P::AbstractFFTs.Plan, x::AbstractArray; real_plan=false, copy_input=false) + _copy = copy_input ? copy : identity + y = rand(eltype(P * _copy(x)), size(P * _copy(x))) # test basic properties - @test_broken eltype(P') === typeof(y) # (AbstractFFTs.jl#110) - @test fftdims(P') == fftdims(P) + @test_skip eltype(P') === typeof(y) # (AbstractFFTs.jl#110) @test (P')' === P # test adjoint of adjoint @test size(P') == AbstractFFTs.output_size(P) # test size of adjoint # test correctness of adjoint and its inverse via the dot test if !real_plan - @test dot(y, P * x) ≈ dot(P' * y, x) - @test dot(y, P \ x) ≈ dot(P' \ y, x) + @test dot(y, P * _copy(x)) ≈ dot(P' * _copy(y), x) + @test dot(y, P \ _copy(x)) ≈ dot(P' \ _copy(y), x) else _component_dot(x, y) = dot(real.(x), real.(y)) + dot(imag.(x), imag.(y)) - @test _component_dot(y, P * copy(x)) ≈ _component_dot(P' * copy(y), x) - @test _component_dot(x, P \ copy(y)) ≈ _component_dot(P' \ copy(x), y) + @test _component_dot(y, P * _copy(x)) ≈ _component_dot(P' * _copy(y), x) + @test _component_dot(x, P \ _copy(y)) ≈ _component_dot(P' \ _copy(x), y) end @test_throws MethodError mul!(x, P', y) end -function TestUtils.test_complex_fft(ArrayType=Array; test_inplace=true, test_adjoint=true) +function TestUtils.test_complex_ffts(ArrayType=Array; test_inplace=true, test_adjoint=true) @testset "correctness of fft, bfft, ifft" begin for test_case in TEST_CASES - _x, dims, _x_fft = test_case.x, test_case.dims, test_case.x_fft + _x, dims, _x_fft = copy(test_case.x), test_case.dims, copy(test_case.x_fft) x = convert(ArrayType, _x) # dummy array that will be passed to plans x_complexf = convert(ArrayType, complex.(float.(x))) # for testing mutating complex FFTs x_fft = convert(ArrayType, _x_fft) @@ -90,25 +108,16 @@ function TestUtils.test_complex_fft(ArrayType=Array; test_inplace=true, test_adj for P in (plan_fft(similar(x_complexf), dims), inv(plan_ifft(similar(x_complexf), dims))) @test eltype(P) <: Complex @test fftdims(P) == dims - @test P * x ≈ x_fft - @test P \ (P * x) ≈ x - _x_out = similar(x_fft) - @test mul!(_x_out, P, x_complexf) ≈ x_fft - @test _x_out ≈ x_fft + TestUtils.test_plan(P, x_complexf, x_fft) if test_adjoint + @test fftdims(P') == fftdims(P) TestUtils.test_plan_adjoint(P, x_complexf) end end if test_inplace # test IIP plans for P in (plan_fft!(similar(x_complexf), dims), inv(plan_ifft!(similar(x_complexf), dims))) - @test eltype(P) <: Complex - @test fftdims(P) == dims - _x_complexf = copy(x_complexf) - @test P * _x_complexf ≈ x_fft - @test _x_complexf ≈ x_fft - @test P \ _x_complexf ≈ x - @test _x_complexf ≈ x + TestUtils.test_plan(P, x_complexf, x_fft; inplace_plan=true) end end @@ -124,24 +133,16 @@ function TestUtils.test_complex_fft(ArrayType=Array; test_inplace=true, test_adj for P in (plan_bfft(similar(x_fft), dims),) @test eltype(P) <: Complex @test fftdims(P) == dims - @test P * x_fft ≈ x_scaled - @test P \ (P * x_fft) ≈ x_fft - _x_complexf = similar(x_complexf) - @test mul!(_x_complexf, P, x_fft) ≈ x_scaled - @test _x_complexf ≈ x_scaled + TestUtils.test_plan(P, x_fft, x_scaled) if test_adjoint - TestUtils.test_plan_adjoint(P, x_complexf) + TestUtils.test_plan_adjoint(P, x_fft) end end # test IIP plans for P in (plan_bfft!(similar(x_fft), dims),) @test eltype(P) <: Complex @test fftdims(P) == dims - _x_fft = copy(x_fft) - @test P * _x_fft ≈ x_scaled - @test _x_fft ≈ x_scaled - @test P \ _x_fft ≈ x_fft - @test _x_fft ≈ x_fft + TestUtils.test_plan(P, x_fft, x_scaled; inplace_plan=true) end # IFFT @@ -155,13 +156,9 @@ function TestUtils.test_complex_fft(ArrayType=Array; test_inplace=true, test_adj for P in (plan_ifft(similar(x_complexf), dims), inv(plan_fft(similar(x_complexf), dims))) @test eltype(P) <: Complex @test fftdims(P) == dims - @test P * x_fft ≈ x - @test P \ (P * x_fft) ≈ x_fft - _x_complexf = similar(x_complexf) - @test mul!(_x_complexf, P, x_fft) ≈ x - @test _x_complexf ≈ x + TestUtils.test_plan(P, x_fft, x) if test_adjoint - TestUtils.test_plan_adjoint(P, x_complexf) + TestUtils.test_plan_adjoint(P, x_fft) end end # test IIP plans @@ -169,21 +166,17 @@ function TestUtils.test_complex_fft(ArrayType=Array; test_inplace=true, test_adj for P in (plan_ifft!(similar(x_complexf), dims), inv(plan_fft!(similar(x_complexf), dims))) @test eltype(P) <: Complex @test fftdims(P) == dims - _x_fft = copy(x_fft) - @test P * _x_fft ≈ x - @test _x_fft ≈ x - @test P \ _x_fft ≈ x_fft - @test _x_fft ≈ x_fft + TestUtils.test_plan(P, x_fft, x; inplace_plan=true) end end end end end -function TestUtils.test_real_fft(ArrayType=Array; test_inplace=true, test_adjoint=true) +function TestUtils.test_real_ffts(ArrayType=Array; test_adjoint=true, copy_input=false) @testset "correctness of rfft, brfft, irfft" begin for test_case in TEST_CASES - _x, dims, _x_fft = test_case.x, test_case.dims, test_case.x_fft + _x, dims, _x_fft = copy(test_case.x), test_case.dims, copy(test_case.x_fft) x = convert(ArrayType, _x) # dummy array that will be passed to plans x_real = float.(x) # for testing mutating real FFTs x_fft = convert(ArrayType, _x_fft) @@ -198,14 +191,9 @@ function TestUtils.test_real_fft(ArrayType=Array; test_inplace=true, test_adjoin for P in (plan_rfft(similar(x_real), dims), inv(plan_irfft(similar(x_rfft), size(x, first(dims)), dims))) @test eltype(P) <: Real @test fftdims(P) == dims - # Always copy input before application due to FFTW real plans possibly mutating input (AbstractFFTs.jl#101) - @test P * copy(x) ≈ x_rfft - @test P \ (P * copy(x)) ≈ x - _x_rfft = similar(x_rfft) - @test mul!(_x_rfft, P, copy(x_real)) ≈ x_rfft - @test _x_rfft ≈ x_rfft + TestUtils.test_plan(P, x_real, x_rfft; copy_input) if test_adjoint - TestUtils.test_plan_adjoint(P, x_real; real_plan=true) + TestUtils.test_plan_adjoint(P, x_real; real_plan=true, copy_input) end end @@ -215,11 +203,7 @@ function TestUtils.test_real_fft(ArrayType=Array; test_inplace=true, test_adjoin for P in (plan_brfft(similar(x_rfft), size(x, first(dims)), dims),) @test eltype(P) <: Complex @test fftdims(P) == dims - @test P * copy(x_rfft) ≈ x_scaled - @test P \ (P * copy(x_rfft)) ≈ x_rfft - _x_scaled = similar(x_real) - @test mul!(_x_scaled, P, copy(x_rfft)) ≈ x_scaled - @test _x_scaled ≈ x_scaled + TestUtils.test_plan(P, x_rfft, x_scaled; copy_input) end # IRFFT @@ -227,13 +211,10 @@ function TestUtils.test_real_fft(ArrayType=Array; test_inplace=true, test_adjoin for P in (plan_irfft(similar(x_rfft), size(x, first(dims)), dims), inv(plan_rfft(similar(x_real), dims))) @test eltype(P) <: Complex @test fftdims(P) == dims - @test P * copy(x_rfft) ≈ x - @test P \ (P * copy(x_rfft)) ≈ x_rfft - _x_real = similar(x_real) - @test mul!(_x_real, P, copy(x_rfft)) ≈ x_real + TestUtils.test_plan(P, x_rfft, x; copy_input) end end end end -end \ No newline at end of file +end diff --git a/src/AbstractFFTs.jl b/src/AbstractFFTs.jl index 6ace1659..32259160 100644 --- a/src/AbstractFFTs.jl +++ b/src/AbstractFFTs.jl @@ -10,7 +10,7 @@ include("TestUtils.jl") if !isdefined(Base, :get_extension) include("../ext/AbstractFFTsChainRulesCoreExt.jl") - include("../ext/AbstractFFTsTestUtilsExt.jl") + include("../ext/AbstractFFTsTestExt.jl") end end # module diff --git a/src/TestUtils.jl b/src/TestUtils.jl index 17ac76a6..14f74d24 100644 --- a/src/TestUtils.jl +++ b/src/TestUtils.jl @@ -1,9 +1,9 @@ module TestUtils """ - TestUtils.test_complex_fft(ArrayType=Array; test_real=true, test_inplace=true) + TestUtils.test_complex_ffts(ArrayType=Array; test_adjoint=true, test_inplace=true) -Run tests to verify correctness of FFT/BFFT/IFFT functionality using a particular backend plan implementation. +Run tests to verify correctness of FFT, BFFT, and IFFT functionality using a particular backend plan implementation. The backend implementation is assumed to be loaded prior to calling this function. # Arguments @@ -14,12 +14,12 @@ The backend implementation is assumed to be loaded prior to calling this functio - `test_inplace=true`: whether to test in-place plans. - `test_adjoint=true`: whether to test [plan adjoints](api.md#Base.adjoint). """ -function test_complex_fft end +function test_complex_ffts end """ - TestUtils.test_real_fft(ArrayType=Array; test_real=true, test_inplace=true) + TestUtils.test_real_ffts(ArrayType=Array; test_adjoint=true, copy_input=false) -Run tests to verify correctness of RFFT/BRFFT/IRFFT functionality using a particular backend plan implementation. +Run tests to verify correctness of RFFT, BRFFT, and IRFFT functionality using a particular backend plan implementation. The backend implementation is assumed to be loaded prior to calling this function. # Arguments @@ -27,30 +27,46 @@ The backend implementation is assumed to be loaded prior to calling this functio - `ArrayType`: determines the `AbstractArray` implementation for which the correctness tests are run. Arrays are constructed via `convert(ArrayType, ...)`. -- `test_inplace=true`: whether to test in-place plans. - `test_adjoint=true`: whether to test [plan adjoints](api.md#Base.adjoint). +- `copy_input=false`: whether to copy the input before applying the plan in tests, to accomodate for + [input-mutating behaviour of real FFTW plans](https://github.com/JuliaMath/AbstractFFTs.jl/issues/101). """ -function test_real_fft end +function test_real_ffts end + # Always copy input before application due to FFTW real plans possibly mutating input (AbstractFFTs.jl#101) """ - TestUtils.test_plan_adjoint(P::Plan, x::AbstractArray; real_plan=false) + TestUtils.test_plan(P::Plan, x::AbstractArray, x_transformed::AbstractArray; + inplace_plan=false, copy_input=false) + +Test basic properties of a plan `P` given an input array `x` and expected output `x_transformed`. -Test basic properties of the adjoint `P'` of a particular plan given an input array to the plan `x`, -including its accuracy via the dot test. Real-to-complex and complex-to-real plans require -a slightly modified dot test, in which case `real_plan=true` should be provided. +Because [real FFTW plans may mutate their input in some cases](https://github.com/JuliaMath/AbstractFFTs.jl/issues/101), +we allow specifying `copy_input=true` to allow for this behaviour in tests by copying the input before applying the plan. +""" +function test_plan end +""" + TestUtils.test_plan_adjoint(P::Plan, x::AbstractArray; real_plan=false, copy_input=false) + +Test basic properties of the [adjoint](api.md#Base.adjoint) `P'` of a particular plan given an input array `x`, +including its accuracy via the dot test. + +Real-to-complex and complex-to-real plans require a slightly modified dot test, in which case `real_plan=true` should be provided. +The plan is assumed out-of-place, as adjoints are not yet supported for in-place plans. +Because [real FFTW plans may mutate their input in some cases](https://github.com/JuliaMath/AbstractFFTs.jl/issues/101), +we allow specifying `copy_input=true` to allow for this behaviour in tests by copying the input before applying the plan. """ function test_plan_adjoint end function __init__() - if isdefined(Base, :Experimental) + if isdefined(Base, :get_extension) && isdefined(Base.Experimental, :register_error_hint) # Better error message if users forget to load Test Base.Experimental.register_error_hint(MethodError) do io, exc, _, _ - if exc.f in (test_real_fft, test_complex_fft) + if (exc.f === test_real_fft || exc.f === test_complex_fft) && Base.get_extension(AbstractFFTs, :AbstractFFTsTestExt) === nothing print(io, "\nDid you forget to load Test?") end end end end -end \ No newline at end of file +end diff --git a/test/runtests.jl b/test/runtests.jl index 3c5d4c69..fe74897e 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -13,8 +13,8 @@ Random.seed!(1234) include("TestPlans.jl") # Run interface tests for TestPlans -AbstractFFTs.TestUtils.test_complex_fft(Array) -AbstractFFTs.TestUtils.test_real_fft(Array) +AbstractFFTs.TestUtils.test_complex_ffts(Array) +AbstractFFTs.TestUtils.test_real_ffts(Array) @testset "rfft sizes" begin A = rand(11, 10) From 73a419bad448c1f02bc41d4b4e329d43e1fa33f9 Mon Sep 17 00:00:00 2001 From: Gaurav Arya Date: Sun, 9 Jul 2023 12:19:57 -0400 Subject: [PATCH 10/18] Support Julia 1.0 --- ext/AbstractFFTsTestExt.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ext/AbstractFFTsTestExt.jl b/ext/AbstractFFTsTestExt.jl index 6d8fbec1..cd038044 100644 --- a/ext/AbstractFFTsTestExt.jl +++ b/ext/AbstractFFTsTestExt.jl @@ -191,9 +191,9 @@ function TestUtils.test_real_ffts(ArrayType=Array; test_adjoint=true, copy_input for P in (plan_rfft(similar(x_real), dims), inv(plan_irfft(similar(x_rfft), size(x, first(dims)), dims))) @test eltype(P) <: Real @test fftdims(P) == dims - TestUtils.test_plan(P, x_real, x_rfft; copy_input) + TestUtils.test_plan(P, x_real, x_rfft; copy_input=copy_input) if test_adjoint - TestUtils.test_plan_adjoint(P, x_real; real_plan=true, copy_input) + TestUtils.test_plan_adjoint(P, x_real; real_plan=true, copy_input=copy_input) end end @@ -203,7 +203,7 @@ function TestUtils.test_real_ffts(ArrayType=Array; test_adjoint=true, copy_input for P in (plan_brfft(similar(x_rfft), size(x, first(dims)), dims),) @test eltype(P) <: Complex @test fftdims(P) == dims - TestUtils.test_plan(P, x_rfft, x_scaled; copy_input) + TestUtils.test_plan(P, x_rfft, x_scaled; copy_input=copy_input) end # IRFFT @@ -211,7 +211,7 @@ function TestUtils.test_real_ffts(ArrayType=Array; test_adjoint=true, copy_input for P in (plan_irfft(similar(x_rfft), size(x, first(dims)), dims), inv(plan_rfft(similar(x_real), dims))) @test eltype(P) <: Complex @test fftdims(P) == dims - TestUtils.test_plan(P, x_rfft, x; copy_input) + TestUtils.test_plan(P, x_rfft, x; copy_input=copy_input) end end end From 4d1fdb7ab8362e42f1dbb68781e474de709da0d2 Mon Sep 17 00:00:00 2001 From: Gaurav Arya Date: Sun, 9 Jul 2023 12:29:22 -0400 Subject: [PATCH 11/18] Reorder kwargs in doc string --- src/TestUtils.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/TestUtils.jl b/src/TestUtils.jl index 14f74d24..2d849869 100644 --- a/src/TestUtils.jl +++ b/src/TestUtils.jl @@ -1,7 +1,7 @@ module TestUtils """ - TestUtils.test_complex_ffts(ArrayType=Array; test_adjoint=true, test_inplace=true) + TestUtils.test_complex_ffts(ArrayType=Array; test_inplace=true, test_adjoint=true) Run tests to verify correctness of FFT, BFFT, and IFFT functionality using a particular backend plan implementation. The backend implementation is assumed to be loaded prior to calling this function. From cc0fe9da876e5a49f93aca6e770c1e915ae90caf Mon Sep 17 00:00:00 2001 From: Gaurav Arya Date: Sun, 9 Jul 2023 12:56:23 -0400 Subject: [PATCH 12/18] Also explicitly test AbstractFFTs.plan_inv --- ext/AbstractFFTsTestExt.jl | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/ext/AbstractFFTsTestExt.jl b/ext/AbstractFFTsTestExt.jl index cd038044..733b4e64 100644 --- a/ext/AbstractFFTsTestExt.jl +++ b/ext/AbstractFFTsTestExt.jl @@ -103,9 +103,10 @@ function TestUtils.test_complex_ffts(ArrayType=Array; test_inplace=true, test_ad @test fft!(_x_complexf, dims) ≈ x_fft @test _x_complexf ≈ x_fft end - # test OOP plans, checking plan_fft and also inv of plan_ifft, + # test OOP plans, checking plan_fft and also inv and plan_inv of plan_ifft, # which should give functionally identical plans - for P in (plan_fft(similar(x_complexf), dims), inv(plan_ifft(similar(x_complexf), dims))) + for P in (plan_fft(similar(x_complexf), dims), + (_inv(plan_ifft(similar(x_complexf), dims)) for _inv in (inv, AbstractFFTs.plan_inv))...) @test eltype(P) <: Complex @test fftdims(P) == dims TestUtils.test_plan(P, x_complexf, x_fft) @@ -116,7 +117,8 @@ function TestUtils.test_complex_ffts(ArrayType=Array; test_inplace=true, test_ad end if test_inplace # test IIP plans - for P in (plan_fft!(similar(x_complexf), dims), inv(plan_ifft!(similar(x_complexf), dims))) + for P in (plan_fft!(similar(x_complexf), dims), + (_inv(plan_ifft!(similar(x_complexf), dims)) for _inv in (inv, AbstractFFTs.plan_inv))...) TestUtils.test_plan(P, x_complexf, x_fft; inplace_plan=true) end end @@ -153,7 +155,8 @@ function TestUtils.test_complex_ffts(ArrayType=Array; test_inplace=true, test_ad @test _x_fft ≈ x end # test OOP plans - for P in (plan_ifft(similar(x_complexf), dims), inv(plan_fft(similar(x_complexf), dims))) + for P in (plan_ifft(similar(x_complexf), dims), + (_inv(plan_fft(similar(x_complexf), dims)) for _inv in (inv, AbstractFFTs.plan_inv))...) @test eltype(P) <: Complex @test fftdims(P) == dims TestUtils.test_plan(P, x_fft, x) @@ -163,7 +166,8 @@ function TestUtils.test_complex_ffts(ArrayType=Array; test_inplace=true, test_ad end # test IIP plans if test_inplace - for P in (plan_ifft!(similar(x_complexf), dims), inv(plan_fft!(similar(x_complexf), dims))) + for P in (plan_ifft!(similar(x_complexf), dims), + (_inv(plan_fft!(similar(x_complexf), dims)) for _inv in (inv, AbstractFFTs.plan_inv))...) @test eltype(P) <: Complex @test fftdims(P) == dims TestUtils.test_plan(P, x_fft, x; inplace_plan=true) @@ -188,7 +192,8 @@ function TestUtils.test_real_ffts(ArrayType=Array; test_adjoint=true, copy_input # RFFT @test rfft(x, dims) ≈ x_rfft - for P in (plan_rfft(similar(x_real), dims), inv(plan_irfft(similar(x_rfft), size(x, first(dims)), dims))) + for P in (plan_rfft(similar(x_real), dims), + (_inv(plan_irfft(similar(x_rfft), size(x, first(dims)), dims)) for _inv in (inv, AbstractFFTs.plan_inv))...) @test eltype(P) <: Real @test fftdims(P) == dims TestUtils.test_plan(P, x_real, x_rfft; copy_input=copy_input) @@ -208,7 +213,8 @@ function TestUtils.test_real_ffts(ArrayType=Array; test_adjoint=true, copy_input # IRFFT @test irfft(x_rfft, size(x, first(dims)), dims) ≈ x - for P in (plan_irfft(similar(x_rfft), size(x, first(dims)), dims), inv(plan_rfft(similar(x_real), dims))) + for P in (plan_irfft(similar(x_rfft), size(x, first(dims)), dims), + (_inv(plan_rfft(similar(x_real), dims)) for _inv in (inv, AbstractFFTs.plan_inv))...) @test eltype(P) <: Complex @test fftdims(P) == dims TestUtils.test_plan(P, x_rfft, x; copy_input=copy_input) From 6da1cc3bb83fa4ad57cdf2891bf3aca682603d40 Mon Sep 17 00:00:00 2001 From: Gaurav Arya Date: Fri, 14 Jul 2023 16:54:37 -0400 Subject: [PATCH 13/18] Lift isdefined checks out of __init__ --- src/TestUtils.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/TestUtils.jl b/src/TestUtils.jl index 2d849869..0bca43a6 100644 --- a/src/TestUtils.jl +++ b/src/TestUtils.jl @@ -58,8 +58,8 @@ we allow specifying `copy_input=true` to allow for this behaviour in tests by co """ function test_plan_adjoint end -function __init__() - if isdefined(Base, :get_extension) && isdefined(Base.Experimental, :register_error_hint) +if isdefined(Base, :get_extension) && isdefined(Base.Experimental, :register_error_hint) + function __init__() # Better error message if users forget to load Test Base.Experimental.register_error_hint(MethodError) do io, exc, _, _ if (exc.f === test_real_fft || exc.f === test_complex_fft) && Base.get_extension(AbstractFFTs, :AbstractFFTsTestExt) === nothing From 74b15cf576e6c5f5a91315ef1cb0c69f14d2e312 Mon Sep 17 00:00:00 2001 From: Gaurav Arya Date: Fri, 14 Jul 2023 16:57:27 -0400 Subject: [PATCH 14/18] Update src/definitions.jl Co-authored-by: David Widmann --- src/definitions.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/definitions.jl b/src/definitions.jl index f349349a..d31bbf9c 100644 --- a/src/definitions.jl +++ b/src/definitions.jl @@ -608,7 +608,7 @@ Form the adjoint operator of an FFT plan. Returns a plan that performs the adjoi the original plan. Note that this differs from the corresponding backwards plan in the case of real FFTs due to the halving of one of the dimensions of the FFT output, as described in [`rfft`](@ref). -!!! warning +!!! warning Adjoint plans do not currently support `LinearAlgebra.mul!`. Further, as a new addition to `AbstractFFTs`, coverage of `Base.adjoint` in downstream implementations may be limited. """ From c5383e6ab0acb6ef3a6f782bb08022cbb9a2159f Mon Sep 17 00:00:00 2001 From: Gaurav Arya Date: Tue, 18 Jul 2023 12:20:52 -0400 Subject: [PATCH 15/18] Note TestUtils is a weak extension --- docs/src/implementations.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/src/implementations.md b/docs/src/implementations.md index 6db6ee0e..82698248 100644 --- a/docs/src/implementations.md +++ b/docs/src/implementations.md @@ -42,7 +42,8 @@ length ``n``, and the "backwards" (unnormalized inverse) transform computes the ## Testing implementations -`AbstractFFTs.jl` provides a `TestUtils` module to help with testing downstream implementations. +`AbstractFFTs.jl` provides an experimental `TestUtils` module to help with testing downstream implementations, +available as a [weak extension](https://pkgdocs.julialang.org/v1.9/creating-packages/#Conditional-loading-of-code-in-packages-(Extensions)) of `Test`. The following functions test that all FFT functionality has been correctly implemented: ```@docs AbstractFFTs.TestUtils.test_complex_ffts From 239129b314ac4e0cc10e79b2128a0c9e70856854 Mon Sep 17 00:00:00 2001 From: Gaurav Arya Date: Wed, 19 Jul 2023 14:43:19 -0400 Subject: [PATCH 16/18] Update function names in error handler --- src/TestUtils.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/TestUtils.jl b/src/TestUtils.jl index 0bca43a6..adfffeaf 100644 --- a/src/TestUtils.jl +++ b/src/TestUtils.jl @@ -62,7 +62,8 @@ if isdefined(Base, :get_extension) && isdefined(Base.Experimental, :register_err function __init__() # Better error message if users forget to load Test Base.Experimental.register_error_hint(MethodError) do io, exc, _, _ - if (exc.f === test_real_fft || exc.f === test_complex_fft) && Base.get_extension(AbstractFFTs, :AbstractFFTsTestExt) === nothing + if any(f -> (f === exc.f), (test_real_ffts, test_complex_ffts, test_plan, test_plan_adjoint)) && + (Base.get_extension(AbstractFFTs, :AbstractFFTsTestExt) === nothing) print(io, "\nDid you forget to load Test?") end end From 18ee10780adc9df5a77b4da6e6144e32e96e5b80 Mon Sep 17 00:00:00 2001 From: Gaurav Arya Date: Wed, 19 Jul 2023 14:47:11 -0400 Subject: [PATCH 17/18] Add missing test_adjoint's for BRFFT, IRFFT --- ext/AbstractFFTsTestExt.jl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ext/AbstractFFTsTestExt.jl b/ext/AbstractFFTsTestExt.jl index 733b4e64..8f3bd8d3 100644 --- a/ext/AbstractFFTsTestExt.jl +++ b/ext/AbstractFFTsTestExt.jl @@ -209,6 +209,9 @@ function TestUtils.test_real_ffts(ArrayType=Array; test_adjoint=true, copy_input @test eltype(P) <: Complex @test fftdims(P) == dims TestUtils.test_plan(P, x_rfft, x_scaled; copy_input=copy_input) + if test_adjoint + TestUtils.test_plan_adjoint(P, x_rfft; real_plan=true, copy_input=copy_input) + end end # IRFFT @@ -218,6 +221,9 @@ function TestUtils.test_real_ffts(ArrayType=Array; test_adjoint=true, copy_input @test eltype(P) <: Complex @test fftdims(P) == dims TestUtils.test_plan(P, x_rfft, x; copy_input=copy_input) + if test_adjoint + TestUtils.test_plan_adjoint(P, x_rfft; real_plan=true, copy_input=copy_input) + end end end end From e14c0457748940b7d85ea61f42dffad529b0f034 Mon Sep 17 00:00:00 2001 From: Gaurav Arya Date: Wed, 19 Jul 2023 14:49:04 -0400 Subject: [PATCH 18/18] Collect x_rfft so as to not hit #112 --- ext/AbstractFFTsTestExt.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/AbstractFFTsTestExt.jl b/ext/AbstractFFTsTestExt.jl index 8f3bd8d3..ccea93a9 100644 --- a/ext/AbstractFFTsTestExt.jl +++ b/ext/AbstractFFTsTestExt.jl @@ -184,7 +184,7 @@ function TestUtils.test_real_ffts(ArrayType=Array; test_adjoint=true, copy_input x = convert(ArrayType, _x) # dummy array that will be passed to plans x_real = float.(x) # for testing mutating real FFTs x_fft = convert(ArrayType, _x_fft) - x_rfft = selectdim(x_fft, first(dims), 1:(size(x_fft, first(dims)) ÷ 2 + 1)) + x_rfft = collect(selectdim(x_fft, first(dims), 1:(size(x_fft, first(dims)) ÷ 2 + 1))) if !(eltype(x) <: Real) continue