Solving DAEs with Probabilistic Numerics
ProbNumDiffEq.jl provides probabilistic numerical solvers for differential algebraic equations (DAEs). Currently, we recommend using the semi-implicit EK1
algorithm.
For a more general tutorial on DAEs check out the DifferentialEquations.jl DAE tutorial.
Solving mass-matrix DAEs with the EK1
First, define the DAE (here the ROBER problem) as an ODE problem with singular mass matrix:
using ProbNumDiffEq, Plots, LinearAlgebra, OrdinaryDiffEq, ModelingToolkit, LinearAlgebra
function rober(du, u, p, t)
y₁, y₂, y₃ = u
k₁, k₂, k₃ = p
du[1] = -k₁ * y₁ + k₃ * y₂ * y₃
du[2] = k₁ * y₁ - k₃ * y₂ * y₃ - k₂ * y₂^2
du[3] = y₁ + y₂ + y₃ - 1
nothing
end
M = [1 0 0
0 1 0
0 0 0]
f = ODEFunction(rober, mass_matrix=M)
prob_mm = ODEProblem(f, [1.0, 0.0, 0.0], (0.0, 1e5), (0.04, 3e7, 1e4))
ODEProblem with uType Vector{Float64} and tType Float64. In-place: true
timespan: (0.0, 100000.0)
u0: 3-element Vector{Float64}:
1.0
0.0
0.0
We can solve this problem directly with the EK1
:
sol = solve(prob_mm, EK1(), reltol=1e-8, abstol=1e-8)
plot(
sol,
xscale=:log10,
tspan=(1e-6, 1e5),
layout=(3, 1),
legend=false,
ylabel=["u₁(t)" "u₂(t)" "u₃(t)"],
xlabel=["" "" "t"],
denseplot=false,
)
Looks good!
Solving an Index-3 DAE directly
The following is based on the "Automatic Index Reduction of DAEs" tutorial by ModelingToolkit.jl, which demonstrates how the classic Rodas4
solver fails to solve a DAE due to the fact that it is of index 3; which is why ModelingToolkit's automatic index reduction is so useful.
It turns out that our probabilistic numerical solvers can directly solve the index-3 DAE!
First, define the pendulum problem as in the tutorial:
function pendulum!(du, u, p, t)
x, dx, y, dy, T = u
g, L = p
du[1] = dx
du[2] = T * x
du[3] = dy
du[4] = T * y - g
du[5] = x^2 + y^2 - L^2
end
pendulum_fun! = ODEFunction(pendulum!, mass_matrix=Diagonal([1, 1, 1, 1, 0]))
u0 = [1.0, 0, 0, 0, 0];
p = [9.8, 1];
tspan = (0, 5.0);
pendulum_prob = ODEProblem(pendulum_fun!, u0, tspan, p)
ODEProblem with uType Vector{Float64} and tType Float64. In-place: true
timespan: (0.0, 5.0)
u0: 5-element Vector{Float64}:
1.0
0.0
0.0
0.0
0.0
We can try to solve it directly with one of the classic mass-matrix DAE solvers from OrdinaryDiffEq.jl:
solve(pendulum_prob, Rodas4())
retcode: Unstable
Interpolation: specialized 3rd order "free" stiffness-aware interpolation
t: 5-element Vector{Float64}:
0.0
1.0e-6
1.1e-5
3.806859541877657e-5
4.5496933032029966e-5
u: 5-element Vector{Vector{Float64}}:
[1.0, 0.0, 0.0, 0.0, 0.0]
[1.0, 0.0, -4.899999999999997e-12, -9.799999999999994e-6, 0.0]
[1.0, -5.282199999999992e-16, -5.928999999999995e-10, -0.00010779999999999988, -9.04785641150889e-11]
[1.0, -2.2119452819316723e-13, -7.101167990076622e-9, -0.00037307223510401005, -2.651533791865963e-8]
[1.0, -3.1661973720383655e-12, -1.0142857485072981e-8, -0.00044586994371389327, -2.9596714810803877e-6]
It does not work! This is because of the index of the DAE; see for example this explanation from the tutorial.
Does this also hold for the EK1
solver? Let's find out:
sol = solve(pendulum_prob, EK1())
retcode: Success
Interpolation: ODE Filter Posterior
t: 637-element Vector{Float64}:
0.0
1.0e-6
1.1e-5
6.293457247564385e-5
0.0001717966902698356
0.00041700284549204755
0.001043224623113885
0.0023835910941641687
0.003763856534047043
0.005191196217624695
⋮
4.907553725698676
4.924636353074359
4.9411967014972324
4.95668039898637
4.970681486255768
4.981792730511177
4.990762595647369
4.998031888191015
5.0
u: 637-element Vector{Vector{Float64}}:
[1.0, 0.0, 0.0, 0.0, 0.0]
[1.0, -2.4320527589833776e-18, -4.8999999999373206e-12, -9.800000000000003e-6, -3.870438350247696e-16]
[1.0, 1.779329265947601e-14, -5.929000004468873e-10, -0.00010780000000000091, -3.0178908529874883e-12]
[0.9999999999999998, -3.705258613763331e-12, -1.9407725978901355e-8, -0.0006167588102611186, -1.3036994235702973e-9]
[0.9999999999999896, 1.231734351558704e-11, -1.4461910374133314e-7, -0.001683607564645096, -3.158050706805358e-8]
[0.9999999999996371, -9.147857460683334e-11, -8.520677279956897e-7, -0.0040866278858198955, -4.845676031758449e-7]
[0.9999999999857808, -1.891233463103192e-9, -5.332756312011546e-6, -0.010223601306518774, -7.83009088217851e-6]
[0.9999999996124844, -5.726676209454051e-8, -2.7839381844867827e-5, -0.023359192721600835, -9.45339864680247e-5]
[0.9999999975906804, -3.4755243956250846e-7, -6.941641839844382e-5, -0.0368857940178753, -0.00037351357964688953]
[0.9999999912816985, -1.2748040564497867e-6, -0.00013204773764628023, -0.05087372282090536, -0.0009815961712321248]
⋮
[0.9901651613957098, -0.23172735115694545, -0.13990337266694583, -1.6405123591693411, -4.115166337579525]
[0.9855712161386252, -0.3083616993192879, -0.16926127369263747, -1.795912092376441, -4.980213518414672]
[0.9797518017566736, -0.3967501726849769, -0.20021590560182462, -1.9415667671742471, -5.894755669328316]
[0.9728831662637859, -0.4927520327194497, -0.23129710247383958, -2.072072459058729, -6.816526114467944]
[0.9653111301491414, -0.5912124320760092, -0.26110234215619854, -2.184252881590318, -7.704686847211488]
[0.9582752681971104, -0.6774847635078917, -0.28584702809597784, -2.268603444057187, -8.445920248663054]
[0.9518735971071052, -0.7524892742760094, -0.3064909010545698, -2.3332438885829054, -9.067471577304934]
[0.9461805448181262, -0.8168252614310116, -0.32363929847919043, -2.383121166331972, -9.58619264314162]
[0.9445584480218617, -0.834794040825908, -0.3283433240601558, -2.3962124390909865, -9.728925871084446]
Nope! The EK1
is able to solve the index-3 DAE directly. Pretty cool!
plot(sol)
Is index-reduction still worth it?
The point of the "Automatic Index Reduction of DAEs" tutorial is to demonstrate ModelingToolkit's utility for automatic index reduction, which enables the classic implicit Runge-Kutta solvers such as Rodas5
to solve this DAE. Let's see if that still helps in this context here.
First, modelingtoolkitize
the problem:
traced_sys = modelingtoolkitize(pendulum_prob)
\[ \begin{align} \frac{\mathrm{d} \mathtt{x_1}\left( t \right)}{\mathrm{d}t} &= \mathtt{x_2}\left( t \right) \\ \frac{\mathrm{d} \mathtt{x_2}\left( t \right)}{\mathrm{d}t} &= \mathtt{x_1}\left( t \right) \mathtt{x_5}\left( t \right) \\ \frac{\mathrm{d} \mathtt{x_3}\left( t \right)}{\mathrm{d}t} &= \mathtt{x_4}\left( t \right) \\ \frac{\mathrm{d} \mathtt{x_4}\left( t \right)}{\mathrm{d}t} &= - \mathtt{\alpha_1} + \mathtt{x_3}\left( t \right) \mathtt{x_5}\left( t \right) \\ 0 &= \left( \mathtt{x_1}\left( t \right) \right)^{2} + \left( \mathtt{x_3}\left( t \right) \right)^{2} - \mathtt{\alpha_2}^{2} \end{align} \]
(how cool is this latex output ?!?)
Next, lower the DAE index and simplify it with ModelingToolkit's dae_index_lowering
and structural_simplify
:
simplified_sys = structural_simplify(dae_index_lowering(traced_sys))
\[ \begin{align} \frac{\mathrm{d} \mathtt{x_1}\left( t \right)}{\mathrm{d}t} &= \mathtt{x_2}\left( t \right) \\ \frac{\mathrm{d} \mathtt{x_2}\left( t \right)}{\mathrm{d}t} &= \mathtt{x_1}\left( t \right) \mathtt{x_5}\left( t \right) \\ \frac{\mathrm{d} \mathtt{x_3}\left( t \right)}{\mathrm{d}t} &= \mathtt{x_4}\left( t \right) \\ \frac{\mathrm{d} \mathtt{x_4}\left( t \right)}{\mathrm{d}t} &= - \mathtt{\alpha_1} + \mathtt{x_3}\left( t \right) \mathtt{x_5}\left( t \right) \\ 0 &= 2 \left( \mathtt{x_4}\left( t \right) \right)^{2} + 2 \left( \mathtt{x_2}\left( t \right) \right)^{2} + 2 \left( \mathtt{x_1}\left( t \right) \right)^{2} \mathtt{x_5}\left( t \right) + 2 \mathtt{x_3}\left( t \right) \left( - \mathtt{\alpha_1} + \mathtt{x_3}\left( t \right) \mathtt{x_5}\left( t \right) \right) \end{align} \]
Let's build two different ODE problems, and check how well we can solve each:
prob_index3 = ODEProblem(complete(traced_sys), Pair[], tspan)
prob_index1 = ODEProblem(simplified_sys, Pair[], tspan)
sol3 = solve(prob_index3, EK1())
sol1 = solve(prob_index1, EK1())
truesol = solve(prob_index1, Rodas4(), abstol=1e-10, reltol=1e-10)
sol1_final_error = norm(sol1.u[end] - truesol.u[end])
sol1_f_evals = sol1.stats.nf
sol3_final_error = norm(sol3.u[end] - truesol.u[end])
sol3_f_evals = sol3.stats.nf
@info "Results" sol1_final_error sol1_f_evals sol3_final_error sol3_f_evals
┌ Info: Results
│ sol1_final_error = 0.010916847865436992
│ sol1_f_evals = 1085
│ sol3_final_error = 0.06751295080513564
└ sol3_f_evals = 1379
The error for the index-1 DAE solve is much lower. Thus it seems that, even if the index-3 DAE could also be solved directly, index lowering might still be beneficial when solving DAEs with the EK1
!
References
- [1]
- N. Bosch, F. Tronarp and P. Hennig. Pick-and-Mix Information Operators for Probabilistic ODE Solvers. In: Proceedings of The 25th International Conference on Artificial Intelligence and Statistics, Vol. 151 of Proceedings of Machine Learning Research, edited by G. Camps-Valls, F. J. Ruiz and I. Valera (PMLR, 28–30 Mar 2022); pp. 10015–10027.