add owens_t function#214
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #214 +/- ##
==========================================
+ Coverage 72.39% 75.39% +3.00%
==========================================
Files 22 23 +1
Lines 1007 1130 +123
==========================================
+ Hits 729 852 +123
Misses 278 278 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Unfortunately, the Boost tests don't seem to cover all of the branches, so I'll need to add more. |
Co-authored-by: David Müller-Widmann <devmotion@users.noreply.github.com>
Co-authored-by: David Müller-Widmann <devmotion@users.noreply.github.com>
|
CI failure seems unrelated: But that reminds me that it is probably worth adding a chain rule for |
|
Again, unrelated CI failure? It seems like one of the tests has some non-negligible probability of failing? |
Co-authored-by: David Müller-Widmann <devmotion@users.noreply.github.com>
|
That seems to indicate that some of the partials/tangents are generated incorrectly (or at least in a numerically problematic way) in the ChainRules tests, or some of the derivative rules have numerically problematic regimens (from the posted snipped it's unclear whether the rule or finite differencing would be to blame). |
|
(To be clear, the occasional test failures are for other functions, unrelated to |
Do you plan to add more tests, or is this PR ready for review? |
|
I've added tests to cover the branches that the Boost tests missed. I think it is ready for review? |
|
I was curious and checked the accuracy against Arb: The reference is Arb's verified numerical integration of the integral representation
Setup. 518 Float64 results.
Per dispatch path: Float32 / Float16. These call the Script# Accuracy check of `StatsFuns.owens_t` against Arb.
#
# Standalone script: not part of runtests.jl, and Arblib is not a test dependency.
# Run it in a temporary environment that has StatsFuns and Arblib:
#
# julia> import Pkg; Pkg.activate(; temp = true)
# julia> Pkg.develop(path = dirname(@__DIR__)); Pkg.add("Arblib")
# julia> include("test/owens_t_error_analysis.jl")
#
# The reference is Arb's verified integration of the integral representation
# T(h, a) = 1/(2π) ∫₀ᵃ exp(-h²(1+x²)/2)/(1+x²) dx
# The (h, a) grid covers every dispatch path (T1–T6 and the a > 1 remapping).
# Float32/Float16 just call the Float64 path and round, so they are only spot-checked.
using StatsFuns, Arblib, Printf, Statistics, Test
# 512-bit precision and a large evaluation budget keep the enclosure tight even for
# large h, where the integrand is a sharp spike near x = 0 and T(h, a) is as small
# as ~1e-89. Returns the value and the enclosure radius relative to it.
const INT_OPTS = Arblib.calc_integrate_opt_struct(0, 10_000_000, 10_000, 0, 0)
function owens_t_arb(h, a; prec = 512)
H = Arb(h; prec)
f = x -> exp(-H^2 * (1 + x^2) / 2) / (1 + x^2)
I = Arblib.integrate(f, 0, a; prec, rtol = 1e-30, atol = Arblib.Mag(), opts = INT_OPTS)
T = real(I) / (2 * Arb(π; prec))
value = Float64(T)
return value, Float64(Arblib.radius(Arf, T)) / abs(value)
end
# `meth` is local to `owens_t_dispatch` in src/owens_t.jl; mirror it to name which of
# T1–T6 handles a given (h, a) with 0 ≤ a ≤ 1.
const METH = (1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 3, 4, 4, 4, 4, 5, 6)
function regime_label(h, a)
h == 0 && return "h = 0"
abs(a) == 1 && return "|a| = 1"
abs(a) > 1 && return abs(h) <= 0.67 ? "a > 1, h <= 0.67" : "a > 1, h > 0.67"
return "T$(METH[StatsFuns.owens_t_compute_code(abs(h), abs(a))])"
end
# h and a values straddle the internal breakpoints (owens_t_hrange / owens_t_arange)
# and push past a = 1 into both remapping branches.
const HS = (0.01, 0.02, 0.05, 0.08, 0.1, 0.125, 0.2, 0.26, 0.4, 0.5, 0.6, 0.67,
0.7, 1.0, 1.6, 1.7, 2.0, 2.33, 2.4, 3.0, 3.36, 3.4, 4.0, 4.8, 6.0, 10.0, 20.0)
const AS = (0.01, 0.025, 0.05, 0.09, 0.12, 0.15, 0.25, 0.36, 0.45, 0.5, 0.7, 0.9,
0.99, 0.99999, 1.0, 1.2, 2.0, 5.0, 100.0)
grid = [(h, a) for h in HS for a in AS]
append!(grid, [(0.0, 0.5), (0.0, 2.0), (1.3, -0.4), (-1.3, 0.4), (-2.5, -0.8)]) # h = 0, sign symmetry
results = map(grid) do (h, a)
computed = owens_t(h, a)
ref, relrad = owens_t_arb(h, a)
(; h, a, label = regime_label(h, a),
relerr = abs(computed - ref) / abs(ref), ulps = abs(computed - ref) / eps(ref), relrad)
end
relerrs = [r.relerr for r in results]
ulpss = [r.ulps for r in results]
println("Owen's T Float64 vs Arb — $(length(results)) points")
@printf("relative error: max %.2e mean %.2e median %.2e\n", maximum(relerrs), mean(relerrs), median(relerrs))
@printf("ULP error: max %.0f mean %.1f median %.0f\n", maximum(ulpss), mean(ulpss), median(ulpss))
@printf("max Arb enclosure relative radius: %.1e (eps = %.1e)\n", maximum(r.relrad for r in results), eps(Float64))
println("\nper dispatch path:")
@printf(" %-16s %6s %12s %9s\n", "path", "count", "max relerr", "max ULPs")
for lab in sort(unique(r.label for r in results))
sub = filter(r -> r.label == lab, results)
@printf(" %-16s %6d %12.2e %9.0f\n", lab, length(sub), maximum(r.relerr for r in sub), maximum(r.ulps for r in sub))
end
println("\nworst 5 by relative error:")
@printf(" %8s %10s %-16s %12s %9s\n", "h", "a", "path", "relerr", "ULPs")
for r in sort(results; by = r -> r.relerr, rev = true)[1:5]
@printf(" %8.4g %10.5g %-16s %12.2e %9.0f\n", r.h, r.a, r.label, r.relerr, r.ulps)
end
# Float32/Float16 round the inputs first, so take the reference at the rounded inputs
# to isolate the output error of the fallback from the input rounding.
println("\nFloat32 / Float16 spot check:")
@printf(" %8s %8s %14s %14s\n", "h", "a", "Float32 relerr", "Float16 relerr")
for (h, a) in ((0.25, 0.5), (1.3, 0.8), (2.0, 0.99), (0.5, 2.0))
h32, a32 = Float32(h), Float32(a)
h16, a16 = Float16(h), Float16(a)
ref32, _ = owens_t_arb(Float64(h32), Float64(a32))
ref16, _ = owens_t_arb(Float64(h16), Float64(a16))
@printf(" %8.4g %8.4g %14.2e %14.2e\n", h, a,
abs(owens_t(h32, a32) - ref32) / abs(ref32), abs(owens_t(h16, a16) - ref16) / abs(ref16))
end
@testset "owens_t accuracy vs Arb" begin
@test maximum(relerrs) < 5e-14
@test maximum(r.relrad for r in results) < 1e-20
end |
|
One thing I also noticed: |
Co-authored-by: David Müller-Widmann <devmotion@users.noreply.github.com>
|
I fixed the |
Co-authored-by: Alex Arslan <ararslan@comcast.net>
Co-authored-by: David Müller-Widmann <devmotion@users.noreply.github.com>
devmotion
left a comment
There was a problem hiding this comment.
Looks good to me. If you update the version number, I could tag a new release right away when the PR is merged.
|
Done: bumped the version to 2.2.0 |
Closes #99, with a port of the well-tested and fairly standard
Float64implementation from Boost. Update: including a chain rule for the derivatives.(Various other algorithms were proposed in that issue, but I think it makes sense to start with a trusted baseline.)