Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lazy W operator support #443

Merged
merged 43 commits into from
Jul 29, 2018
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
71147ee
`WOperator` and test
MSeeker1340 Jul 19, 2018
42ce8c5
Merge remote-tracking branch 'origin/master' into lazy-W
MSeeker1340 Jul 20, 2018
a4addeb
Adjust position for `WOperator`
MSeeker1340 Jul 20, 2018
197fc40
Update `calc_W!` for constant cache
MSeeker1340 Jul 20, 2018
1a1f5ad
Scalar `WOperator` compatibility
MSeeker1340 Jul 20, 2018
2f17963
Mass matrix for finite differece `calc_W!`
MSeeker1340 Jul 20, 2018
df84f70
Fix non-scalar adaptive `ImplicitEuler`
MSeeker1340 Jul 20, 2018
c57604b
Merge remote-tracking branch 'origin/master' into lazy-W
MSeeker1340 Jul 20, 2018
9f40d55
Support transformed W
MSeeker1340 Jul 20, 2018
66c62aa
Merge remote-tracking branch 'origin/master' into lazy-W
MSeeker1340 Jul 20, 2018
2c5850e
Merge remote-tracking branch 'origin/master' into lazy-W
MSeeker1340 Jul 22, 2018
f7e425e
Add constructor with uninitiated `mass_matrix`
MSeeker1340 Jul 22, 2018
4bd3617
Add `WOperator` support to in-place `calc_W!`
MSeeker1340 Jul 22, 2018
bb185eb
Update `J` and `W` allocation for `ImplicitEulerCache`
MSeeker1340 Jul 22, 2018
d175f9b
Use `mass_matrix` from `ODEFunction`
MSeeker1340 Jul 24, 2018
7b7a1cd
Merge remote-tracking branch 'origin/master' into lazy-W
MSeeker1340 Jul 24, 2018
08f460d
Merge remote-tracking branch 'origin/master' into lazy-W
MSeeker1340 Jul 25, 2018
54b9029
Safer lazy W predicate
MSeeker1340 Jul 25, 2018
fcc937f
Non-allocating `convert` method
MSeeker1340 Jul 25, 2018
9fe4202
Use lazy cache for `mul!`
MSeeker1340 Jul 25, 2018
e6f639c
Bugfix
MSeeker1340 Jul 26, 2018
5cedc29
Remove `lazy_W` and always use `WOperator`
MSeeker1340 Jul 26, 2018
bf600ea
Constructor using `f`
MSeeker1340 Jul 26, 2018
c48a0de
W_transform consistency
MSeeker1340 Jul 26, 2018
7983293
Update test script
MSeeker1340 Jul 26, 2018
21b1a5b
Improve handling of out-of-place jacobian
MSeeker1340 Jul 26, 2018
be258f8
Merge remote-tracking branch 'origin/master' into lazy-W
MSeeker1340 Jul 28, 2018
66a38fb
Update sdirk methods
MSeeker1340 Jul 28, 2018
f01872d
Update ABM methods
MSeeker1340 Jul 28, 2018
e3c1505
Update bdf methods
MSeeker1340 Jul 28, 2018
3f8cefe
Update EulerIMEX
MSeeker1340 Jul 28, 2018
c4437ea
Update Kencarp Kvaerno methods
MSeeker1340 Jul 28, 2018
1eba5e9
Update Rosenbrock methods
MSeeker1340 Jul 28, 2018
b8ae169
Add back concrete W for functions without `jac`
MSeeker1340 Jul 29, 2018
e0eea48
Bugfix
MSeeker1340 Jul 29, 2018
72cde28
Fix invW
MSeeker1340 Jul 29, 2018
80d072b
Fix differentiation traits tests
MSeeker1340 Jul 29, 2018
87e174d
Fix test scripts
MSeeker1340 Jul 29, 2018
90f377b
Address `jac_prototype=nothing` case
MSeeker1340 Jul 29, 2018
e2beccb
Merge remote-tracking branch 'origin/master' into lazy-W
MSeeker1340 Jul 29, 2018
b520fe0
Integration test
MSeeker1340 Jul 29, 2018
c1e53da
Update REQUIRE
ChrisRackauckas Jul 29, 2018
284fa70
Update utility_tests.jl
ChrisRackauckas Jul 29, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions REQUIRE
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
julia 0.7-beta2
DiffEqBase 3.8.0
DiffEqOperators 3.2.0
Parameters 0.5.0
ForwardDiff 0.7.0
GenericSVD 0.0.2
Expand Down
2 changes: 2 additions & 0 deletions src/OrdinaryDiffEq.jl
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ module OrdinaryDiffEq
# Internal utils
import DiffEqBase: ODE_DEFAULT_NORM, ODE_DEFAULT_ISOUTOFDOMAIN, ODE_DEFAULT_PROG_MESSAGE, ODE_DEFAULT_UNSTABLE_CHECK

using DiffEqOperators: DiffEqArrayOperator

import RecursiveArrayTools: chain, recursivecopy!

using Parameters, GenericSVD, ForwardDiff, RecursiveArrayTools,
Expand Down
13 changes: 9 additions & 4 deletions src/caches/sdirk_caches.jl
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
mutable struct ImplicitEulerCache{uType,rateType,uNoUnitsType,J,UF,JC,uToltype,F} <: OrdinaryDiffEqMutableCache
mutable struct ImplicitEulerCache{uType,rateType,uNoUnitsType,J,W,UF,JC,uToltype,F} <: OrdinaryDiffEqMutableCache
u::uType
uprev::uType
uprev2::uType
Expand All @@ -11,7 +11,7 @@ mutable struct ImplicitEulerCache{uType,rateType,uNoUnitsType,J,UF,JC,uToltype,F
tmp::uType
atmp::uNoUnitsType
J::J
W::J
W::W
uf::UF
jac_config::JC
linsolve::F
Expand All @@ -28,8 +28,13 @@ function alg_cache(alg::ImplicitEuler,u,rate_prototype,uEltypeNoUnits,uBottomElt
tTypeNoUnits,uprev,uprev2,f,t,dt,reltol,p,calck,::Type{Val{true}})

du1 = zero(rate_prototype)
J = fill(zero(uEltypeNoUnits),length(u),length(u)) # uEltype?
W = similar(J)
if DiffEqBase.has_jac(f)
W = WOperator(f, dt)
J = nothing # is J = W.J better?
else
J = fill(zero(uEltypeNoUnits),length(u),length(u)) # uEltype?
W = similar(J)
end
z = similar(u,axes(u))
dz = similar(u,axes(u)); tmp = similar(u,axes(u)); b = similar(u,axes(u))
fsalfirst = zero(rate_prototype)
Expand Down
203 changes: 181 additions & 22 deletions src/derivative_utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,161 @@ function calc_J!(integrator, cache::OrdinaryDiffEqMutableCache, is_compos)
is_compos && (integrator.eigen_est = opnorm(J, Inf))
end

"""
WOperator(mass_matrix,gamma,J[;transform=false])

A linear operator that represents the W matrix of an ODEProblem, defined as

```math
W = MM - \\gamma J
```

or, if `transform=true`:

```math
W = \\frac{1}{\\gamma}MM - J
```

where `MM` is the mass matrix (a regular `AbstractMatrix` or a `UniformScaling`),
`γ` is a real number proportional to the time step, and `J` is the Jacobian
operator (must be a `AbstractDiffEqLinearOperator`). A `WOperator` can also be
constructed using a `*DEFunction` directly as

WOperator(f,gamma[;transform=false])

`f` needs to have a jacobian and `jac_prototype`, but the prototype does not need
to be a diffeq operator --- it will automatically be converted to one.

`WOperator` supports lazy `*` and `mul!` operations, the latter utilizing an
internal cache (can be specified in the constructor; default to regular `Vector`).
It supports all of `AbstractDiffEqLinearOperator`'s interface.
"""
mutable struct WOperator{T,
MType <: Union{UniformScaling,AbstractMatrix},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

diffeqoperators are <: AbstractMatrix?

GType <: Real,
JType <: DiffEqBase.AbstractDiffEqLinearOperator{T}
} <: DiffEqBase.AbstractDiffEqLinearOperator{T}
mass_matrix::MType
gamma::GType
J::JType
transform::Bool # true => W = mm/gamma - J; false => W = mm - gamma*J
_func_cache # cache used in `mul!`
_concrete_form # non-lazy form (matrix/number) of the operator
WOperator(mass_matrix, gamma, J; transform=false) = new{eltype(J),typeof(mass_matrix),
typeof(gamma),typeof(J)}(mass_matrix,gamma,J,transform,nothing,nothing)
end
function WOperator(f::DiffEqBase.AbstractODEFunction, gamma; transform=false)
@assert DiffEqBase.has_jac(f) "f needs to have an associated jacobian"
if isa(f, Union{SplitFunction, DynamicalODEFunction})
error("WOperator does not support $(typeof(f)) yet")
end
# Convert mass matrix, if needed
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fine for now. I am hoping to support time-dependent mass matrices but we can just add an issue that it's a feature to implement in the future.

mass_matrix = f.mass_matrix
if !isa(mass_matrix, Union{AbstractMatrix,UniformScaling})
mass_matrix = convert(AbstractMatrix, mass_matrix)
end
# Convert jacobian, if needed
J = deepcopy(f.jac_prototype)
if !isa(J, DiffEqBase.AbstractDiffEqLinearOperator)
J = DiffEqArrayOperator(J; update_func=f.jac)
end
return WOperator(mass_matrix, gamma, J; transform=transform)
end

set_gamma!(W::WOperator, gamma) = (W.gamma = gamma; W)
DiffEqBase.update_coefficients!(W::WOperator,u,p,t) = (update_coefficients!(W.J,u,p,t); W)
function Base.convert(::Type{AbstractMatrix}, W::WOperator)
if W._concrete_form == nothing
# Allocating
if W.transform
W._concrete_form = W.mass_matrix / W.gamma - convert(AbstractMatrix,W.J)
else
W._concrete_form = W.mass_matrix - W.gamma * convert(AbstractMatrix,W.J)
end
else
# Non-allocating
if W.transform
rmul!(copyto!(W._concrete_form, W.mass_matrix), 1/W.gamma)
axpy!(-1, convert(AbstractMatrix,W.J), W._concrete_form)
else
copyto!(W._concrete_form, W.mass_matrix)
axpy!(-W.gamma, convert(AbstractMatrix,W.J), W._concrete_form)
end
end
W._concrete_form
end
function Base.convert(::Type{Number}, W::WOperator)
if W.transform
W._concrete_form = W.mass_matrix / W.gamma - convert(Number,W.J)
else
W._concrete_form = W.mass_matrix - W.gamma * convert(Number,W.J)
end
W._concrete_form
end
Base.size(W::WOperator, args...) = size(W.J, args...)
function Base.getindex(W::WOperator, i::Int)
if W.transform
W.mass_matrix[i] / W.gamma - W.J[i]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this shows up everywhere and it's constant given by the algorithm, it might make sense to make this a type parameter so the code can simply dispatch off of it, or at least not have large performance cuts because of this conditioning in indexing. I'm not sure this indexing is really used all that much though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this indexing is really used all that much though.

This is true both for factorization (it indexes the concrete matrix instead of W) and mul! (which is lazy).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good

else
W.mass_matrix[i] - W.gamma * W.J[i]
end
end
function Base.getindex(W::WOperator, I::Vararg{Int,N}) where {N}
if W.transform
W.mass_matrix[I...] / W.gamma - W.J[I...]
else
W.mass_matrix[I...] - W.gamma * W.J[I...]
end
end
function Base.:*(W::WOperator, x::Union{AbstractVecOrMat,Number})
if W.transform
(W.mass_matrix*x) / W.gamma - W.J*x
else
W.mass_matrix*x - W.gamma * (W.J*x)
end
end
function Base.:\(W::WOperator, x::Union{AbstractVecOrMat,Number})
if size(W) == () # scalar operator
convert(Number,W) \ x
else
convert(AbstractMatrix,W) \ x
end
end
function LinearAlgebra.mul!(Y::AbstractVecOrMat, W::WOperator, B::AbstractVecOrMat)
if W._func_cache == nothing
# Allocate cache only if needed
W._func_cache = Vector{eltype(W)}(undef, size(Y, 1))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think algorithm caches should be able to supply this during construction when possible. That's an optimization we can add later.

end
if W.transform
# Compute mass_matrix * B
if isa(W.mass_matrix, UniformScaling)
a = W.mass_matrix.λ / W.gamma
@. Y = a * B
else
mul!(Y, W.mass_matrix, B)
lmul!(1/W.gamma, Y)
end
# Compute J * B and subtract
mul!(W._func_cache, W.J, B)
Y .-= W._func_cache
else
# Compute mass_matrix * B
if isa(W.mass_matrix, UniformScaling)
@. Y = W.mass_matrix.λ * B
else
mul!(Y, W.mass_matrix, B)
end
# Compute J * B
mul!(W._func_cache, W.J, B)
# Subtract result
axpy!(-W.gamma, W._func_cache, Y)
end
end

function calc_W!(integrator, cache::OrdinaryDiffEqMutableCache, dtgamma, repeat_step, W_transform=false)
@inbounds begin
@unpack t,dt,uprev,u,f,p = integrator
@unpack J,W,jac_config = cache
@unpack J,W = cache
mass_matrix = integrator.f.mass_matrix
is_compos = typeof(integrator.alg) <: CompositeAlgorithm
alg = unwrap_alg(integrator, true)
Expand All @@ -78,21 +229,14 @@ function calc_W!(integrator, cache::OrdinaryDiffEqMutableCache, dtgamma, repeat_
new_jac = false
else
new_jac = true
calc_J!(integrator, cache, is_compos)
DiffEqBase.update_coefficients!(W,uprev,p,t)
end
# skip calculation of W if step is repeated
if !repeat_step && (!alg_can_repeat_jac(alg) ||
(integrator.iter < 1 || new_jac ||
abs(dt - (t-integrator.tprev)) > 100eps(typeof(integrator.t))))
if W_transform
for j in 1:length(u), i in 1:length(u)
W[i,j] = mass_matrix[i,j]/dtgamma - J[i,j]
end
else
for j in 1:length(u), i in 1:length(u)
W[i,j] = mass_matrix[i,j] - dtgamma*J[i,j]
end
end
W.transform = W_transform
set_gamma!(W, dtgamma)
else
new_W = false
end
Expand All @@ -102,27 +246,42 @@ function calc_W!(integrator, cache::OrdinaryDiffEqMutableCache, dtgamma, repeat_
end

function calc_W!(integrator, cache::OrdinaryDiffEqConstantCache, dtgamma, repeat_step, W_transform=false)
@unpack t,uprev,f = integrator
@unpack t,uprev,p,f = integrator
@unpack uf = cache
mass_matrix = integrator.f.mass_matrix
# calculate W
uf.t = t
isarray = typeof(uprev) <: AbstractArray
iscompo = typeof(integrator.alg) <: CompositeAlgorithm
if !W_transform
if isarray
J = ForwardDiff.jacobian(uf,uprev)
W = I - dtgamma*J
if DiffEqBase.has_jac(f)
J = f.jac(uprev, p, t)
if !isa(f, DiffEqBase.AbstractDiffEqLinearOperator)
J = DiffEqArrayOperator(J)
end
W = WOperator(mass_matrix, dtgamma, J; transform=false)
else
J = ForwardDiff.derivative(uf,uprev)
W = 1 - dtgamma*J
if isarray
J = ForwardDiff.jacobian(uf,uprev)
else
J = ForwardDiff.derivative(uf,uprev)
end
W = mass_matrix - dtgamma*J
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

out of place isn't using WOperator yet?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not all of the time?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it's not necessary all of the time, and if we don't allow a linsolve option on it then it's pointless for now.

end
else
if isarray
J = ForwardDiff.jacobian(uf,uprev)
W = I*inv(dtgamma) - J
if DiffEqBase.has_jac(f)
J = f.jac(uprev, p, t)
if !isa(f, DiffEqBase.AbstractDiffEqLinearOperator)
J = DiffEqArrayOperator(J)
end
W = WOperator(mass_matrix, dtgamma, J; transform=true)
else
J = ForwardDiff.derivative(uf,uprev)
W = inv(dtgamma) - J
if isarray
J = ForwardDiff.jacobian(uf,uprev)
else
J = ForwardDiff.derivative(uf,uprev)
end
W = mass_matrix*inv(dtgamma) - J
end
end
iscompo && (integrator.eigen_est = isarray ? opnorm(J, Inf) : J)
Expand Down
2 changes: 1 addition & 1 deletion src/perform_step/sdirk_perform_step.jl
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ end
c = 7/12 # default correction factor in SPICE (LTE overestimated by DD)
r = c*dt^2 # by mean value theorem 2nd DD equals y''(s)/2 for some s

tmp = r*abs((u - uprev)/dt1 - (uprev - uprev2)/dt2)
tmp = r*abs.((u - uprev)/dt1 - (uprev - uprev2)/dt2)
atmp = calculate_residuals(tmp, uprev, u, integrator.opts.abstol, integrator.opts.reltol,integrator.opts.internalnorm)
integrator.EEst = integrator.opts.internalnorm(atmp)
else
Expand Down
42 changes: 41 additions & 1 deletion test/utility_tests.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using OrdinaryDiffEq: phi, phi, phiv, phiv_timestep, expv, expv_timestep, arnoldi, getH, getV
using LinearAlgebra, SparseArrays, Random, Test
using OrdinaryDiffEq: WOperator, set_gamma!, calc_W!
using OrdinaryDiffEq, LinearAlgebra, SparseArrays, Random, Test, DiffEqOperators

@testset "Exponential Utilities" begin
# Scalar phi
Expand Down Expand Up @@ -67,3 +68,42 @@ using LinearAlgebra, SparseArrays, Random, Test
u = expv_timestep(t, A, B[:, 1]; adaptive=true, tol=tol)
@test norm(u - u_exact) / norm(u_exact) < tol
end

@testset "Derivative Utilities" begin
@testset "WOperator" begin
srand(0); y = zeros(2); b = rand(2)
mm = I; _J = rand(2,2)
W = WOperator(mm, 1.0, DiffEqArrayOperator(_J))
set_gamma!(W, 2.0)
_W = mm - 2.0 * _J
@test convert(AbstractMatrix,W) ≈ _W
@test W * b ≈ _W * b
mul!(y, W, b); @test y ≈ _W * b
end

@testset "calc_W!" begin
A = [-1.0 0.0; 0.0 -0.5]; mm = [2.0 0.0; 0.0 1.0]
u0 = [1.0, 1.0]; tmp = zeros(2)
tspan = (0.0,1.0); dt = 0.01; dtgamma = 0.5dt
concrete_W = mm - dtgamma * A
concrete_Wt = mm/dtgamma - A

# Out-of-place
fun = ODEFunction((u,p,t) -> A*u;
mass_matrix=mm,
jac=(u,p,t) -> A)
integrator = init(ODEProblem(fun,u0,tspan), ImplicitEuler(); adaptive=false, dt=dt)
W = calc_W!(integrator, integrator.cache, dtgamma, false)
@test convert(AbstractMatrix, W) ≈ concrete_W
@test W \ u0 ≈ concrete_W \ u0

# In-place
fun = ODEFunction((du,u,p,t) -> mul!(du,A,u);
mass_matrix=mm,
jac_prototype=DiffEqArrayOperator(A))
integrator = init(ODEProblem(fun,u0,tspan), ImplicitEuler(); adaptive=false, dt=dt)
calc_W!(integrator, integrator.cache, dtgamma, false)
@test convert(AbstractMatrix, integrator.cache.W) ≈ concrete_W
ldiv!(tmp, lu!(W), u0); @test tmp ≈ concrete_W \ u0
end
end