Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
11134a8
a bit of code deduplication
nsajko Jul 20, 2025
2e17039
eliminate common subexpression
nsajko Jul 21, 2025
0011a4f
better accuracy for `cosc` for `Float32`, `Float64` around `x = 0`
nsajko Jul 21, 2025
a3c4a73
test accuracy
nsajko Aug 4, 2025
0ab1f6b
add copyright notice
nsajko Aug 4, 2025
d991f1e
ULPError simplification as per Oscar's suggestion
nsajko Aug 4, 2025
4459c67
ULPError common subexpression elimination
nsajko Aug 6, 2025
2592ee9
ULPError: handle `isinf(approximate)`
nsajko Aug 6, 2025
48b8092
put comment in correct position
nsajko Aug 6, 2025
7140f60
style: get rid of the `let`s/nesting
nsajko Aug 6, 2025
11c29f3
style: simplify `return`s
nsajko Aug 6, 2025
fb2ac2f
fix `ULPError` edge case regarding zero/inf
nsajko Aug 6, 2025
c65115a
test `ULPError` edge cases
nsajko Aug 6, 2025
33717f5
`ULPError`: simplify
nsajko Aug 6, 2025
f677062
`ULPError`: delete dead code
nsajko Aug 6, 2025
c0340a8
`ULPError`: eliminate variables only used once
nsajko Aug 6, 2025
85ace6c
`ULPError`: use `Float32` literals
nsajko Aug 6, 2025
0b05e22
`ULPError`: guard the edge case handling behind a single `if`
nsajko Aug 6, 2025
71126d5
`ULPError`: use `Float64` instead of `BigFloat`
nsajko Aug 6, 2025
cc22fc9
`ULPError`: avoid unnecessary conversion to `BigFloat`
nsajko Aug 6, 2025
ca57e60
delete debugging throw
nsajko Aug 6, 2025
ce8d25d
test: add missing edge case
nsajko Aug 6, 2025
8a60bf0
test: `ULPError`: faitfhul approximation
nsajko Aug 6, 2025
a95af77
Revert "`ULPError`: avoid unnecessary conversion to `BigFloat`"
nsajko Aug 6, 2025
92a9378
handle precision better
nsajko Aug 7, 2025
cee5a83
expand `ULPError` test suite
nsajko Aug 7, 2025
e8319ac
`ulp_error_maximum`: use `Fix1`
nsajko Aug 9, 2025
1ca04f3
use `ulp_error_maximum` instead of a loop
nsajko Aug 9, 2025
4f86cf3
`ULPError`: delete unnecessary method static parameters
nsajko Aug 9, 2025
e933115
improve style
nsajko Aug 11, 2025
57fea2b
get rid of the `struct`
nsajko Aug 16, 2025
b7adfd3
fix
nsajko Aug 16, 2025
c1e72b0
merge two methods into one
nsajko Aug 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 120 additions & 5 deletions base/special/trig.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1104,6 +1104,10 @@ Return a `T(NaN)` if `isnan(x)`.
See also [`sinc`](@ref).
"""
cosc(x::Number) = _cosc(float(x))
function _cosc_generic(x)
pi_x = pi * x
(pi_x*cospi(x)-sinpi(x))/(pi_x*x)
end
function _cosc(x::Number)
# naive cosc formula is susceptible to catastrophic
# cancellation error near x=0, so we use the Taylor series
Expand All @@ -1124,17 +1128,128 @@ function _cosc(x::Number)
end
return π*s
else
return isinf_real(x) ? zero(x) : ((pi*x)*cospi(x)-sinpi(x))/((pi*x)*x)
return isinf_real(x) ? zero(x) : _cosc_generic(x)
end
end

#=

## `cosc(x)` for `x` around the first zero, at `x = 0`

`Float32`:

```sollya
prec = 500!;
accurate = ((pi * x) * cos(pi * x) - sin(pi * x)) / (pi * x * x);
b1 = 0.27001953125;
b2 = 0.449951171875;
domain_0 = [-b1/2, b1];
domain_1 = [b1, b2];
machinePrecision = 24;
freeMonomials = [|1, 3, 5, 7|];
freeMonomialPrecisions = [|machinePrecision, machinePrecision, machinePrecision, machinePrecision|];
polynomial_0 = fpminimax(accurate, freeMonomials, freeMonomialPrecisions, domain_0);
polynomial_1 = fpminimax(accurate, freeMonomials, freeMonomialPrecisions, domain_1);
polynomial_0;
polynomial_1;
```

`Float64`:

```sollya
prec = 500!;
accurate = ((pi * x) * cos(pi * x) - sin(pi * x)) / (pi * x * x);
b1 = 0.1700439453125;
b2 = 0.27001953125;
b3 = 0.340087890625;
b4 = 0.39990234375;
domain_0 = [-b1/2, b1];
domain_1 = [b1, b2];
domain_2 = [b2, b3];
domain_3 = [b3, b4];
machinePrecision = 53;
freeMonomials = [|1, 3, 5, 7, 9, 11|];
freeMonomialPrecisions = [|machinePrecision, machinePrecision, machinePrecision, machinePrecision, machinePrecision, machinePrecision|];
polynomial_0 = fpminimax(accurate, freeMonomials, freeMonomialPrecisions, domain_0);
polynomial_1 = fpminimax(accurate, freeMonomials, freeMonomialPrecisions, domain_1);
polynomial_2 = fpminimax(accurate, freeMonomials, freeMonomialPrecisions, domain_2);
polynomial_3 = fpminimax(accurate, freeMonomials, freeMonomialPrecisions, domain_3);
polynomial_0;
polynomial_1;
polynomial_2;
polynomial_3;
```

=#

function _cos_cardinal_eval(x::AbstractFloat, polynomials_close_to_origin::NTuple)
function choose_poly(a::AbstractFloat, polynomials_close_to_origin::NTuple{2})
((b1, p0), (_, p1)) = polynomials_close_to_origin
if a ≤ b1
p0
else
p1
end
end
function choose_poly(a::AbstractFloat, polynomials_close_to_origin::NTuple{4})
((b1, p0), (b2, p1), (b3, p2), (_, p3)) = polynomials_close_to_origin
if a ≤ b2 # hardcoded binary search
if a ≤ b1
p0
else
p1
end
else
if a ≤ b3
p2
else
p3
end
end
end
a = abs(x)
if (polynomials_close_to_origin !== ()) && (a ≤ polynomials_close_to_origin[end][1])
x * evalpoly(x * x, choose_poly(a, polynomials_close_to_origin))
elseif isinf(x)
typeof(x)(0)
else
_cosc_generic(x)
end
end

const _cosc_f32 = let b = Float32 ∘ Float16
(
(b(0.27), (-3.289868f0, 3.246966f0, -1.1443111f0, 0.20542027f0)),
(b(0.45), (-3.2898617f0, 3.2467577f0, -1.1420113f0, 0.1965574f0)),
)
end

const _cosc_f64 = let b = Float64 ∘ Float16
(
(b(0.17), (-3.289868133696453, 3.2469697011333203, -1.1445109446992934, 0.20918277797812262, -0.023460519561502552, 0.001772485141534688)),
(b(0.27), (-3.289868133695205, 3.246969700970421, -1.1445109360543062, 0.20918254132488637, -0.023457115021035743, 0.0017515112964895303)),
(b(0.34), (-3.289868133634355, 3.246969697075094, -1.1445108347839286, 0.209181201609773, -0.023448079433318045, 0.001726628430505518)),
(b(0.4), (-3.289868133074254, 3.2469696736659346, -1.1445104406286049, 0.20917785794416457, -0.02343378376047161, 0.0017019796223768677)),
)
end

function _cosc(x::Union{Float32, Float64})
if x isa Float32
pols = _cosc_f32
elseif x isa Float64
pols = _cosc_f64
end
_cos_cardinal_eval(x, pols)
end

# hard-code Float64/Float32 Taylor series, with coefficients
# Float64.([(-1)^n*big(pi)^(2n)/((2n+1)*factorial(2n-1)) for n = 1:6])
_cosc(x::Union{Float64,ComplexF64}) =
_cosc(x::ComplexF64) =
fastabs(x) < 0.14 ? x*evalpoly(x^2, (-3.289868133696453, 3.2469697011334144, -1.1445109447325053, 0.2091827825412384, -0.023460810354558236, 0.001781145516372852)) :
isinf_real(x) ? zero(x) : ((pi*x)*cospi(x)-sinpi(x))/((pi*x)*x)
_cosc(x::Union{Float32,ComplexF32}) =
isinf_real(x) ? zero(x) : _cosc_generic(x)
_cosc(x::ComplexF32) =
fastabs(x) < 0.26f0 ? x*evalpoly(x^2, (-3.289868f0, 3.2469697f0, -1.144511f0, 0.20918278f0)) :
isinf_real(x) ? zero(x) : ((pi*x)*cospi(x)-sinpi(x))/((pi*x)*x)
isinf_real(x) ? zero(x) : _cosc_generic(x)
_cosc(x::Float16) = Float16(_cosc(Float32(x)))
_cosc(x::ComplexF16) = ComplexF16(_cosc(ComplexF32(x)))

Expand Down
58 changes: 58 additions & 0 deletions test/math.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
include("testhelpers/EvenIntegers.jl")
using .EvenIntegers

include("testhelpers/ULPError.jl")
using .ULPError

using Random
using LinearAlgebra
using Base.Experimental: @force_compile
Expand All @@ -25,6 +28,55 @@ has_fma = Dict(
BigFloat => true,
)

@testset "meta test: ULPError" begin
examples_f64 = (-3e0, -2e0, -1e0, -1e-1, -1e-10, -0e0, 0e0, 1e-10, 1e-1, 1e0, 2e0, 3e0)::Tuple{Vararg{Float64}}
examples_f16 = Float16.(examples_f64)
examples_f32 = Float32.(examples_f64)
examples = (examples_f16..., examples_f32..., examples_f64...)
@testset "edge cases" begin
@testset "zero error - two equivalent values" begin
@test 0 == @inferred ulp_error(NaN, NaN)
@test 0 == @inferred ulp_error(-NaN, -NaN)
@test 0 == @inferred ulp_error(-NaN, NaN)
@test 0 == @inferred ulp_error(NaN, -NaN)
@test 0 == @inferred ulp_error(Inf, Inf)
@test 0 == @inferred ulp_error(-Inf, -Inf)
@test 0 == @inferred ulp_error(0.0, 0.0)
@test 0 == @inferred ulp_error(-0.0, -0.0)
@test 0 == @inferred ulp_error(-0.0, 0.0)
@test 0 == @inferred ulp_error(0.0, -0.0)
@test 0 == @inferred ulp_error(3.0, 3.0)
@test 0 == @inferred ulp_error(-3.0, -3.0)
end
@testset "infinite error" begin
@test Inf == @inferred ulp_error(NaN, 0.0)
@test Inf == @inferred ulp_error(0.0, NaN)
@test Inf == @inferred ulp_error(NaN, 3.0)
@test Inf == @inferred ulp_error(3.0, NaN)
@test Inf == @inferred ulp_error(NaN, Inf)
@test Inf == @inferred ulp_error(Inf, NaN)
@test Inf == @inferred ulp_error(Inf, -Inf)
@test Inf == @inferred ulp_error(-Inf, Inf)
@test Inf == @inferred ulp_error(0.0, Inf)
@test Inf == @inferred ulp_error(Inf, 0.0)
@test Inf == @inferred ulp_error(3.0, Inf)
@test Inf == @inferred ulp_error(Inf, 3.0)
end
end
@testset "faithful" begin
for x in examples
@test 1 == @inferred ulp_error(x, nextfloat(x, 1))
@test 1 == @inferred ulp_error(x, nextfloat(x, -1))
end
end
@testset "midpoint" begin
for x in examples
a = abs(x)
@test 1 == 2 * @inferred ulp_error(copysign((widen(a) + nextfloat(a, 1))/2, x), x)
end
end
end

@testset "clamp" begin
let
@test clamp(0, 1, 3) == 1
Expand Down Expand Up @@ -685,6 +737,12 @@ end
@test cosc(big"0.5") ≈ big"-1.273239544735162686151070106980114896275677165923651589981338752471174381073817" rtol=1e-76
@test cosc(big"0.499") ≈ big"-1.272045747741181369948389133250213864178198918667041860771078493955590574971317" rtol=1e-76
end

@testset "accuracy of `cosc` around the origin" begin
for t in (Float32, Float64)
@test ulp_error_maximum(cosc, range(start = t(-1), stop = t(1), length = 5000)) < 4
end
end
end

@testset "Irrational args to sinpi/cospi/tanpi/sinc/cosc" begin
Expand Down
47 changes: 47 additions & 0 deletions test/testhelpers/ULPError.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# This file is a part of Julia. License is MIT: https://julialang.org/license

module ULPError
export ulp_error, ulp_error_maximum
function ulp_error(accurate::AbstractFloat, approximate::AbstractFloat)
# the ULP error is usually not required to great accuracy, so `Float32` should be precise enough
zero_return = 0f0
inf_return = Inf32
# handle floating-point edge cases
if !(isfinite(accurate) && isfinite(approximate))
accur_is_nan = isnan(accurate)
approx_is_nan = isnan(approximate)
if accur_is_nan || approx_is_nan
return if accur_is_nan === approx_is_nan
zero_return
else
inf_return
end
end
if isinf(approximate)
return if isinf(accurate) && (signbit(accurate) == signbit(approximate))
zero_return
else
inf_return
end
end
end
acc = if accurate isa Union{Float16, Float32}
# widen for better accuracy when doing so does not impact performance too much
widen(accurate)
else
accurate
end
abs(Float32((approximate - acc) / eps(approximate))::Float32)
end
function ulp_error(accurate, approximate, x::AbstractFloat)
acc = accurate(x)
app = approximate(x)
ulp_error(acc, app)
end
function ulp_error(func::Func, x::AbstractFloat) where {Func}
ulp_error(func ∘ BigFloat, func, x)
end
function ulp_error_maximum(func::Func, iterator) where {Func}
maximum(Base.Fix1(ulp_error, func), iterator)
end
end