Skip to content

Commit b4dfe82

Browse files
authored
add ArnoldiFit type for higher-degree fitting. Close #290 (#307)
* add ArnoldiFit type for higher-degree fitting. Close #290
1 parent 236bbe3 commit b4dfe82

File tree

3 files changed

+119
-2
lines changed

3 files changed

+119
-2
lines changed

src/common.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ this will use weighted linear least squares. That is, the norm of
8484
using their squares: for a number use `w^2`, for a vector `wᵢ^2`, and for a matrix
8585
specify `W'*W`. This behavior may change in the future.)
8686
87+
For fitting with a large degree, the Vandermonde matrix is exponentially ill-conditioned. The [`ArnoldiFit`](@ref) type introduces an Arnoldi orthogonalization that fixes this problem.
88+
8789
"""
8890
function fit(P::Type{<:AbstractPolynomial},
8991
x::AbstractVector{T},

src/polynomials/standard-basis.jl

Lines changed: 109 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,113 @@ function _polynomial_fit(P::Type{<:StandardBasisPolynomial}, x::AbstractVector{T
344344
P(R.(coeffs), var)
345345
end
346346

347+
## --------------------------------------------------
348+
## More accurate fitting for higher degree polynomials
349+
350+
"""
351+
polyfitA(x, y, n=length(x)-1; var=:x)
352+
fit(ArnoldiFit, x, y, n=length(x)-1; var=:x)
353+
354+
Fit a degree ``n`` or less polynomial through the points ``(x_i, y_i)`` using Arnoldi orthogonalization of the Vandermonde matrix.
355+
356+
The use of a Vandermonde matrix to fit a polynomial to data is exponentially ill-conditioned for larger values of ``n``. The Arnoldi orthogonalization fixes this problem.
357+
358+
# Returns
359+
360+
Returns an instance of `ArnoldiFit`. This object can be used to evaluate the polynomial. To manipulate the polynomial, the object can be `convert`ed to other polynomial types, though there may be some loss in accuracy when doing polynomial evaluations afterwards for higher-degree polynomials.
361+
362+
# Citations:
363+
364+
The two main functions are translations from example code in:
365+
366+
*VANDERMONDE WITH ARNOLDI*;
367+
PABLO D. BRUBECK, YUJI NAKATSUKASA, AND LLOYD N. TREFETHEN;
368+
[arXiv:1911.09988](https://people.maths.ox.ac.uk/trefethen/vander_revised.pdf)
369+
370+
# Examples:
371+
372+
```
373+
f(x) = 1/(1 + 25x^2)
374+
N = 80; xs = [cos(j*pi/N) for j in N:-1:0];
375+
p = fit(Polynomial, xs, f.(xs));
376+
q = fit(ArnoldiFit, xs, f.(xs));
377+
maximum(abs, p(x) - f(x) for x ∈ range(-1,stop=1,length=500)) # 3.304586010148457e16
378+
maximum(abs, q(x) - f(x) for x ∈ range(-1,stop=1,length=500)) # 1.1939520722092922e-7
379+
380+
N = 250; xs = [cos(j*pi/N) for j in N:-1:0];
381+
p = fit(Polynomial, xs, f.(xs));
382+
q = fit(ArnoldiFit, xs, f.(xs));
383+
maximum(abs, p(x) - f(x) for x ∈ range(-1,stop=1,length=500)) # 3.55318186254542e92
384+
maximum(abs, q(x) - f(x) for x ∈ range(-1,stop=1,length=500)) # 8.881784197001252e-16
385+
386+
p = fit(Polynomial, xs, f.(xs), 10); # least-squares fit
387+
q = fit(ArnoldiFit, xs, f.(xs), 10);
388+
maximum(abs, q(x) - p(x) for x ∈ range(-1,stop=1,length=500)) # 4.6775083806238626e-14
389+
Polynomials.norm(q-p, Inf) # 2.2168933355715126e-12 # promotes `q` to `Polynomial`
390+
```
391+
392+
"""
393+
function polyfitA(x, y, n=length(x)-1; var=:x)
394+
m = length(x)
395+
T = eltype(y)
396+
Q = ones(T, m, n+1)
397+
#H = UpperHessenberg(zeros(T, n+1, n))
398+
H = zeros(T, n+1, n)
399+
400+
q = zeros(T, m)
401+
402+
@inbounds for k = 1:n
403+
q .= x .* Q[:,k]
404+
for j in 1:k
405+
λ = dot(Q[:,j], q)/m
406+
H[j,k] = λ
407+
q .-= λ * Q[:,j]
408+
end
409+
H[k+1,k] = norm(q)/sqrt(m)
410+
Q[:,k+1] .= q/H[k+1,k]
411+
end
412+
d = Q \ y
413+
ArnoldiFit(d, H, var)
414+
end
415+
416+
function polyvalA(d, H::AbstractMatrix{S}, s::T) where {T, S}
417+
R = promote_type(T,S)
418+
n = length(d) - 1
419+
W = ones(R, n+1)
420+
@inbounds for k in 1:n
421+
w = s .* W[k]
422+
for j in 1:k
423+
w -= H[j,k] * W[j]
424+
end
425+
W[k+1] = w/H[k+1,k]
426+
end
427+
sum(W[i]*d[i] for i in eachindex(d))
428+
end
429+
430+
# Polynomial Interface
431+
"""
432+
ArnoldiFit
433+
434+
A polynomial type produced through fitting a degree ``n`` or less polynomial to data ``(x_1,y_1),…,(x_N, y_N), N ≥ n+1``, This uses Arnoldi orthogonalization to avoid the exponentially ill-conditioned Vandermonde polynomial. See [`Polynomials.polyfitA`](@ref) for details.
435+
"""
436+
struct ArnoldiFit{T, M<:AbstractArray{T,2}} <: AbstractPolynomial{T}
437+
coeffs::Vector{T}
438+
H::M
439+
var::Symbol
440+
end
441+
export ArnoldiFit
442+
@register ArnoldiFit
443+
domain(::Type{<:ArnoldiFit}) = Interval(-Inf, Inf)
444+
445+
Base.show(io::IO, mimetype::MIME"text/plain", p::ArnoldiFit) = print(io, "ArnoldiFit of degree $(length(p.coeffs)-1)")
446+
447+
(p::ArnoldiFit)(x) = polyvalA(p.coeffs, p.H, x)
448+
449+
fit(::Type{ArnoldiFit}, x::AbstractVector{T}, y::AbstractVector{T}, deg::Int=length(x)-1; var=:x, kwargs...) where{T} = polyfitA(x, y, deg; var=var)
450+
451+
Base.convert(::Type{P}, p::ArnoldiFit) where {P <: AbstractPolynomial} = p(variable(P,p.var))
452+
453+
347454

348455

349456
## --------------------------------------------------
@@ -365,7 +472,7 @@ As noted, this reflects the accuracy of a backward stable computation performed
365472
366473
Pointed out in https://discourse.julialang.org/t/more-accurate-evalpoly/45932/5.
367474
"""
368-
function compensated_horner(p::P, x) where {T, P <: Polynomials.StandardBasisPolynomial{T}}
475+
function compensated_horner(p::P, x) where {T, P <: StandardBasisPolynomial{T}}
369476
compensated_horner(coeffs(p), x)
370477
end
371478

@@ -425,7 +532,7 @@ end
425532
# Condition number of a standard basis polynomial
426533
# rule of thumb: p̂ a compute value
427534
# |p(x) - p̃(x)|/|p(x)| ≤ α(n)⋅u ⋅ cond(p,x), where u = finite precision of compuation (2^-p)
428-
function LinearAlgebra.cond(p::P, x) where {P <: Polynomials.StandardBasisPolynomial}
535+
function LinearAlgebra.cond(p::P, x) where {P <: StandardBasisPolynomial}
429536
= map(abs, p)
430537
(abs(x))/ abs(p(x))
431538
end

test/StandardBasis.jl

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,14 @@ end
351351

352352
end
353353

354+
f(x) = 1/(1 + 25x^2)
355+
N = 250; xs = [cos(j*pi/N) for j in N:-1:0];
356+
q = fit(ArnoldiFit, xs, f.(xs));
357+
@test maximum(abs, q(x) - f(x) for x range(-1,stop=1,length=500)) < 10eps()
358+
q = fit(ArnoldiFit, xs, f.(xs), 100);
359+
@test maximum(abs, q(x) - f(x) for x range(-1,stop=1,length=500)) < sqrt(eps())
360+
361+
354362
# test default (issue #228)
355363
fit(1:3, rand(3))
356364

0 commit comments

Comments
 (0)