Skip to content

Commit 8f4c8f4

Browse files
authored
roots(c) function to find the real roots of a chebyshev polynomial (#31)
* roots of chebyshev polynomials * handle degree < 2 cases * added roots * tweak * docs for roots * added tests for roots * document and test colleague_matrix * fix empty-matrix syntax for Julia 1.3 * fix matrix-literal syntax for Julia 1.3 * compat for diagview * grr, diagind also not defined in Julia 1.3 * whoops * paranoia in fallback hesseneigvals * tweak * tweak * consistent truncation
1 parent c425bbf commit 8f4c8f4

File tree

5 files changed

+123
-1
lines changed

5 files changed

+123
-1
lines changed

Project.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@ uuid = "cf66c380-9a80-432c-aff8-4f9c79c0bdde"
33
version = "1.2.0"
44

55
[deps]
6+
ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4"
67
FFTW = "7a1cc6ca-52ef-59f5-83cd-3a7055c09341"
8+
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
79
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
8-
ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4"
910

1011
[compat]
1112
ChainRulesCore = "1"
1213
ChainRulesTestUtils = "1"
1314
FFTW = "1.0"
15+
LinearAlgebra = "<0.0.1, 1"
1416
StaticArrays = "0.12, 1.0"
1517
julia = "1.3"
1618

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ The FastChebInterp package also supports complex and vector-valued functions `f`
3131
the latter case, `c(y)` returns a vector of interpolants, and one can use `chebjacobian(c,y)`
3232
to compute the tuple `(c(y), J(y))` of the interpolant and its Jacobian matrix at `y`.
3333

34+
There is also a function `roots(c)` to return an array of the real roots of `c` on the interval `[lb, ub]`,
35+
computed efficiently by a ["colleague"-matrix method (combined with recursive domain decomposition)](https://epubs.siam.org/doi/10.1137/1.9781611975949.ch18). One can also obtain the colleague matrix (whose eigenvalues are the roots)
36+
directly via `colleague_matrix(c)` (which can be useful to examine complex roots and roots slightly outside `[lb, ub]`).
37+
3438
### Regression from arbitrary points
3539

3640
We also have a function `chebregression(x, y, [lb, ub,], order)` that

src/FastChebInterp.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ Base.zero(c::ChebPoly{N,T,Td}) where {N,T,Td} = ChebPoly{N,T,Td}(zero(c.coefs),
5858
include("interp.jl")
5959
include("regression.jl")
6060
include("eval.jl")
61+
include("roots.jl")
6162
include("chainrules.jl")
6263

6364
end # module

src/roots.jl

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# Computing roots of Chebyshev real-valued polynomials in 1d via colleague matrices on
2+
# recursively subdivided intervals, as described in Trefethen, "Approximation
3+
# Theory and Approximation Practice", chapter 18. https://epubs.siam.org/doi/10.1137/1.9781611975949
4+
#
5+
# Some additional implementation tricks were inspired by the chebfun source
6+
# code: https://github.com/chebfun/chebfun/blob/7574c77680d7e82b79626300bf255498271a72df/%40chebtech/roots.m
7+
8+
using LinearAlgebra
9+
export colleague_matrix, roots
10+
11+
# colleague matrix for 1d array of Chebyshev coefficients, assuming
12+
# trailing zero coefficients have already been dropped.
13+
function _colleague_matrix(coefs::AbstractVector{<:Number})
14+
n = length(coefs)
15+
n <= 1 && return Matrix{float(eltype(coefs))}(undef,0,0) # 0×0 case (no roots)
16+
iszero(coefs[end]) && throw(ArgumentError("trailing coefficient must be nonzero"))
17+
18+
if n == 2 # trivial 1×1 (degree 1) case
19+
C = Matrix{float(eltype(coefs))}(undef,1,1)
20+
C[1,1] = float(-coefs[1])/coefs[2]
21+
return C
22+
else
23+
# colleague matrix, transposed to make it upper-Hessenberg (which doesn't change eigenvalues)
24+
halves = fill(one(float(eltype(coefs)))/2, n-2)
25+
C = diagm(1 => halves, -1 => halves)
26+
C[2,1] = 1
27+
@views C[:,end] .-= coefs[1:end-1] ./ 2coefs[end]
28+
return C
29+
end
30+
end
31+
32+
33+
if !isdefined(LinearAlgebra, :diagview) # added in Julia 1.12
34+
if !isdefined(LinearAlgebra, :diagind)
35+
diagind(A::Matrix) = range(1, step=size(A,1)+1, length=min(size(A)...))
36+
end
37+
diagview(A::Matrix) = @view A[diagind(A)]
38+
end
39+
40+
function _colleague_matrix(coefs::AbstractVector{<:Number}, lb::Real, ub::Real)
41+
C = _colleague_matrix(coefs)
42+
# scale and shift from [-1,1] to [lb,ub] via C * (ub-lb)/2 + (ub+lb)/2 * I
43+
C .*= (ub - lb)/2
44+
diagview(C) .+= (ub + lb)/2
45+
return C
46+
end
47+
48+
"""
49+
colleague_matrix(c::ChebPoly{1,<:Number}; tol=5eps)
50+
51+
Return the "colleague matrix" whose eigenvalues are the roots of the 1d real-valued
52+
Chebyshev polynomial `c`, dropping trailing coefficients whose relative contributions
53+
are `< tol` (defaulting to 5 × floating-point `eps`).
54+
"""
55+
function colleague_matrix(c::ChebPoly{1,<:Number}; tol::Real=5*epsvals(c.coefs))
56+
abstol = sum(abs, c.coefs) * tol # absolute tolerance = L1 norm * tol
57+
n = something(findlast(c -> abs(c) abstol, c.coefs), 1)
58+
return UpperHessenberg(_colleague_matrix(@view(c.coefs[1:n]), c.lb[1], c.ub[1]))
59+
end
60+
61+
function filter_roots(c::ChebPoly{1,<:Real}, roots::AbstractVector{<:Number})
62+
@inbounds lb, ub = c.lb[1], c.ub[1]
63+
htol = eps(float(ub - lb)) * 100 # similar to chebfun
64+
return [ clamp(real(r), lb, ub) for r in roots if abs(imag(r)) < htol && lb - htol <= real(r) <= ub + htol ]
65+
end
66+
67+
if isdefined(LinearAlgebra.LAPACK, :hseqr!) # added in Julia 1.10
68+
# see also LinearAlgebra.jl#1557 - optimized in-place eigenvalues for upper-Hessenberg colleague matrix
69+
function hesseneigvals!(C::UpperHessenberg{T,Matrix{T}}) where {T<:Union{LinearAlgebra.BlasReal, LinearAlgebra.BlasComplex}}
70+
ilo, ihi, _ = LinearAlgebra.LAPACK.gebal!('S', triu!(C.data, -1))
71+
return sort!(LinearAlgebra.LAPACK.hseqr!('E', 'N', 1, size(C,1), C.data, C.data)[3], by=reim)
72+
end
73+
end
74+
hesseneigvals!(C::UpperHessenberg{T,Matrix{T}}) where {T} = eigvals!(triu!(C.data, -1))
75+
76+
"""
77+
roots(c::ChebPoly{1,<:Real}; tol=5eps, maxsize::Integer=50)
78+
79+
Returns a sorted array of the real roots of `c` on the interval `[lb,ub]` (the lower and
80+
upper bounds of the interpolation domain).
81+
82+
Uses a colleague-matrix method combined with recursive subdivision of the domain
83+
to keep the maximum matrix size `≤ maxsize`, following an algorithm described by
84+
Trefethen (*Approximation Theory and Approximation Practice*, ch. 18). The `tol`
85+
argument is a relative tolerance for dropping small polynomial coefficients, defaulting
86+
to `5eps` where `eps` is the precision of `c`.
87+
"""
88+
function roots(c::ChebPoly{1,<:Real}; tol::Real=5*epsvals(c.coefs), maxsize::Integer=50)
89+
tol > 0 || throw(ArgumentError("tolerance $tol for truncating coefficients must be > 0"))
90+
abstol = sum(abs, c.coefs) * tol # absolute tolerance = L1 norm * tol
91+
n = something(findlast(c -> abs(c) abstol, c.coefs), 1)
92+
if n <= maxsize
93+
λ = hesseneigvals!(UpperHessenberg(_colleague_matrix(@view c.coefs[1:n])))
94+
@inbounds λ .=.+ 1) .* ((c.ub[1] - c.lb[1])/2) .+ c.lb[1] # scale and shift to [lb,ub]
95+
return filter_roots(c, λ)
96+
else
97+
# roughly halve the domain, constructing new Chebyshev polynomials on each half, and
98+
# call roots recursively. Following chebfun, we split at an arbitrary point 0.004849834917525
99+
# on [-1,1] rather than at 0 to avoid introducing additional spurious roots (since 0 is
100+
# often a special point by symmetry).
101+
@inbounds split = oftype(float(c.lb[1]), 1.004849834917525) * ((c.ub[1] - c.lb[1])/2) + c.lb[1]
102+
103+
# pick a fast order for the recursive DCT, should be highly composite, say 2^m
104+
order = nextpow(2, n-1)
105+
c1 = chebinterp(c, order, c.lb[1], split; tol=2tol)
106+
c2 = chebinterp(c, order, split, c.ub[1]; tol=2tol)
107+
return vcat(roots(c1; tol=2tol, maxsize=maxsize), roots(c2; tol=2tol, maxsize=maxsize))
108+
end
109+
end

test/runtests.jl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ Random.seed!(314159) # make chainrules tests deterministic
2424
@test chebgradient(interp, x1) ′ (f(x1), f′(x1))
2525
test_frule(interp, x1, rtol=sqrt(eps(T)), atol=sqrt(eps(T)))
2626
test_rrule(interp, x1, rtol=sqrt(eps(T)), atol=sqrt(eps(T)))
27+
28+
@test roots(chebinterp(x -> 1.1, 10, 0,1)) Float64[]
29+
@test roots(chebinterp(x -> x - 0.1, 10, 0,1)) [0.1]
30+
@test roots(chebinterp(x -> (x - 0.1)*(x-0.2), 10, 0,1)) [0.1, 0.2]
31+
@test roots(chebinterp(cos, 10000, 0, 1000))/pi (0:317) .+ T(0.5)
32+
@test colleague_matrix(chebinterp(x -> (x - 0.1)*(x-0.2), 10, 0,1)) T[0.5 -0.24; 0.5 -0.2]
2733
end
2834
end
2935

0 commit comments

Comments
 (0)