Skip to content

Commit 3ca2be0

Browse files
committed
Add VectorInterface package extension
Implements the VectorInterface.jl API (scalartype, zerovector, scale, add families and inner) for Fun, so Fun can be used directly as a vector type with packages like KrylovKit without type piracy. In-place variants operate on the coefficient vectors of Funs in compatible spaces; the !! variants fall back to out-of-place versions on space mismatch or scalar type promotion. Assisted-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 1d2e264 commit 3ca2be0

3 files changed

Lines changed: 242 additions & 1 deletion

File tree

‎Project.toml‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@ StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
2020

2121
[weakdeps]
2222
DualNumbers = "fa6b7ba4-c1ee-5f82-b5fc-ecf0adba8f74"
23+
VectorInterface = "409d34a3-91d5-4945-b6ec-7529ddf182d8"
2324

2425
[extensions]
2526
ApproxFunDualNumbersExt = "DualNumbers"
27+
ApproxFunVectorInterfaceExt = "VectorInterface"
2628

2729
[compat]
2830
AbstractFFTs = "1.0"
@@ -48,6 +50,7 @@ Reexport = "1.0"
4850
SpecialFunctions = "1.1, 2"
4951
StaticArrays = "1"
5052
Test = "1"
53+
VectorInterface = "0.4, 0.5, 0.6"
5154
julia = "1.10"
5255

5356
[extras]
@@ -61,6 +64,7 @@ IntervalSets = "8197267c-284f-5f27-9208-e0e47529a953"
6164
ParallelTestRunner = "d3525ed8-44d0-4b2c-a655-542cee43accc"
6265
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
6366
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
67+
VectorInterface = "409d34a3-91d5-4945-b6ec-7529ddf182d8"
6468

6569
[targets]
66-
test = ["Aqua", "BandedMatrices", "BlockBandedMatrices", "Documenter", "DualNumbers", "FFTW", "ParallelTestRunner", "Random", "Test"]
70+
test = ["Aqua", "BandedMatrices", "BlockBandedMatrices", "Documenter", "DualNumbers", "FFTW", "ParallelTestRunner", "Random", "Test", "VectorInterface"]

‎ext/ApproxFunVectorInterfaceExt.jl‎

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
module ApproxFunVectorInterfaceExt
2+
3+
using ApproxFun: Fun, space, coefficients, innerproduct
4+
using ApproxFun.ApproxFunBase: spacescompatible
5+
using VectorInterface: VectorInterface, One, scalartype, zerovector, zerovector!,
6+
scale, scale!, scale!!, add, add!
7+
8+
# Funs in compatible spaces share a coefficient basis, so the vector operations
9+
# can act directly on the (possibly differently truncated) coefficient vectors.
10+
# The in-place (`!`) variants require compatible spaces and mutable coefficient
11+
# vectors; the `!!` variants fall back to the out-of-place versions otherwise.
12+
13+
VectorInterface.scalartype(::Type{<:Fun{<:Any,T}}) where {T} = scalartype(T)
14+
15+
function _check_compatible(y::Fun, x::Fun)
16+
spacescompatible(y, x) || throw(ArgumentError(
17+
"in-place operations require Funs in compatible spaces, got $(space(y)) and $(space(x))"))
18+
return nothing
19+
end
20+
21+
_resizable(c) = c isa Vector
22+
23+
function VectorInterface.zerovector(f::Fun, ::Type{S}) where {S<:Number}
24+
return Fun(space(f), zerovector(coefficients(f), S))
25+
end
26+
VectorInterface.zerovector!(f::Fun) = (zerovector!(coefficients(f)); f)
27+
function VectorInterface.zerovector!!(f::Fun)
28+
c = coefficients(f)
29+
c′ = VectorInterface.zerovector!!(c)
30+
return c′ === c ? f : Fun(space(f), c′)
31+
end
32+
33+
VectorInterface.scale(f::Fun, α::Number) = Fun(space(f), scale(coefficients(f), α))
34+
VectorInterface.scale!(f::Fun, α::Number) = (scale!(coefficients(f), α); f)
35+
function VectorInterface.scale!!(f::Fun, α::Number)
36+
c = coefficients(f)
37+
c′ = scale!!(c, α)
38+
return c′ === c ? f : Fun(space(f), c′)
39+
end
40+
41+
function VectorInterface.scale!(y::Fun, x::Fun, α::Number)
42+
_check_compatible(y, x)
43+
cy, cx = coefficients(y), coefficients(x)
44+
length(cy) == length(cx) || resize!(cy, length(cx))
45+
cy .= scale.(cx, α)
46+
return y
47+
end
48+
function VectorInterface.scale!!(y::Fun, x::Fun, α::Number)
49+
if spacescompatible(y, x) &&
50+
(length(coefficients(y)) == length(coefficients(x)) || _resizable(coefficients(y))) &&
51+
VectorInterface.promote_scale(x, α) <: scalartype(y)
52+
return scale!(y, x, α)
53+
end
54+
return scale(x, α)
55+
end
56+
57+
VectorInterface.add(y::Fun, x::Fun, α::Number, β::Number) = scale(y, β) + scale(x, α)
58+
59+
function VectorInterface.add!(y::Fun, x::Fun, α::Number, β::Number)
60+
_check_compatible(y, x)
61+
cy, cx = coefficients(y), coefficients(x)
62+
ny, nx = length(cy), length(cx)
63+
if ny < nx
64+
resize!(cy, nx)
65+
for i in (ny + 1):nx
66+
@inbounds cy[i] = scale(cx[i], α)
67+
end
68+
add!(view(cy, 1:ny), view(cx, 1:ny), α, β)
69+
else
70+
add!(view(cy, 1:nx), cx, α, β)
71+
β === One() || scale!(view(cy, (nx + 1):ny), β)
72+
end
73+
return y
74+
end
75+
function VectorInterface.add!!(y::Fun, x::Fun, α::Number, β::Number)
76+
if spacescompatible(y, x) &&
77+
(length(coefficients(y)) >= length(coefficients(x)) || _resizable(coefficients(y))) &&
78+
VectorInterface.promote_add(y, x, α, β) <: scalartype(y)
79+
return add!(y, x, α, β)
80+
end
81+
return add(y, x, α, β)
82+
end
83+
84+
VectorInterface.inner(f::Fun, g::Fun) = innerproduct(f, g)
85+
86+
end

‎test/VectorInterfaceTest.jl‎

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
module VectorInterfaceTest
2+
3+
using ApproxFun
4+
using Test
5+
using VectorInterface
6+
using VectorInterface: One, Zero
7+
8+
include(joinpath(@__DIR__, "testutils.jl"))
9+
10+
@verbose @testset "VectorInterface" begin
11+
f = Fun(x -> exp(x) * cos(x))
12+
g = Fun(x -> sin(3x))
13+
pts = range(-1, 1; length=10)
14+
15+
@testset "scalartype" begin
16+
@test scalartype(f) == Float64
17+
@test scalartype(typeof(f)) == Float64
18+
@test scalartype(Fun(x -> cis(x), Fourier())) == ComplexF64
19+
@test scalartype(Fun(Chebyshev(), Float32[0, 1])) == Float32
20+
end
21+
22+
@testset "zerovector" begin
23+
z = zerovector(f)
24+
@test z isa Fun
25+
@test space(z) == space(f)
26+
@test iszero(coefficients(z))
27+
@test scalartype(z) == Float64
28+
29+
z = zerovector(f, ComplexF64)
30+
@test iszero(coefficients(z))
31+
@test scalartype(z) == ComplexF64
32+
33+
h = copy(f)
34+
@test zerovector!(h) === h
35+
@test iszero(coefficients(h))
36+
37+
h = copy(f)
38+
@test zerovector!!(h) === h
39+
@test iszero(coefficients(h))
40+
end
41+
42+
@testset "scale" begin
43+
h = scale(f, 2.5)
44+
@test h(0.3) ≈ 2.5 * f(0.3)
45+
@test coefficients(f) == coefficients(Fun(x -> exp(x) * cos(x)))
46+
47+
h = scale(f, 2im)
48+
@test scalartype(h) == ComplexF64
49+
@test h(0.3) ≈ 2im * f(0.3)
50+
51+
h = copy(f)
52+
@test scale!(h, 3.0) === h
53+
@test h(0.3) ≈ 3 * f(0.3)
54+
@test scale!(h, One()) === h
55+
@test h(0.3) ≈ 3 * f(0.3)
56+
57+
h = copy(f)
58+
@test scale!!(h, 3.0) === h
59+
@test h(0.3) ≈ 3 * f(0.3)
60+
61+
h = copy(f)
62+
h2 = scale!!(h, im)
63+
@test h2 !== h
64+
@test scalartype(h2) == ComplexF64
65+
@test h2(0.3) ≈ im * f(0.3)
66+
67+
h = copy(f)
68+
@test scale!(h, g, 2.0) === h
69+
@test ncoefficients(h) == ncoefficients(g)
70+
@test h(0.3) ≈ 2 * g(0.3)
71+
72+
h = copy(f)
73+
@test scale!!(h, g, 2.0) === h
74+
@test h(0.3) ≈ 2 * g(0.3)
75+
76+
h = copy(f)
77+
h2 = scale!!(h, g, im)
78+
@test h2 !== h
79+
@test h2(0.3) ≈ im * g(0.3)
80+
81+
@test_throws ArgumentError scale!(copy(f), Fun(sin, 0..1), 2.0)
82+
end
83+
84+
@testset "add" begin
85+
for (α, β) in ((2.0, 3.0), (2.0, One()), (One(), One()), (0.5, Zero()))
86+
h = add(copy(f), g, α, β)
87+
@test all(x -> isapprox(h(x), β * f(x) + α * g(x); atol=1e-12), pts)
88+
89+
h = copy(f)
90+
@test add!(h, g, α, β) === h
91+
@test all(x -> isapprox(h(x), β * f(x) + α * g(x); atol=1e-12), pts)
92+
93+
h = copy(g)
94+
@test add!(h, f, α, β) === h
95+
@test all(x -> isapprox(h(x), β * g(x) + α * f(x); atol=1e-12), pts)
96+
97+
h = copy(f)
98+
@test add!!(h, g, α, β) === h
99+
@test all(x -> isapprox(h(x), β * f(x) + α * g(x); atol=1e-12), pts)
100+
end
101+
102+
h = add(copy(f), g)
103+
@test all(x -> isapprox(h(x), f(x) + g(x); atol=1e-12), pts)
104+
h = add(copy(f), g, 2.0)
105+
@test all(x -> isapprox(h(x), f(x) + 2g(x); atol=1e-12), pts)
106+
h = add!(copy(f), g)
107+
@test all(x -> isapprox(h(x), f(x) + g(x); atol=1e-12), pts)
108+
109+
h = copy(f)
110+
h2 = add!!(h, g, im, 1.0)
111+
@test h2 !== h
112+
@test scalartype(h2) == ComplexF64
113+
@test all(x -> isapprox(h2(x), f(x) + im * g(x); atol=1e-12), pts)
114+
115+
f2 = Fun(x -> x^2, 0..1)
116+
g2 = Fun(x -> cospi(2x), Fourier(0..1))
117+
@test_throws ArgumentError add!(copy(f2), g2, 1.0, 1.0)
118+
h = add!!(copy(f2), g2, 2.0, 3.0)
119+
@test all(x -> isapprox(h(x), 3 * f2(x) + 2 * g2(x); atol=1e-10), range(0.1, 0.9; length=5))
120+
h = add(copy(f2), g2, 2.0, 3.0)
121+
@test all(x -> isapprox(h(x), 3 * f2(x) + 2 * g2(x); atol=1e-10), range(0.1, 0.9; length=5))
122+
end
123+
124+
@testset "inner and norm" begin
125+
@test inner(f, g) ≈ sum(f * g)
126+
@test norm(f) ≈ sqrt(inner(f, f))
127+
128+
u = Fun(x -> cis(x), Fourier())
129+
v = Fun(x -> sin(x) * cis(2x), Fourier())
130+
@test inner(u, v) ≈ sum(conj(u) * v)
131+
@test inner(v, u) ≈ conj(inner(u, v))
132+
@test norm(u) ≈ sqrt(real(inner(u, u)))
133+
end
134+
135+
@testset "Gram-Schmidt orthogonalization" begin
136+
basis = [Fun(x -> x^k) for k in 0:3]
137+
ortho = typeof(f)[]
138+
for b in basis
139+
v = copy(b)
140+
for q in ortho
141+
v = add!!(v, q, -inner(q, v) / inner(q, q), One())
142+
end
143+
push!(ortho, scale!!(v, 1 / norm(v)))
144+
end
145+
for (i, p) in enumerate(ortho), (j, q) in enumerate(ortho)
146+
@test inner(p, q) ≈ (i == j ? 1.0 : 0.0) atol=1e-10
147+
end
148+
end
149+
end
150+
151+
end # module

0 commit comments

Comments
 (0)