Manifold Projection
The following callbacks are designed to provide post-step modifications to preserve geometric behaviors in the solution.
DiffEqCallbacks.ManifoldProjection — Type
ManifoldProjection(
manifold; nlsolve = missing, save = true, autonomous = nothing,
manifold_jacobian = nothing, autodiff = nothing,
resid_prototype = nothing, kwargs...) -> DiscreteCallbackIn many cases, you may want to declare a manifold on which a solution lives. Mathematically, a manifold M is defined by a function g as the set of points where g(u) = 0. An embedded manifold can be a lower dimensional object which constrains the solution. For example, g(u) = E(u) - C where E is the energy of the system in state u, meaning that the energy must be constant (energy preservation). Thus by defining the manifold the solution should live on, you can retain desired properties of the solution.
ManifoldProjection projects the solution of the differential equation to the chosen manifold g, conserving a property while conserving the order. It is a consequence of convergence proofs both in the deterministic and stochastic cases that post-step projection to manifolds keep the same convergence rate, thus any algorithm can be easily extended to conserve properties. If the solution is supposed to live on a specific manifold or conserve such property, this guarantees the conservation law without modifying the convergence properties.
Arguments
manifold: residual function defining the manifold. For an in-place problem, definemanifold(resid, u, p)ormanifold(resid, u, p, t). For an out-of-place problem, definemanifold(u, p)ormanifold(u, p, t). The residual is zero on the manifold.
Keywords
nlsolve = missing: use the built-in single-factorization projection algorithm. Pass a nonlinear solver in the NonlinearSolve.jl format to select another algorithm. Passnothingto use the NonlinearSolve polyalgorithm.save::Bool = true: save immediately after the projection.autonomous = nothing: whethermanifoldomits the time argument. PassVal(true)orVal(false)to avoid runtime branching;nothinginfers the form during initialization.resid_prototype = nothing: prototype defining the residual shape for an in-place problem. Without one, the residual is assumed to have the same shape asu.autodiff = nothing: DifferentiationInterface automatic-differentiation backend used whenmanifold_jacobianis not supplied.manifold_jacobian = nothing: analytic Jacobian ofmanifoldwith respect tou. Use the same calling form asmanifold, with the Jacobian output as the first argument for an in-place problem.kwargs...: additional keywords passed to the built-in projection algorithm or to NonlinearSolve.jl ifnlsolveis notmissing.
Returns
DiscreteCallback: a callback that projects the state after each accepted step. If the nonlinear projection does not converge, the callback terminates the integrator with the projection solver's unsuccessful return code.
Throws
ErrorException: during callback initialization if bothmanifold_jacobianandautodiffarenothing.
Saveat Warning
Note that the ManifoldProjection callback modifies the endpoints of the integration intervals and thus breaks assumptions of internal interpolations. Because of this, the values for given by saveat will not be order-matching. However, the interpolation error can be proportional to the change by the projection, so if the projection is making small changes then one is still safe. However, if there are large changes from each projection, you should consider only saving at stopping/projection times. To do this, set tstops to the same values as saveat. There is a performance hit by doing so because now the integrator is forced to stop at every saving point, but this is guaranteed to match the order of the integrator even with the ManifoldProjection.
References
[1] Ernst Hairer, Christian Lubich, Gerhard Wanner. Geometric Numerical Integration: Structure-Preserving Algorithms for Ordinary Differential Equations. Berlin ; New York :Springer, 2002.
Examples
using ADTypes, DiffEqCallbacks, OrdinaryDiffEq
function unit_circle(resid, u, p, t)
resid[1] = sum(abs2, u) - 1
end
function rotation!(du, u, p, t)
du[1] = -u[2]
du[2] = u[1]
end
prob = ODEProblem(rotation!, [1.0, 0.0], (0.0, 10.0))
cb = ManifoldProjection(unit_circle; resid_prototype = [0.0], autodiff = AutoFiniteDiff())
sol = solve(prob, Tsit5(); callback = cb)Example
Here we solve the harmonic oscillator:
using OrdinaryDiffEq, DiffEqCallbacks, NonlinearSolve, Plots, ADTypes
u0 = ones(2)
function f(du, u, p, t)
du[1] = u[2]
du[2] = -u[1]
end
prob = ODEProblem(f, u0, (0.0, 100.0))ODEProblem with uType Vector{Float64} and tType Float64. In-place: true
Non-trivial mass matrix: false
timespan: (0.0, 100.0)
u0: 2-element Vector{Float64}:
1.0
1.0However, this problem is supposed to conserve energy, and thus we define our manifold to conserve the sum of squares:
function g(resid, u, p, t)
resid[1] = u[2]^2 + u[1]^2 - 2
endg (generic function with 1 method)To build the callback, we just call
cb = ManifoldProjection(g; autodiff = AutoForwardDiff(), resid_prototype = zeros(1))SciMLBase.DiscreteCallback{Returns{Bool}, ManifoldProjection{DiffEqCallbacks.UntypedNonAutonomousFunction{typeof(Main.g)}, Nothing, ADTypes.AutoForwardDiff{nothing, Nothing}, Missing, Base.Pairs{Symbol, Vector{Float64}, Nothing, @NamedTuple{resid_prototype::Vector{Float64}}}, Nothing}, typeof(DiffEqCallbacks.initialize_manifold_projection), typeof(SciMLBase.FINALIZE_DEFAULT), Nothing, Tuple{}}(Returns{Bool}(true), ManifoldProjection{DiffEqCallbacks.UntypedNonAutonomousFunction{typeof(Main.g)}, Nothing, ADTypes.AutoForwardDiff{nothing, Nothing}, Missing, Base.Pairs{Symbol, Vector{Float64}, Nothing, @NamedTuple{resid_prototype::Vector{Float64}}}, Nothing}(DiffEqCallbacks.UntypedNonAutonomousFunction{typeof(Main.g)}(false, Main.g, nothing), nothing, ADTypes.AutoForwardDiff(), nothing, missing, Base.Pairs(:resid_prototype => [0.0]), nothing), DiffEqCallbacks.initialize_manifold_projection, SciMLBase.FINALIZE_DEFAULT, Bool[0, 1], nothing, (), true)Using this callback, the Runge-Kutta method Vern7 conserves energy. Note that the standard saving occurs after the step and before the callback, and thus we set save_everystep=false to turn off all standard saving and let the callback save after the projection is applied.
sol = solve(prob, Vern7(), save_everystep = false, callback = cb)
@show sol.u[end][1]^2 + sol.u[end][2]^2 ≈ 2trueusing Plots
plot(sol, idxs = (1, 2))