Skip to content

Commit a424701

Browse files
authored
Support Jacobi matrix computation via Cholesky and QR (#120)
* workaround for bug * Update README.md * begin also implementing QR variant * rm wrong type test * rename files * more name changing * minor changes * minor changes to tests * make some safety checks optional via bool * fix minor typo in main branch comments * change entry computation + remove redundant field * clarify a comment * add more tests * use known lengths instead of padding * remove redundant line in test * lots of small changes, see description *) split storage 2xN matrix into 2 vectors. *) remove an instance of Float64 hardcoding *) remove an unnecessary infinite tail of zeros *) pre-fill cached arrays to avoid step-by-step expansion * minor type adjustments * remove now unnecessary methods * simplify resizing since we are using vectors now
1 parent 60abdde commit a424701

File tree

5 files changed

+313
-4
lines changed

5 files changed

+313
-4
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,8 @@ julia> associatedlegendre(2)[0.1,1:10]
6969
## p-Finite Element Method
7070

7171
The language of quasi-arrays gives a natural framework for constructing p-finite element methods. The convention
72-
is that adjoint-products are understood as inner products over the axes with uniform weight. Thus to solve Poisson's equation
73-
using its weak formulation with Dirichlet conditions we can expand in a weighted Jacobi basis:
72+
is that adjoint-products are understood as inner products over the axes with uniform weight. To solve Poisson's equation
73+
using its weak formulation with Dirichlet conditions we can thus expand in a weighted Jacobi basis:
7474
```julia
7575
julia> P¹¹ = Jacobi(1.0,1.0); # Quasi-matrix of Jacobi polynomials
7676

src/ClassicalOrthogonalPolynomials.jl

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,8 @@ __sum(::MappedOPLayout, A, dims) = __sum(MappedBasisLayout(), A, dims)
100100
ContinuumArrays.transform_ldiv_if_columns(L::Ldiv{MappedOPLayout,Lay}, ax::OneTo) where Lay = ContinuumArrays.transform_ldiv_if_columns(Ldiv{MappedBasisLayout,Lay}(L.A,L.B), ax)
101101
ContinuumArrays.transform_ldiv_if_columns(L::Ldiv{MappedOPLayout,ApplyLayout{typeof(hcat)}}, ax::OneTo) = ContinuumArrays.transform_ldiv_if_columns(Ldiv{MappedBasisLayout,UnknownLayout}(L.A,L.B), ax)
102102

103-
_equals(::AbstractOPLayout, ::AbstractWeightedBasisLayout, _, _) = false # Weighedt-Legendre doesn't exist
104-
_equals(::AbstractWeightedBasisLayout, ::AbstractOPLayout, _, _) = false # Weighedt-Legendre doesn't exist
103+
_equals(::AbstractOPLayout, ::AbstractWeightedBasisLayout, _, _) = false # Weighted-Legendre doesn't exist
104+
_equals(::AbstractWeightedBasisLayout, ::AbstractOPLayout, _, _) = false # Weighted-Legendre doesn't exist
105105

106106
_equals(::WeightedOPLayout, ::WeightedOPLayout, wP, wQ) = unweighted(wP) == unweighted(wQ)
107107
_equals(::WeightedOPLayout, ::WeightedBasisLayout, wP, wQ) = unweighted(wP) == unweighted(wQ) && weight(wP) == weight(wQ)
@@ -322,6 +322,7 @@ include("classical/chebyshev.jl")
322322
include("classical/ultraspherical.jl")
323323
include("classical/laguerre.jl")
324324
include("classical/fourier.jl")
325+
include("choleskyQR.jl")
325326
include("roots.jl")
326327

327328
end # module

src/choleskyQR.jl

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
"""
2+
cholesky_jacobimatrix(w, P)
3+
4+
returns the Jacobi matrix `X` associated to a quasi-matrix of polynomials
5+
orthogonal with respect to `w(x) w_p(x)` where `w_p(x)` is the weight of the polynomials in `P`.
6+
7+
The resulting polynomials are orthonormal on the same domain as `P`. The supplied `P` must be normalized. Accepted inputs are `w` as a function or `W` as an infinite matrix representing multiplication with the function `w` on the basis `P`.
8+
9+
An optional bool can be supplied, i.e. `cholesky_jacobimatrix(sqrtw, P, false)` to disable checks of symmetry for the weight multiplication matrix and orthonormality for the basis (use with caution).
10+
"""
11+
function cholesky_jacobimatrix(w::Function, P::OrthogonalPolynomial, checks::Bool = true)
12+
checks && !(P isa Normalized) && error("Polynomials must be orthonormal.")
13+
W = Symmetric(P \ (w.(axes(P,1)) .* P)) # Compute weight multiplication via Clenshaw
14+
return cholesky_jacobimatrix(W, P, false) # At this point checks already passed or were entered as false, no need to recheck
15+
end
16+
function cholesky_jacobimatrix(W::AbstractMatrix, P::OrthogonalPolynomial, checks::Bool = true)
17+
checks && !(P isa Normalized) && error("Polynomials must be orthonormal.")
18+
checks && !(W isa Symmetric) && error("Weight modification matrix must be symmetric.")
19+
return SymTridiagonal(CholeskyJacobiBands{:dv}(W,P),CholeskyJacobiBands{:ev}(W,P))
20+
end
21+
22+
# The generated Jacobi operators are symmetric tridiagonal, so we store their data in cached bands
23+
mutable struct CholeskyJacobiBands{dv,T} <: AbstractCachedVector{T}
24+
data::Vector{T} # store band entries, :dv for diagonal, :ev for off-diagonal
25+
U::UpperTriangular{T} # store upper triangular conversion matrix (needed to extend available entries)
26+
UX::ApplyArray{T} # store U*X, where X is the Jacobi matrix of the original P (needed to extend available entries)
27+
datasize::Int # size of so-far computed block
28+
end
29+
30+
# Computes the initial data for the Jacobi operator bands
31+
function CholeskyJacobiBands{:dv}(W, P::OrthogonalPolynomial{T}) where T
32+
U = cholesky(W).U
33+
X = jacobimatrix(P)
34+
UX = ApplyArray(*,U,X)
35+
dv = zeros(T,2) # compute a length 2 vector on first go
36+
dv[1] = dot(view(UX,1,1), U[1,1] \ [one(T)])
37+
dv[2] = dot(view(UX,2,1:2), U[1:2,1:2] \ [zero(T); one(T)])
38+
return CholeskyJacobiBands{:dv,T}(dv, U, UX, 2)
39+
end
40+
function CholeskyJacobiBands{:ev}(W, P::OrthogonalPolynomial{T}) where T
41+
U = cholesky(W).U
42+
X = jacobimatrix(P)
43+
UX = ApplyArray(*,U,X)
44+
ev = zeros(T,2) # compute a length 2 vector on first go
45+
ev[1] = dot(view(UX,1,1:2), U[1:2,1:2] \ [zero(T); one(T)])
46+
ev[2] = dot(view(UX,2,1:3), U[1:3,1:3] \ [zeros(T,2); one(T)])
47+
return CholeskyJacobiBands{:ev,T}(ev, U, UX, 2)
48+
end
49+
50+
size(::CholeskyJacobiBands) = (ℵ₀,) # Stored as an infinite cached vector
51+
52+
# Resize and filling functions for cached implementation
53+
function resizedata!(K::CholeskyJacobiBands, nm::Integer)
54+
νμ = K.datasize
55+
if nm > νμ
56+
resize!(K.data,nm)
57+
cache_filldata!(K, νμ:nm)
58+
K.datasize = nm
59+
end
60+
K
61+
end
62+
function cache_filldata!(J::CholeskyJacobiBands{:dv,T}, inds::UnitRange{Int}) where T
63+
# pre-fill U and UX to prevent expensive step-by-step filling in of cached U and UX in the loop
64+
getindex(J.U,inds[end]+1,inds[end]+1)
65+
getindex(J.UX,inds[end]+1,inds[end]+1)
66+
67+
ek = [zero(T); one(T)]
68+
@inbounds for k in inds
69+
J.data[k] = dot(view(J.UX,k,k-1:k), J.U[k-1:k,k-1:k] \ ek)
70+
end
71+
end
72+
function cache_filldata!(J::CholeskyJacobiBands{:ev, T}, inds::UnitRange{Int}) where T
73+
# pre-fill U and UX to prevent expensive step-by-step filling in of cached U and UX in the loop
74+
getindex(J.U,inds[end]+1,inds[end]+1)
75+
getindex(J.UX,inds[end]+1,inds[end]+1)
76+
77+
ek = [zeros(T,2); one(T)]
78+
@inbounds for k in inds
79+
J.data[k] = dot(view(J.UX,k,k-1:k+1), J.U[k-1:k+1,k-1:k+1] \ ek)
80+
end
81+
end
82+
83+
84+
"""
85+
qr_jacobimatrix(sqrtw, P)
86+
87+
returns the Jacobi matrix `X` associated to a quasi-matrix of polynomials
88+
orthogonal with respect to `w(x) w_p(x)` where `w_p(x)` is the weight of the polynomials in `P`.
89+
90+
The resulting polynomials are orthonormal on the same domain as `P`. The supplied `P` must be normalized. Accepted inputs for `sqrtw` are the square root of the weight modification as a function or `sqrtW` as an infinite matrix representing multiplication with the function `sqrt(w)` on the basis `P`.
91+
92+
An optional bool can be supplied, i.e. `qr_jacobimatrix(sqrtw, P, false)` to disable checks of symmetry for the weight multiplication matrix and orthonormality for the basis (use with caution).
93+
"""
94+
function qr_jacobimatrix(sqrtw::Function, P::OrthogonalPolynomial, checks::Bool = true)
95+
checks && !(P isa Normalized) && error("Polynomials must be orthonormal.")
96+
sqrtW = (P \ (sqrtw.(axes(P,1)) .* P)) # Compute weight multiplication via Clenshaw
97+
return qr_jacobimatrix(sqrtW, P, false) # At this point checks already passed or were entered as false, no need to recheck
98+
end
99+
function qr_jacobimatrix(sqrtW::AbstractMatrix, P::OrthogonalPolynomial, checks::Bool = true)
100+
checks && !(P isa Normalized) && error("Polynomials must be orthonormal.")
101+
checks && !(sqrtW isa Symmetric) && error("Weight modification matrix must be symmetric.")
102+
K = SymTridiagonal(QRJacobiBands{:dv}(sqrtW,P),QRJacobiBands{:ev}(sqrtW,P))
103+
return K
104+
end
105+
106+
# The generated Jacobi operators are symmetric tridiagonal, so we store their data in cached bands
107+
mutable struct QRJacobiBands{dv,T} <: AbstractCachedVector{T}
108+
data::Vector{T} # store band entries, :dv for diagonal, :ev for off-diagonal
109+
U::ApplyArray{T} # store upper triangular conversion matrix (needed to extend available entries)
110+
UX::ApplyArray{T} # store U*X, where X is the Jacobi matrix of the original P (needed to extend available entries)
111+
datasize::Int # size of so-far computed block
112+
end
113+
114+
# Computes the initial data for the Jacobi operator bands
115+
function QRJacobiBands{:dv}(sqrtW, P::OrthogonalPolynomial{T}) where T
116+
U = qr(sqrtW).R
117+
U = ApplyArray(*,Diagonal(sign.(view(U,band(0)))),U)
118+
X = jacobimatrix(P)
119+
UX = ApplyArray(*,U,X)
120+
dv = zeros(T,2) # compute a length 2 vector on first go
121+
dv[1] = dot(view(UX,1,1), U[1,1] \ [one(T)])
122+
dv[2] = dot(view(UX,2,1:2), U[1:2,1:2] \ [zero(T); one(T)])
123+
return QRJacobiBands{:dv,T}(dv, U, UX, 2)
124+
end
125+
function QRJacobiBands{:ev}(sqrtW, P::OrthogonalPolynomial{T}) where T
126+
U = qr(sqrtW).R
127+
U = ApplyArray(*,Diagonal(sign.(view(U,band(0)))),U)
128+
X = jacobimatrix(P)
129+
UX = ApplyArray(*,U,X)
130+
ev = zeros(T,2) # compute a length 2 vector on first go
131+
ev[1] = dot(view(UX,1,1:2), U[1:2,1:2] \ [zero(T); one(T)])
132+
ev[2] = dot(view(UX,2,1:3), U[1:3,1:3] \ [zeros(T,2); one(T)])
133+
return QRJacobiBands{:ev,T}(ev, U, UX, 2)
134+
end
135+
136+
size(::QRJacobiBands) = (ℵ₀,) # Stored as an infinite cached vector
137+
138+
# Resize and filling functions for cached implementation
139+
function resizedata!(K::QRJacobiBands, nm::Integer)
140+
νμ = K.datasize
141+
if nm > νμ
142+
resize!(K.data,nm)
143+
cache_filldata!(K, νμ:nm)
144+
K.datasize = nm
145+
end
146+
K
147+
end
148+
function cache_filldata!(J::QRJacobiBands{:dv,T}, inds::UnitRange{Int}) where T
149+
# pre-fill U and UX to prevent expensive step-by-step filling in of cached U and UX in the loop
150+
getindex(J.U,inds[end]+1,inds[end]+1)
151+
getindex(J.UX,inds[end]+1,inds[end]+1)
152+
153+
ek = [zero(T); one(T)]
154+
@inbounds for k in inds
155+
J.data[k] = dot(view(J.UX,k,k-1:k), J.U[k-1:k,k-1:k] \ ek)
156+
end
157+
end
158+
function cache_filldata!(J::QRJacobiBands{:ev, T}, inds::UnitRange{Int}) where T
159+
# pre-fill U and UX to prevent expensive step-by-step filling in of cached U and UX in the loop
160+
getindex(J.U,inds[end]+1,inds[end]+1)
161+
getindex(J.UX,inds[end]+1,inds[end]+1)
162+
163+
ek = [zeros(T,2); one(T)]
164+
@inbounds for k in inds
165+
J.data[k] = dot(view(J.UX,k,k-1:k+1), J.U[k-1:k+1,k-1:k+1] \ ek)
166+
end
167+
end

test/runtests.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ include("test_ratios.jl")
4141
include("test_normalized.jl")
4242
include("test_lanczos.jl")
4343
include("test_interlace.jl")
44+
include("test_choleskyQR.jl")
4445
include("test_roots.jl")
4546

4647
@testset "Auto-diff" begin

test/test_choleskyQR.jl

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
using Test, ClassicalOrthogonalPolynomials, BandedMatrices, LinearAlgebra, LazyArrays, ContinuumArrays, LazyBandedMatrices, InfiniteLinearAlgebra
2+
import ClassicalOrthogonalPolynomials: CholeskyJacobiBands, cholesky_jacobimatrix, qr_jacobimatrix, QRJacobiBands
3+
import LazyArrays: AbstractCachedMatrix
4+
5+
@testset "Comparison of Cholesky with Lanczos and Classical" begin
6+
@testset "Using Clenshaw for polynomial weights" begin
7+
@testset "w(x) = x^2*(1-x)" begin
8+
P = Normalized(legendre(0..1))
9+
x = axes(P,1)
10+
J = jacobimatrix(P)
11+
wf(x) = x^2*(1-x)
12+
# compute Jacobi matrix via cholesky
13+
Jchol = cholesky_jacobimatrix(wf, P)
14+
# compute Jacobi matrix via classical recurrence
15+
Q = Normalized(Jacobi(1,2)[affine(0..1,Inclusion(-1..1)),:])
16+
Jclass = jacobimatrix(Q)
17+
# compute Jacobi matrix via Lanczos
18+
Jlanc = jacobimatrix(LanczosPolynomial(@.(wf.(x)),Normalized(legendre(0..1))))
19+
# Comparison with Lanczos
20+
@test Jchol[1:500,1:500] Jlanc[1:500,1:500]
21+
# Comparison with Classical
22+
@test Jchol[1:500,1:500] Jclass[1:500,1:500]
23+
end
24+
25+
@testset "w(x) = (1-x^2)" begin
26+
P = Normalized(Legendre()[affine(0..1,Inclusion(-1..1)),:])
27+
x = axes(P,1)
28+
J = jacobimatrix(P)
29+
wf(x) = (1-x^2)
30+
# compute Jacobi matrix via cholesky
31+
Jchol = cholesky_jacobimatrix(wf, P)
32+
# compute Jacobi matrix via Lanczos
33+
Jlanc = jacobimatrix(LanczosPolynomial(@.(wf.(x)),Normalized(legendre(0..1))))
34+
# Comparison with Lanczos
35+
@test Jchol[1:500,1:500] Jlanc[1:500,1:500]
36+
end
37+
38+
@testset "w(x) = (1-x^4)" begin
39+
P = Normalized(legendre(0..1))
40+
x = axes(P,1)
41+
J = jacobimatrix(P)
42+
wf(x) = (1-x^4)
43+
# compute Jacobi matrix via cholesky
44+
Jchol = cholesky_jacobimatrix(wf, P)
45+
# compute Jacobi matrix via Lanczos
46+
Jlanc = jacobimatrix(LanczosPolynomial(@.(wf.(x)),Normalized(legendre(0..1))))
47+
# Comparison with Lanczos
48+
@test Jchol[1:500,1:500] Jlanc[1:500,1:500]
49+
end
50+
51+
@testset "w(x) = (1.014-x^3)" begin
52+
P = Normalized(Legendre()[affine(0..1,Inclusion(-1..1)),:])
53+
x = axes(P,1)
54+
J = jacobimatrix(P)
55+
wf(x) = 1.014-x^4
56+
# compute Jacobi matrix via cholesky
57+
Jchol = cholesky_jacobimatrix(wf, P)
58+
# compute Jacobi matrix via Lanczos
59+
Jlanc = jacobimatrix(LanczosPolynomial(@.(wf.(x)),Normalized(legendre(0..1))))
60+
# Comparison with Lanczos
61+
@test Jchol[1:500,1:500] Jlanc[1:500,1:500]
62+
end
63+
end
64+
65+
@testset "Using Cholesky with exponential weights" begin
66+
@testset "w(x) = exp(x)" begin
67+
P = Normalized(Legendre()[affine(0..1,Inclusion(-1..1)),:])
68+
x = axes(P,1)
69+
J = jacobimatrix(P)
70+
wf(x) = exp(x)
71+
# compute Jacobi matrix via cholesky
72+
Jchol = cholesky_jacobimatrix(wf, P)
73+
# compute Jacobi matrix via Lanczos
74+
Jlanc = jacobimatrix(LanczosPolynomial(@.(wf.(x)),Normalized(legendre(0..1))))
75+
# Comparison with Lanczos
76+
@test Jchol[1:500,1:500] Jlanc[1:500,1:500]
77+
end
78+
79+
@testset "w(x) = (1-x)*exp(x)" begin
80+
P = Normalized(Legendre()[affine(0..1,Inclusion(-1..1)),:])
81+
x = axes(P,1)
82+
J = jacobimatrix(P)
83+
wf(x) = (1-x)*exp(x)
84+
# compute Jacobi matrix via cholesky
85+
Jchol = cholesky_jacobimatrix(wf, P)
86+
# compute Jacobi matrix via Lanczos
87+
Jlanc = jacobimatrix(LanczosPolynomial(@.(wf.(x)),Normalized(Legendre()[affine(0..1,Inclusion(-1..1)),:])))
88+
# Comparison with Lanczos
89+
@test Jchol[1:500,1:500] Jlanc[1:500,1:500]
90+
end
91+
92+
@testset "w(x) = (1-x^2)*exp(x^2)" begin
93+
P = Normalized(legendre(0..1))
94+
x = axes(P,1)
95+
J = jacobimatrix(P)
96+
wf(x) = (1-x)^2*exp(x^2)
97+
# compute Jacobi matrix via decomp
98+
Jchol = cholesky_jacobimatrix(wf, P)
99+
# compute Jacobi matrix via Lanczos
100+
Jlanc = jacobimatrix(LanczosPolynomial(@.(wf.(x)),Normalized(legendre(0..1))))
101+
# Comparison with Lanczos
102+
@test Jchol[1:500,1:500] Jlanc[1:500,1:500]
103+
end
104+
105+
@testset "w(x) = x*(1-x^2)*exp(-x^2)" begin
106+
P = Normalized(Legendre()[affine(0..1,Inclusion(-1..1)),:])
107+
x = axes(P,1)
108+
J = jacobimatrix(P)
109+
wf(x) = x*(1-x^2)*exp(-x^2)
110+
# compute Jacobi matrix via cholesky
111+
Jchol = cholesky_jacobimatrix(wf, P)
112+
# compute Jacobi matrix via Lanczos
113+
Jlanc = jacobimatrix(LanczosPolynomial(@.(wf.(x)),Normalized(legendre(0..1))))
114+
# Comparison with Lanczos
115+
@test Jchol[1:500,1:500] Jlanc[1:500,1:500]
116+
end
117+
end
118+
end
119+
120+
@testset "Comparison of QR with Lanczos" begin
121+
@testset "QR case, w(x) = (1-x)^2" begin
122+
P = Normalized(legendre(0..1))
123+
x = axes(P,1)
124+
J = jacobimatrix(P)
125+
wf(x) = (1-x)^2
126+
sqrtwf(x) = (1-x)
127+
# compute Jacobi matrix via decomp
128+
Jchol = cholesky_jacobimatrix(wf, P)
129+
Jqr = qr_jacobimatrix(sqrtwf, P)
130+
# use alternative inputs
131+
sqrtW = (P \ (sqrtwf.(x) .* P))
132+
Jqralt = qr_jacobimatrix(sqrtW, P, false)
133+
# compute Jacobi matrix via Lanczos
134+
Jlanc = jacobimatrix(LanczosPolynomial(@.(wf.(x)),Normalized(legendre(0..1))))
135+
# Comparison with Lanczos
136+
@test Jchol[1:500,1:500] Jlanc[1:500,1:500]
137+
@test Jqr[1:500,1:500] Jlanc[1:500,1:500]
138+
@test Jqralt[1:500,1:500] Jlanc[1:500,1:500]
139+
end
140+
end

0 commit comments

Comments
 (0)