Skip to content

Commit ee042b4

Browse files
authored
allow chebinterp to omit the order and determine it adaptively (#32)
* allow chebinterp to omit the order and determine it adaptively, similar to ApproxFun * use powers of 2 for repeatedly doubled orders to speed up DCTs
1 parent d1a5b6a commit ee042b4

File tree

4 files changed

+71
-9
lines changed

4 files changed

+71
-9
lines changed

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,17 @@ via `c(y)`. Alternatively, you can combine `chebpoints` and `chebinterp` into a
2121
```jl
2222
c = chebinterp(f, order, lb, ub)
2323
```
24-
which evaluates the given function at Chebyshev points for you.
24+
which evaluates the given function at Chebyshev points for you. In fact, you can omit the `order`
25+
and simply call
26+
```jl
27+
c = chebinterp(f, lb, ub)
28+
```
29+
and it will adaptively double the order until a given relative tolerance is attained
30+
(specified by the optional `tol` keyword argument, default to `5*eps` in the
31+
given floating-point precision). The starting order for the doubling can be specified
32+
by a `min_order` keyword (which defaults to `8` in each dimension), and a maximum
33+
can be specified by the `max_order` keyword. This feature is best for *smooth* functions.
34+
(More sophisticated versions of this idea are implemented by [ApproxFun.jl](https://github.com/JuliaApproximation/ApproxFun.jl).)
2535

2636
We also provide a function `chebgradient(c,y)` that returns a tuple `(c(y), ∇c(y))` of
2737
the interpolant and its gradient at a point `y`. (You can also use automatic differentiation, e.g. via the [ForwardDiff.jl package](https://github.com/JuliaDiff/ForwardDiff.jl),

src/interp.jl

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ chebpoints(order::Integer, lb::Real, ub::Real) =
3737

3838
# O(n log n) method to compute Chebyshev coefficients
3939
function chebcoefs(vals::AbstractArray{<:Number,N}) where {N}
40+
all(isfinite, vals) || throw(DomainError("non-finite interpolant value"))
41+
4042
# type-I DCT, except for size-1 dimensions where we want identity
4143
kind = map(n -> n > 1 ? FFTW.REDFT00 : FFTW.DHT, size(vals))
4244
coefs = FFTW.r2r(vals, kind)
@@ -73,7 +75,7 @@ infnorm(x::Number) = abs(x)
7375
infnorm(x::AbstractArray) = maximum(abs, x)
7476

7577
function droptol(coefs::Array{<:Any,N}, tol::Real) where {N}
76-
abstol = sum(infnorm, coefs) * tol # absolute tolerance = L1 norm * tol
78+
abstol = maximum(infnorm, coefs) * tol # absolute tolerance = Linf norm * tol
7779
all(c -> infnorm(c) abstol, coefs) && return coefs # nothing to drop
7880

7981
# compute the new size along each dimension by checking
@@ -103,6 +105,7 @@ epsbounds(lb, ub) = eps(float(real(promote_type(eltype(lb), eltype(ub)))))
103105
"""
104106
chebinterp(vals, lb, ub; tol=eps)
105107
chebinterp(f::Function, order, lb, ub; tol=eps)
108+
chebinterp(f::Function, lb, ub; tol=eps, min_order=(8,..), max_order=(typemax(Int),...))
106109
107110
Given a multidimensional array `vals` of function values (at
108111
points corresponding to the coordinates returned by `chebpoints`),
@@ -115,6 +118,12 @@ or a tuple of integers), and it will call [`chebpoints`](@ref) for you
115118
to obtain the Chebyshev points and then compute `vals` by evaluating
116119
`f` at these points.
117120
121+
If a function `f` is supplied and the `order` argument is omitted, it will adaptively
122+
determine the order by repeatedly doubling it until `tol` is achieved
123+
or `max_order` is reached, starting at `min_order` (which defaults to `8` or
124+
a tuple of `8`s in each dimension; this might need to be increased for
125+
highly oscillatory functions). This feature is best used for smooth functions.
126+
118127
This object `c = chebinterp(...)` can be used to
119128
evaluate the interpolating polynomial at a point `x` via
120129
`c(x)`.
@@ -146,6 +155,11 @@ of vectors are painful to construct.)
146155
chebinterp_v1(vals::AbstractArray{T}, lb, ub; tol::Real=epsvals(vals)) where {T<:Number} =
147156
chebinterp(dropdims(reinterpret(SVector{size(vals,1),T}, Array(vals)), dims=1), lb, ub; tol=tol)
148157

158+
#####################################################################################################
159+
# supplying the function rather than the values
160+
161+
# supplying both function and order:
162+
149163
chebinterp(f::Function, order::NTuple{N,Int}, lb::SVector{N,<:Real}, ub::SVector{N,<:Real}; tol::Real=epsbounds(lb,ub)) where {N} =
150164
chebinterp(map(f, chebpoints(order, lb, ub)), lb, ub; tol=tol)
151165

@@ -154,3 +168,36 @@ chebinterp(f::Function, order::NTuple{N,Int}, lb::AbstractArray{<:Real}, ub::Abs
154168

155169
chebinterp(f::Function, order::Integer, lb::Real, ub::Real; tol::Real=epsbounds(lb,ub)) =
156170
chebinterp(x -> f(@inbounds x[1]), (Int(order),), SVector(lb), SVector(ub); tol=tol)
171+
172+
## adaptively determine the order by repeated doublying, ala chebfun or approxfun:
173+
174+
function chebinterp(f::Function, lb::SVector{N,<:Real}, ub::SVector{N,<:Real};
175+
tol::Real=5epsbounds(lb,ub),
176+
min_order::NTuple{N,Int}=ntuple(i->8,Val{N}()),
177+
max_order::NTuple{N,Int}=ntuple(i->typemax(Int),Val{N}())) where {N}
178+
tol > 0 || throw(ArgumentError("tolerance $tol must be > 0"))
179+
all(min_order .> 0) || throw(ArgumentError("minimum order $min_order must be > 0"))
180+
all(max_order .>= 0) || throw(ArgumentError("maximum order $max_order must be ≥ 0"))
181+
order = min.(min_order, max_order)
182+
while true
183+
# in principle we could re-use function evaluations when doubling the order,
184+
# but that would greatly complicate the code and only saves a factor of 2
185+
c = chebinterp(map(f, chebpoints(order, lb, ub)), lb, ub; tol=tol)
186+
order_done = (size(c.coefs) .- 1 .< order) .| (order .== max_order)
187+
all(order_done) && return c
188+
order = ifelse.(order_done, order, min.(max_order, nextpow.(2, order .* 2)))
189+
end
190+
end
191+
192+
function chebinterp(f::Function, lb::AbstractArray{<:Real}, ub::AbstractArray{<:Real};
193+
tol::Real=5epsbounds(lb,ub),
194+
min_order=fill(8, length(lb)), max_order=fill(typemax(Int), length(lb)))
195+
N = length(lb)
196+
N == length(ub) == length(min_order) == length(max_order) || throw(DimensionMismatch("dimensions must all == $N"))
197+
Base.require_one_based_indexing(min_order, max_order)
198+
chebinterp(f, SVector{N}(lb), SVector{N}(ub);
199+
tol=tol, min_order=ntuple(i -> Int(min_order[i]), N), max_order=ntuple(i -> Int(max_order[i]), N))
200+
end
201+
202+
chebinterp(f::Function, lb::Real, ub::Real; tol::Real=5epsbounds(lb,ub), min_order::Integer=8, max_order::Integer=typemax(Int)) =
203+
chebinterp(x -> f(@inbounds x[1]), SVector(lb), SVector(ub); tol=tol, min_order=(Int(min_order),), max_order=(Int(max_order),))

src/roots.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ to `5eps` where `eps` is the precision of `c`.
8888
function roots(c::ChebPoly{1,<:Real}; tol::Real=5*epsvals(c.coefs), maxsize::Integer=50)
8989
tol > 0 || throw(ArgumentError("tolerance $tol for truncating coefficients must be > 0"))
9090
maxsize > 0 || throw(ArgumentError("maxsize $maxsize must be > 0"))
91-
abstol = sum(abs, c.coefs) * tol # absolute tolerance = L1 norm * tol
91+
abstol = maximum(abs, c.coefs) * tol # absolute tolerance = Linf norm * tol
9292
n = something(findlast(c -> abs(c) abstol, c.coefs), 1)
9393
if n <= maxsize
9494
λ = hesseneigvals!(UpperHessenberg(_colleague_matrix(@view c.coefs[1:n])))

test/runtests.jl

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,13 @@ Random.seed!(314159) # make chainrules tests deterministic
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)))
2727

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]
28+
@test roots(chebinterp(x -> 1.1, 0,1)) Float64[]
29+
@test roots(chebinterp(x -> x - 0.1, 0,1)) [0.1]
30+
p = chebinterp(x -> (x - 0.1)*(x-0.2), 0,1)
31+
@test length(p.coefs) == 3 # degree-2 polynomial
32+
@test roots(p) [0.1, 0.2]
33+
@test roots(chebinterp(cos, 10^4, 0, 1000))/pi (0:317) .+ T(0.5)
34+
@test colleague_matrix(chebinterp(x -> (x - 0.1)*(x-0.2), 0,1)) T[0.5 -0.24; 0.5 -0.2]
3335
end
3436
end
3537

@@ -42,13 +44,16 @@ end
4244
@test eltype(x) == SVector{2,T}
4345
interp = chebinterp(f.(x), lb, ub)
4446
interp2 = chebinterp(f, (48,39), lb, ub)
47+
interp3 = chebinterp(f, lb, ub)
48+
@test all(size(interp3.coefs) .< (40,30))
49+
@test all(size(chebinterp(f, lb, ub, max_order=(18, 4)).coefs) .≤ (19,5))
4550
@test interp isa FastChebInterp.ChebPoly{2,T,T}
4651
@test interp2.coefs interp.coefs
4752
interp0 = chebinterp(f.(x), lb, ub, tol=0)
4853
@test repr("text/plain", interp0) == "ChebPoly{2,$T,$T} order (48, 39) polynomial on [-0.3,0.9] × [0.1,1.2]"
4954
@test ndims(interp) == 2
5055
x1 = T[0.2, 0.8] # not too close to boundary or test_frule can fail by taking a big FD step
51-
@test interp(x1) f(x1)
56+
@test interp(x1) f(x1) interp3(x1)
5257
@test interp(x1) interp0(x1) rtol=10eps(T)
5358
@test all(n -> n[1] < n[2], zip(size(interp.coefs), size(interp0.coefs)))
5459
@test chebgradient(interp, x1) ′ (f(x1), ∇f(x1))

0 commit comments

Comments
 (0)