-
Notifications
You must be signed in to change notification settings - Fork 30
Expand file tree
/
Copy pathmethods.jl
More file actions
317 lines (271 loc) · 10.1 KB
/
methods.jl
File metadata and controls
317 lines (271 loc) · 10.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
export FiniteDifferenceMethod, fdm, backward_fdm, forward_fdm, central_fdm
"""
FiniteDifferences.DEFAULT_CONDITION
The default [condition number](https://en.wikipedia.org/wiki/Condition_number) used when
computing bounds. It provides amplification of the ∞-norm when passed to the function's
derivatives.
"""
const DEFAULT_CONDITION = 100
"""
FiniteDifferences.TINY
A tiny number added to some quantities to ensure that division by 0 does not occur.
"""
const TINY = 1e-20
forward_grid(p::Int) = 0:(p - 1)
backward_grid(p::Int) = (1 - p):0
function central_grid(p::Int)
if isodd(p)
return div(1 - p, 2):div(p - 1, 2)
else
return vcat(div(-p, 2):-1, 1:div(p, 2))
end
end
"""
History
A mutable type that tracks several values during adaptive bound computation.
"""
mutable struct History
adapt::Int
eps::Real
bound::Real
step::Real
accuracy::Real
function History(; kwargs...)
h = new()
for (k, v) in kwargs
setfield!(h, k, v)
end
return h
end
end
"""
FiniteDifferenceMethod
Abstract type for all finite differencing method types.
Subtypes of `FiniteDifferenceMethod` are callable with the signature
```
method(f, x; kwargs...)
```
where the keyword arguments can be any of
* `adapt`: The number of adaptive steps to use improve the estimate of `bound`.
* `bound`: Bound on the value of the function and its derivatives at `x`.
* `condition`: The condition number. See [`DEFAULT_CONDITION`](@ref).
* `eps`: The assumed roundoff error. Defaults to `eps()` plus [`TINY`](@ref).
"""
abstract type FiniteDifferenceMethod end
function Base.show(io::IO, x::FiniteDifferenceMethod)
@printf io "FiniteDifferenceMethod:\n"
@printf io " order of method: %d\n" x.p
@printf io " order of derivative: %d\n" x.q
@printf io " grid: %s\n" x.grid
@printf io " coefficients: %s\n" x.coefs
h = x.history
if all(p->isdefined(h, p), propertynames(h))
@printf io " roundoff error: %.2e\n" h.eps
@printf io " bounds on derivatives: %.2e\n" h.bound
@printf io " step size: %.2e\n" h.step
@printf io " accuracy: %.2e\n" h.accuracy
end
end
for D in (:Forward, :Backward, :Central, :Nonstandard)
@eval begin
struct $D{G<:AbstractVector, C<:AbstractVector} <: FiniteDifferenceMethod
p::Int
q::Int
grid::G
coefs::C
history::History
end
(d::$D)(f, x=0.0; kwargs...) = fdm(d, f, x; kwargs...)
end
end
# The below does not apply to Nonstandard, as it has its own constructor
for D in (:Forward, :Backward, :Central)
lcname = lowercase(String(D))
gridf = Symbol(lcname, "_grid")
fdmf = Symbol(lcname, "_fdm")
@eval begin
# Compatibility layer over the "old" API
function $fdmf(p::Integer, q::Integer; adapt=1, kwargs...)
_dep_kwarg(kwargs)
return $D(p, q; adapt=adapt, kwargs...)
end
function $D(p::Integer, q::Integer; adapt=1, kwargs...)
_check_p_q(p, q)
grid = $gridf(p)
coefs = _coefs(grid, p, q)
hist = History(; adapt=adapt, kwargs...)
return $D{typeof(grid), typeof(coefs)}(Int(p), Int(q), grid, coefs, hist)
end
@doc """
FiniteDifferences.$($(Meta.quot(D)))(p, q; kwargs...)
$($(Meta.quot(fdmf)))(p, q; kwargs...)
Construct a $($lcname) finite difference method of order `p` to compute the `q`th
derivative.
See [`FiniteDifferenceMethod`](@ref) for more details.
"""
($D, $fdmf)
end
end
"""
FiniteDifferences.Nonstandard(grid, q; kwargs...)
An finite differencing method which is constructed based on a user-defined grid. It is
nonstandard in the sense that it represents neither forward, backward, nor central
differencing.
See [`FiniteDifferenceMethod`](@ref) for further details.
"""
function Nonstandard(grid::AbstractVector{<:Real}, q::Integer; adapt=0, kwargs...)
p = length(grid)
_check_p_q(p, q)
coefs = _coefs(grid, p, q)
hist = History(; adapt=adapt, kwargs...)
return Nonstandard{typeof(grid), typeof(coefs)}(Int(p), Int(q), grid, coefs, hist)
end
# Check the method and derivative orders for consistency
function _check_p_q(p::Integer, q::Integer)
q >= 0 || throw(ArgumentError("order of derivative must be nonnegative"))
q < p || throw(ArgumentError("order of the method must be strictly greater than that " *
"of the derivative"))
# Check whether the method can be computed. We require the factorial of the
# method order to be computable with regular `Int`s, but `factorial` will overflow
# after 20, so 20 is the largest we can allow.
p > 20 && throw(ArgumentError("order of the method is too large to be computed"))
return
end
# Compute coefficients for the method
function _coefs(grid::AbstractVector{<:Real}, p::Integer, q::Integer)
C = [g^i for i in 0:(p - 1), g in grid]
x = zeros(Int, p)
x[q + 1] = factorial(q)
return C \ x
end
# Estimate the bound on the function value and its derivatives at a point
_estimate_bound(x, cond) = cond * maximum(abs, x) + TINY
"""
fdm(m::FiniteDifferenceMethod, f, x[, Val(false)]; kwargs...) -> Real
fdm(m::FiniteDifferenceMethod, f, x, Val(true); kwargs...) -> Tuple{FiniteDifferenceMethod, Real}
Compute the derivative of `f` at `x` using the finite differencing method `m`.
The optional `Val` argument dictates whether the method should be returned alongside the
derivative value, which can be useful for examining the step size used and other such
parameters.
The recognized keywords are:
* `adapt`: The number of adaptive steps to use improve the estimate of `bound`.
* `bound`: Bound on the value of the function and its derivatives at `x`.
* `condition`: The condition number. See [`DEFAULT_CONDITION`](@ref).
* `eps`: The assumed roundoff error. Defaults to `eps()` plus [`TINY`](@ref).
* `track_history`: wether to update the history of the method `m` with e.g. accuracy stats.
!!! warning
Bounds can't be adaptively computed over nonstandard grids; passing a value for
`adapt` greater than 0 when `m::Nonstandard` results in an error.
!!! note
Calling [`FiniteDifferenceMethod`](@ref) objects is equivalent to passing them to `fdm`.
# Examples
```julia-repl
julia> fdm(central_fdm(5, 1), sin, 1; adapt=2)
0.5403023058681039
julia> fdm(central_fdm(2, 1), exp, 0, Val(true))
(FiniteDifferenceMethod:
order of method: 2
order of derivative: 1
grid: [-1, 1]
coefficients: [-0.5, 0.5]
roundoff error: 1.42e-14
bounds on derivatives: 1.00e+02
step size: 1.69e-08
accuracy: 1.69e-06
, 1.0000000031817473)
```
"""
function fdm(
m::M,
f,
x::T,
::Val{true};
condition=DEFAULT_CONDITION,
bound::T=_estimate_bound(f(x)::T, condition)::T,
eps::T=(Base.eps(float(bound)) + TINY)::T,
adapt=m.history.adapt,
max_step=0.1,
track_history=true # will be set to false if `Val{false}()` used
)::Tuple{M, T} where {M<:FiniteDifferenceMethod, T}
if M <: Nonstandard && adapt > 0
throw(ArgumentError("can't adaptively compute bounds over Nonstandard grids"))
end
eps > 0 || throw(ArgumentError("eps must be positive, got $eps"))
bound > 0 || throw(ArgumentError("bound must be positive, got $bound"))
0 <= adapt < 20 - m.p || throw(ArgumentError("can't perform $adapt adaptation steps"))
# The below calculation can fail for `m.q == 0`, because then `C₂` may be zero. We
# therefore handle this edge case here.
m.q == 0 && return m, f(x)
p = m.p
q = m.q
grid = m.grid
coefs = m.coefs
# Adaptively compute the bound on the function and derivative values, if applicable.
if adapt > 0
newm = (M.name.wrapper)(p + 1, p)
dfdx = fdm(
newm,
f,
x;
condition=condition,
eps=eps,
bound=bound,
max_step=max_step,
adapt=(adapt - 1),
)
bound = _estimate_bound(dfdx, condition)
end
# Set the step size by minimising an upper bound on the error of the estimate.
C₁ = eps * sum(abs, coefs)
C₂ = bound * sum(n->abs(coefs[n] * grid[n]^p), eachindex(coefs)) / factorial(p)
ĥ = min((q / (p - q) * C₁ / C₂)^(1 / p), max_step)
# Estimate the value of the derivative.
dfdx_s = sum(eachindex(grid)) do i
(@inbounds coefs[i] * f(x + ĥ * grid[i]))::T
end
dfdx = dfdx_s / ĥ^q
if track_history
# Estimate the accuracy of the method.
accuracy = ĥ^(-q) * C₁ + ĥ^(p - q) * C₂
m.history.eps = eps
m.history.bound = bound
m.history.step = ĥ
m.history.accuracy = accuracy
end
return m, dfdx
end
function fdm(m::FiniteDifferenceMethod, f, x, ::Val{false}=Val(false); kwargs...)
_, dfdx = fdm(m, f, x, Val{true}(); track_history=false, kwargs...)
return dfdx
end
## Deprecations
# Used for keyword argument name deprecations
function _dep_kwarg(kwargs)
for (old, new) in [(:ε, :eps), (:M, :bound)]
haskey(kwargs, old) || continue
val = kwargs[old]
error(
"keyword argument `", old, "` should now be passed as `", new, "` upon ",
"application of the method. For example:\n ",
"central_fdm(5, 1)(f, x; $new=$val)\n",
"not\n ",
"central_fdm(5, 1; $old=$val)(f, x)"
)
end
end
function fdm(
grid::AbstractVector{<:Real},
q::Int,
::Union{Val{true}, Val{false}}=Val(false);
kwargs...,
)
error("to use a custom grid, use `Nonstandard(grid, q)` and pass the result to `fdm`")
end
for fdmf in (:central_fdm, :backward_fdm, :forward_fdm)
@eval function $fdmf(p::Int, q::Int, ::Union{Val{true}, Val{false}}; kwargs...)
error(
"the `Val` argument should now be passed directly to `fdm` after ",
"constructing the method, not to the method constructor itself"
)
end
end