Skip to content

Commit 930b116

Browse files
authored
Merge pull request #257 from jverzani/compensated_horner
add compensated_horner method
2 parents 7107f2b + aebc798 commit 930b116

File tree

3 files changed

+106
-2
lines changed

3 files changed

+106
-2
lines changed

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name = "Polynomials"
22
uuid = "f27b6e38-b328-58d1-80ce-0feddd5e7a45"
33
license = "MIT"
44
author = "JuliaMath"
5-
version = "1.1.6"
5+
version = "1.1.7"
66

77
[deps]
88
Intervals = "d8418881-c3e1-53bb-8760-2df7ec849ed5"

src/polynomials/standard-basis.jl

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,3 +297,91 @@ function vander(P::Type{<:StandardBasisPolynomial}, x::AbstractVector{T}, n::Int
297297
end
298298
return A
299299
end
300+
301+
302+
## --------------------------------------------------
303+
"""
304+
compensated_horner(p::P, x)
305+
compensated_horner(ps, x)
306+
307+
Evaluate `p(x)` using a compensation scheme of S. Graillat, Ph. Langlois, N. Louve [Compensated Horner Scheme](https://cadxfem.org/cao/Compensation-horner.pdf). Either a `Polynomial` `p` or it coefficients may be passed in.
308+
309+
The Horner scheme has relative error given by
310+
311+
`|(p(x) - p̂(x))/p(x)| ≤ α(n) ⋅ u ⋅ cond(p, x)`, where `u` is the precision (`2⁻⁵³ = eps()/2`)).
312+
313+
The compensated Horner scheme has relative error bounded by
314+
315+
`|(p(x) - p̂(x))/p(x)| ≤ u + β(n) ⋅ u² ⋅ cond(p, x)`.
316+
317+
As noted, this reflects the accuracy of a backward stable computation performed in doubled working precision `u²`. (E.g., polynomial evaluation of a `Polynomial{Float64}` object through `compensated_horner` is as accurate as evaluation of a `Polynomial{Double64}` object (using the `DoubleFloat` package), but significantly faster.
318+
319+
Pointed out in https://discourse.julialang.org/t/more-accurate-evalpoly/45932/5.
320+
"""
321+
function compensated_horner(p::P, x) where {T, P <: Polynomials.StandardBasisPolynomial{T}}
322+
compensated_horner(coeffs(p), x)
323+
end
324+
325+
# rexpressed from paper to compute horner_sum in same pass
326+
# sᵢ -- horner sum
327+
# c -- compensating term
328+
@inline function compensated_horner(ps, x)
329+
n, T = length(ps), eltype(ps)
330+
aᵢ = ps[end]
331+
sᵢ = aᵢ * _one(x)
332+
c = zero(T) * _one(x)
333+
for i in n-1:-1:1
334+
aᵢ = ps[i]
335+
pᵢ, πᵢ = two_product_fma(sᵢ, x)
336+
sᵢ, σᵢ = two_sum(pᵢ, aᵢ)
337+
c = fma(c, x, πᵢ + σᵢ)
338+
end
339+
sᵢ + c
340+
end
341+
342+
function compensated_horner(ps::Tuple, x::S) where {S}
343+
ps == () && return zero(S)
344+
if @generated
345+
n = length(ps.parameters)
346+
sσᵢ =:(ps[end] * _one(x), zero(S))
347+
c = :(zero(S) * _one(x))
348+
for i in n-1:-1:1
349+
pπᵢ = :(two_product_fma($sσᵢ[1], x))
350+
sσᵢ = :(two_sum($pπᵢ[1], ps[$i]))
351+
Σ = :($pπᵢ[2] + $sσᵢ[2])
352+
c = :(fma($c, x, $Σ))
353+
end
354+
s = :($sσᵢ[1] + $c)
355+
s
356+
else
357+
compensated_horner(ps, x)
358+
end
359+
end
360+
361+
# error-free-transformations (EFT) of a∘b = x + y where x, y floating point, ∘ mathematical
362+
# operator (not ⊕ or ⊗)
363+
@inline function two_sum(a, b)
364+
x = a + b
365+
z = x - a
366+
y = (a - (x-z)) + (b-z)
367+
x, y
368+
end
369+
370+
# return x, y, floats, with x + y = a * b
371+
@inline function two_product_fma(a, b)
372+
x = a * b
373+
y = fma(a, b, -x)
374+
x, y
375+
end
376+
377+
378+
# Condition number of a standard basis polynomial
379+
# rule of thumb: p̂ a compute value
380+
# |p(x) - p̃(x)|/|p(x)| ≤ α(n)⋅u ⋅ cond(p,x), where u = finite precision of compuation (2^-p)
381+
function LinearAlgebra.cond(p::P, x) where {P <: Polynomials.StandardBasisPolynomial}
382+
= Polynomials.:(P)(abs.(coeffs(p)), p.var)
383+
(abs(x))/ abs(p(x))
384+
end
385+
386+
387+

test/StandardBasis.jl

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -342,8 +342,10 @@ end
342342
# issue #209
343343
ps = [P([0,1]), P([0,0,1])]
344344
@test Polynomials.evalpoly.(1/2, ps) [p(1/2) for p in ps]
345+
345346
end
346347

348+
347349
# constant polynomials and type
348350
Ts = (Int, Float32, Float64, Complex{Int}, Complex{Float64})
349351
for P in (Polynomial, ImmutablePolynomial, SparsePolynomial)
@@ -374,7 +376,21 @@ end
374376
@test eltype(p3) == eltype(p2)
375377
end
376378

377-
379+
# compensated_horner
380+
# polynomial evaluation for polynomials with large condition numbers
381+
for P in (Polynomial, ImmutablePolynomial, SparsePolynomial)
382+
x = variable(P{Float64})
383+
f(x) = (x - 1)^20
384+
p = f(x)
385+
e₁ = abs( (f(4/3) - p(4/3))/ p(4/3) )
386+
e₂ = abs( (f(4/3) - Polynomials.compensated_horner(p, 4/3))/ p(4/3) )
387+
λ = cond(p, 4/3)
388+
u = eps()/2
389+
@test λ > 1/u
390+
@test e₁ <= 2 * 20 * u * λ
391+
@test e₁ > u^(1/4)
392+
@test e₂ <= u + u^2 * λ * 100
393+
end
378394
end
379395

380396
@testset "Conversion" begin

0 commit comments

Comments
 (0)