Skip to content

Commit cfef6f8

Browse files
authored
WIP: Remove add_tiny and add estimate_magitude (#125)
* Remove add_tiny and add estimate_magitude * Improve comment * Add test for #124 * Remove inlines and add tests * Fix equality checking in tests * Fix test * Increase patch version * Actually test the edge case * Add missing change * Convert asserts to tests * Fix and test one other edge case * Add and test estimate_roundoff_error * Bump patch number
1 parent a023666 commit cfef6f8

File tree

3 files changed

+76
-23
lines changed

3 files changed

+76
-23
lines changed

Diff for: Project.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name = "FiniteDifferences"
22
uuid = "26cc04aa-876d-5657-8c51-4c34ba976000"
3-
version = "0.11.4"
3+
version = "0.11.5"
44

55
[deps]
66
ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4"

Diff for: src/methods.jl

+36-14
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,33 @@
11
export FiniteDifferenceMethod, fdm, backward_fdm, forward_fdm, central_fdm, extrapolate_fdm
22

33
"""
4-
add_tiny(x::Union{AbstractFloat, Integer})
4+
estimate_magitude(f, x::T) where T<:AbstractFloat
55
6-
Add a tiny number, 10^{-40}, to a real floating point number `x`, preserving the type. If
7-
`x` is an `Integer`, it is promoted to a suitable floating point type.
6+
Estimate the magnitude of `f` in a neighbourhood of `x`, assuming that the outputs of `f`
7+
have a "typical" order of magnitude. The result should be interpreted as a very rough
8+
estimate. This function deals with the case that `f(x) = 0`.
89
"""
9-
add_tiny(x::T) where T<:AbstractFloat = x + convert(T, 1e-40)
10-
add_tiny(x::Integer) = add_tiny(float(x))
10+
function estimate_magitude(f, x::T) where T<:AbstractFloat
11+
M = float(maximum(abs, f(x)))
12+
M > 0 && (return M)
13+
# Ouch, `f(x) = 0`. But it may not be zero around `x`. We conclude that `x` is likely a
14+
# pathological input for `f`. Perturb `x`. Assume that the pertubed value for `x` is
15+
# highly unlikely also a pathological value for `f`.
16+
Δ = convert(T, 0.1) * max(abs(x), one(x))
17+
return float(maximum(abs, f(x + Δ)))
18+
end
19+
20+
"""
21+
estimate_roundoff_error(f, x::T) where T<:AbstractFloat
22+
23+
Estimate the round-off error of `f(x)`. This function deals with the case that `f(x) = 0`.
24+
"""
25+
function estimate_roundoff_error(f, x::T) where T<:AbstractFloat
26+
# Estimate the round-off error. It can happen that the function is zero around `x`, in
27+
# which case we cannot take `eps(f(x))`. Therefore, we assume a lower bound that is
28+
# equal to `eps(T) / 1000`, which gives `f` four orders of magnitude wiggle room.
29+
return max(eps(estimate_magitude(f, x)), eps(T) / 1000)
30+
end
1131

1232
"""
1333
FiniteDifferences.DEFAULT_CONDITION
@@ -209,7 +229,7 @@ end
209229

210230
# Estimate the bound on the derivative by amplifying the ∞-norm.
211231
function _make_default_bound_estimator(; condition::Real=DEFAULT_CONDITION)
212-
default_bound_estimator(f, x) = condition * maximum(abs, f(x))
232+
default_bound_estimator(f, x) = condition * estimate_magitude(f, x)
213233
return default_bound_estimator
214234
end
215235

@@ -256,17 +276,18 @@ function estimate_step(
256276
) where T<:AbstractFloat
257277
p = length(m.coefs)
258278
q = m.q
259-
f_x = float(f(x))
260279

261-
# Estimate the bound and round-off error.
262-
ε = add_tiny(maximum(eps, f_x)) * factor
263-
M = add_tiny(m.bound_estimator(f, x))
280+
# Estimate the round-off error.
281+
ε = estimate_roundoff_error(f, x) * factor
282+
283+
# Estimate the bound on the derivatives.
284+
M = m.bound_estimator(f, x)
264285

265286
# Set the step size by minimising an upper bound on the error of the estimate.
266287
C₁ = ε * sum(abs, m.coefs)
267288
C₂ = M * sum(n -> abs(m.coefs[n] * m.grid[n]^p), eachindex(m.coefs)) / factorial(p)
268289
# Type inference fails on this, so we annotate it, which gives big performance benefits.
269-
h::T = convert(T, min((q / (p - q) * C₁ / C₂)^(1 / p), max_step))
290+
h::T = convert(T, min((q / (p - q) * (C₁ / C₂))^(1 / p), max_step))
270291

271292
# Estimate the accuracy of the method.
272293
accuracy = h^(-q) * C₁ + h^(p - q) * C₂
@@ -292,7 +313,7 @@ for direction in [:forward, :central, :backward]
292313
grid,
293314
q,
294315
coefs,
295-
_make_adaptive_bound_estimator($fdm_fun, p, adapt, condition, geom=geom),
316+
_make_adaptive_bound_estimator($fdm_fun, p, q, adapt, condition, geom=geom),
296317
)
297318
end
298319

@@ -327,16 +348,17 @@ end
327348

328349
function _make_adaptive_bound_estimator(
329350
constructor::Function,
351+
p::Int,
330352
q::Int,
331353
adapt::Int,
332354
condition::Int;
333355
kw_args...
334356
)
335357
if adapt >= 1
336358
estimate_derivative = constructor(
337-
q + 1, q, adapt=adapt - 1, condition=condition; kw_args...
359+
p + 1, p, adapt=adapt - 1, condition=condition; kw_args...
338360
)
339-
return (f, x) -> maximum(abs, estimate_derivative(f, x))
361+
return (f, x) -> estimate_magitude(x′ -> estimate_derivative(f, x′), x)
340362
else
341363
return _make_default_bound_estimator(condition=condition)
342364
end

Diff for: test/methods.jl

+39-8
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,37 @@
1-
using FiniteDifferences: add_tiny
1+
import FiniteDifferences: estimate_magitude, estimate_roundoff_error
22

33
@testset "Methods" begin
4+
@testset "estimate_magitude" begin
5+
f64(x::Float64) = x
6+
f64_int(x::Float64) = Int(10x)
7+
@test estimate_magitude(f64, 0.0) === 0.1
8+
@test estimate_magitude(f64, 1.0) === 1.0
9+
@test estimate_magitude(f64_int, 0.0) === 1.0
10+
@test estimate_magitude(f64_int, 1.0) === 10.0
11+
12+
f32(x::Float32) = x
13+
f32_int(x::Float32) = Int(10 * x)
14+
@test estimate_magitude(f32, 0f0) === 0.1f0
15+
@test estimate_magitude(f32, 1f0) === 1f0
16+
# In this case, the `Int` is converted with `float`, so we expect a `Float64`.
17+
@test estimate_magitude(f32_int, 0f0) === 1.0
18+
@test estimate_magitude(f32_int, 1f0) === 10.0
19+
end
20+
21+
@testset "estimate_roundoff_error" begin
22+
# `Float64`s:
23+
@test estimate_roundoff_error(identity, 1.0) == eps(1.0)
24+
# Pertubation from `estimate_magitude`:
25+
@test estimate_roundoff_error(identity, 0.0) == eps(0.1)
426

5-
@testset "add_tiny" begin
6-
@test add_tiny(convert(Float64, 5)) isa Float64
7-
@test add_tiny(convert(Float32, 5)) isa Float32
8-
@test add_tiny(convert(Float16, 5)) isa Float16
27+
# `Float32`s:
28+
@test estimate_roundoff_error(identity, 1f0) == eps(1f0)
29+
# Pertubation from `estimate_magitude`:
30+
@test estimate_roundoff_error(identity, 0.0f0) == eps(0.1f0)
931

10-
@test add_tiny(convert(Int, 5)) isa Float64
11-
@test add_tiny(convert(UInt, 5)) isa Float64
12-
@test add_tiny(convert(Bool, 1)) isa Float64
32+
# Test lower bound of `eps(T) / 1000`.
33+
@test estimate_roundoff_error(x -> 1e-100, 0.0) == eps(1.0) / 1000
34+
@test estimate_roundoff_error(x -> 1f-100, 0f0) == eps(1f0) / 1000
1335
end
1436

1537
# The different approaches to approximating the gradient to try.
@@ -131,4 +153,13 @@ using FiniteDifferences: add_tiny
131153
end
132154
end
133155
end
156+
157+
@testset "Derivative of cosc at 0 (#124)" begin
158+
@test central_fdm(5, 1)(cosc, 0) -(pi ^ 2) / 3 atol=1e-9
159+
@test central_fdm(10, 1, adapt=3)(cosc, 0) -(pi ^ 2) / 3 atol=5e-14
160+
end
161+
162+
@testset "Derivative of a constant (#125)" begin
163+
@test central_fdm(2, 1)(x -> 0, 0) 0 atol=1e-10
164+
end
134165
end

0 commit comments

Comments
 (0)