Skip to content

Commit 767a86e

Browse files
Merge pull request #11 from thomvet/Support-nested-duals
removed random numbers from tests; updated readme
2 parents 389e261 + 67e2d77 commit 767a86e

File tree

2 files changed

+84
-21
lines changed

2 files changed

+84
-21
lines changed

README.md

Lines changed: 72 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,17 @@ needed.
2424
### Using dualcache
2525

2626
```julia
27-
dualcache(u::AbstractArray, N = Val{default_cache_size(length(u))})
27+
dualcache(u::AbstractArray, N::Int=ForwardDiff.pickchunksize(length(u)); levels::Int = 1)
28+
dualcache(u::AbstractArray, N::AbstractArray{<:Int})
2829
```
2930

3031
The `dualcache` function builds a `DualCache` object that stores both a version
3132
of the cache for `u` and for the `Dual` version of `u`, allowing use of
32-
pre-cached vectors with forward-mode automatic differentiation. To access the
33-
caches, one uses:
33+
pre-cached vectors with forward-mode automatic differentiation. Note that
34+
`dualcache`, due to its design, is only compatible with arrays that contain concretely
35+
typed elements.
36+
37+
To access the caches, one uses:
3438

3539
```julia
3640
get_tmp(tmp::DualCache, u)
@@ -41,19 +45,29 @@ version of the cache. Otherwise it returns the standard cache (for use in the
4145
calls without automatic differentiation).
4246

4347
In order to preallocate to the right size, the `dualcache` needs to be specified
44-
to have the corrent `N` matching the chunk size of the dual numbers or larger.
48+
to have the correct `N` matching the chunk size of the dual numbers or larger.
49+
If the chunk size `N` specified is too large, `get_tmp` will automatically resize
50+
when dispatching; this remains type-stable and non-allocating, but comes at the
51+
expense of additional memory.
52+
4553
In a differential equation, optimization, etc., the default chunk size is computed
4654
from the state vector `u`, and thus if one creates the `dualcache` via
4755
`dualcache(u)` it will match the default chunking of the solver libraries.
4856

57+
`dualcache` is also compatible with nested automatic differentiation calls through
58+
the `levels` keyword (`N` for each level computed using based on the size of the
59+
state vector) or by specifying `N` as an array of integers of chunk sizes, which
60+
enables full control of chunk sizes on all differentation levels.
61+
4962
### dualcache Example 1: Direct Usage
5063

5164
```julia
52-
randmat = rand(10, 2)
65+
using ForwardDiff, PreallocationTools
66+
randmat = rand(5, 3)
5367
sto = similar(randmat)
5468
stod = dualcache(sto)
5569

56-
function claytonsample!(sto, τ; randmat=randmat)
70+
function claytonsample!(sto, τ, α; randmat=randmat)
5771
sto = get_tmp(sto, τ)
5872
sto .= randmat
5973
τ == 0 && return sto
@@ -62,14 +76,23 @@ function claytonsample!(sto, τ; randmat=randmat)
6276
for i in 1:n
6377
v = sto[i, 2]
6478
u = sto[i, 1]
79+
sto[i, 1] = (1 - u^(-τ) + u^(-τ)*v^(-/(1 + τ))))^(-1/τ)*α
6580
sto[i, 2] = (1 - u^(-τ) + u^(-τ)*v^(-/(1 + τ))))^(-1/τ)
6681
end
6782
return sto
6883
end
6984

70-
ForwardDiff.derivative-> claytonsample!(stod, τ), 0.3)
85+
ForwardDiff.derivative-> claytonsample!(stod, τ, 0.0), 0.3)
86+
ForwardDiff.jacobian(x -> claytonsample!(stod, x[1], x[2]), [0.3; 0.0])
7187
```
7288

89+
In the above, the chunk size of the dual numbers has been selected based on the size
90+
of `randmat`, resulting in a chunk size of 8 in this case. However, since the derivative
91+
is calculated with respect to τ and the Jacobian is calculated with respect to τ and α,
92+
specifying the `dualcache` with `stod = dualcache(sto, 1)` or `stod = dualcache(sto, 2)`,
93+
respectively, would have been the most memory efficient way of performing these calculations
94+
(only really relevant for much larger problems).
95+
7396
### dualcache Example 2: ODEs
7497

7598
```julia
@@ -80,7 +103,7 @@ function foo(du, u, (A, tmp), t)
80103
nothing
81104
end
82105
prob = ODEProblem(foo, ones(5, 5), (0., 1.0), (ones(5,5), zeros(5,5)))
83-
solve(prob, Rosenbrock23())
106+
solve(prob, TRBDF2())
84107
```
85108

86109
fails because `tmp` is only real numbers, but during automatic differentiation
@@ -96,7 +119,7 @@ function foo(du, u, (A, tmp), t)
96119
nothing
97120
end
98121
chunk_size = 5
99-
prob = ODEProblem(foo, ones(5, 5), (0., 1.0), (ones(5,5), dualcache(zeros(5,5), Val{chunk_size})))
122+
prob = ODEProblem(foo, ones(5, 5), (0., 1.0), (ones(5,5), dualcache(zeros(5,5), chunk_size)))
100123
solve(prob, TRBDF2(chunk_size=chunk_size))
101124
```
102125

@@ -114,6 +137,46 @@ chunk_size = 5
114137
prob = ODEProblem(foo, ones(5, 5), (0., 1.0), (ones(5,5), dualcache(zeros(5,5))))
115138
solve(prob, TRBDF2())
116139
```
140+
### dualcache Example 3: Nested AD calls in an optimization problem involving a Hessian matrix
141+
142+
```julia
143+
using LinearAlgebra, OrdinaryDiffEq, PreallocationTools, Optim, GalacticOptim
144+
function foo(du, u, p, t)
145+
tmp = p[2]
146+
A = reshape(p[1], size(tmp.du))
147+
tmp = get_tmp(tmp, u)
148+
mul!(tmp, A, u)
149+
@. du = u + tmp
150+
nothing
151+
end
152+
153+
coeffs = -collect(0.1:0.1:0.4)
154+
cache = dualcache(zeros(2,2), levels = 3)
155+
prob = ODEProblem(foo, ones(2, 2), (0., 1.0), (coeffs, cache))
156+
realsol = solve(prob, TRBDF2(), saveat = 0.0:0.1:10.0, reltol = 1e-8)
157+
158+
function objfun(x, prob, realsol, cache)
159+
prob = remake(prob, u0 = eltype(x).(prob.u0), p = (x, cache))
160+
sol = solve(prob, TRBDF2(), saveat = 0.0:0.1:10.0, reltol = 1e-8)
161+
162+
ofv = 0.0
163+
if any((s.retcode != :Success for s in sol))
164+
ofv = 1e12
165+
else
166+
ofv = sum((sol.-realsol).^2)
167+
end
168+
return ofv
169+
end
170+
fn(x,p) = objfun(x, p[1], p[2], p[3])
171+
optfun = OptimizationFunction(fn, GalacticOptim.AutoForwardDiff())
172+
optprob = OptimizationProblem(optfun, zeros(length(coeffs)), (prob, realsol, cache))
173+
solve(optprob, Newton())
174+
```
175+
Solves an optimization problem for the coefficients, `coeffs`, appearing in a differential equation.
176+
The optimization is done with [Optim.jl](https://github.com/JuliaNLSolvers/Optim.jl)'s `Newton()`
177+
algorithm. Since this involves automatic differentiation in the ODE solver and the calculation
178+
of Hessians, three automatic differentiations are nested within each other. Therefore, the `dualcache`
179+
is specified with `levels = 3`.
117180

118181
## LazyBufferCache
119182

test/core_nesteddual.jl

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -52,34 +52,34 @@ function foo(du, u, p, t)
5252
nothing
5353
end
5454

55-
ps = 3 #use to specify problem size; don't go crazy on this, because of the compilation time...
56-
coeffs = -rand(ps,ps)
55+
ps = 2 #use to specify problem size; don't go crazy on this, because of the compilation time...
56+
coeffs = -collect(0.1:0.1:(ps^2/10))
5757
cache = dualcache(zeros(ps,ps), levels = 3)
5858
prob = ODEProblem(foo, ones(ps, ps), (0., 1.0), (coeffs, cache))
5959
realsol = solve(prob, TRBDF2(), saveat = 0.0:0.1:10.0, reltol = 1e-8)
60-
u0 = rand(length(coeffs))
6160

6261
function objfun(x, prob, realsol, cache)
6362
prob = remake(prob, u0 = eltype(x).(prob.u0), p = (x, cache))
6463
sol = solve(prob, TRBDF2(), saveat = 0.0:0.1:10.0, reltol = 1e-8)
6564

6665
ofv = 0.0
6766
if any((s.retcode != :Success for s in sol))
68-
ofv = 1e12
67+
ofv = 1e12
6968
else
70-
ofv = sum((sol.-realsol).^2)
69+
ofv = sum((sol.-realsol).^2)
7170
end
7271
return ofv
7372
end
7473
fn(x,p) = objfun(x, p[1], p[2], p[3])
7574
optfun = OptimizationFunction(fn, GalacticOptim.AutoForwardDiff())
76-
optprob = OptimizationProblem(optfun, -rand(length(coeffs)), (prob, realsol, cache), chunk_size = 2)
75+
optprob = OptimizationProblem(optfun, zeros(length(coeffs)), (prob, realsol, cache))
7776
newtonsol = solve(optprob, Newton())
7877

79-
@test all(abs.(coeffs[:] .- newtonsol.u) .< 1e-2)
78+
@test all(abs.(coeffs .- newtonsol.u) .< 1e-3)
8079

8180
#an example where chunk_sizes are not the same on all differentiation levels:
82-
cache = dualcache(zeros(ps,ps), [9, 9, 2])
81+
cache = dualcache(zeros(ps,ps), [4, 4, 2])
82+
prob = ODEProblem(foo, ones(ps, ps), (0., 1.0), (coeffs, cache))
8383
realsol = solve(prob, TRBDF2(chunk_size = 2), saveat = 0.0:0.1:10.0, reltol = 1e-8)
8484

8585
function objfun(x, prob, realsol, cache)
@@ -88,17 +88,17 @@ function objfun(x, prob, realsol, cache)
8888

8989
ofv = 0.0
9090
if any((s.retcode != :Success for s in sol))
91-
ofv = 1e12
91+
ofv = 1e12
9292
else
93-
ofv = sum((sol.-realsol).^2)
93+
ofv = sum((sol.-realsol).^2)
9494
end
9595
return ofv
9696
end
9797

9898
fn(x,p) = objfun(x, p[1], p[2], p[3])
9999

100100
optfun = OptimizationFunction(fn, GalacticOptim.AutoForwardDiff())
101-
optprob = OptimizationProblem(optfun, -rand(length(coeffs)), (prob, realsol, cache))
101+
optprob = OptimizationProblem(optfun, zeros(length(coeffs)), (prob, realsol, cache))
102102
newtonsol2 = solve(optprob, Newton())
103103

104-
@test all(abs.(coeffs[:] .- newtonsol2.u) .< 1e-2)
104+
@test all(abs.(coeffs .- newtonsol2.u) .< 1e-3)

0 commit comments

Comments
 (0)