Skip to content

Commit ccacb8d

Browse files
Add a tutorial for initialization handling
1 parent 411efc7 commit ccacb8d

File tree

1 file changed

+364
-0
lines changed

1 file changed

+364
-0
lines changed

docs/src/tutorials/initialization.md

Lines changed: 364 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,364 @@
1+
# Initialization of ODESystems
2+
3+
While for simple numerical ODEs choosing an initial condition can be an easy
4+
affair, with ModelingToolkit's more general differential-algebraic equation
5+
(DAE) system there is more care needed due to the flexability of the solver
6+
state. In this tutorial we will walk through the functionality involved in
7+
initialization of ODESystem and the diagonstics to better understand and
8+
debug the initialization problem.
9+
10+
## Primer on Initialization of Differential-Algebraic Equations
11+
12+
Before getting started, let's do a brief walkthrough of the mathematical
13+
principles of initialization of DAE systems. Take a DAE written in semi-explicit
14+
form:
15+
16+
```math
17+
x' = f(x,y,t)\\
18+
0 = g(x,y,t)
19+
```
20+
21+
where ``x`` are the differential variables and ``y`` are the algebraic variables.
22+
An initial condition ``u0 = [x(t_0) y(t_0)]`` is said to be consistent if
23+
``g(x(t_0),y(t_0),t_0) = 0``.
24+
25+
For ODEs, this is trivially satisfied. However, for more complicated systems it may
26+
not be easy to know how to choose the variables such that all of the conditions
27+
are satisfied. This is even more complicated when taking into account ModelingToolkit's
28+
simplification engine, given that variables can be eliminated and equations can be
29+
changed. If this happens, how do you know how to initialize the system?
30+
31+
## Initialization By Example: The Cartesian Pendulum
32+
33+
To illustrate how to perform the initialization, let's take a look at the Cartesian
34+
pendulum:
35+
36+
```@example init
37+
using ModelingToolkit, OrdinaryDiffEq, Test
38+
using ModelingToolkit: t_nounits as t, D_nounits as D
39+
40+
@parameters g
41+
@variables x(t) y(t) [state_priority = 10] λ(t)
42+
eqs = [D(D(x)) ~ λ * x
43+
D(D(y)) ~ λ * y - g
44+
x^2 + y^2 ~ 1]
45+
@mtkbuild pend = ODESystem(eqs, t)
46+
```
47+
48+
While we defined the system using second derivatives and a length constraint,
49+
the structural simplification system improved the numerics of the system to
50+
be solvable using the dummy derivative technique, which results in 3 algebraic
51+
equations and 2 differential equations. In this case, the differential equations
52+
with respect to `y` and `D(y)`, though it could have just as easily have been
53+
`x` and `D(x)`. How do you initialize such a system if you don't know in advance
54+
what variables may defined the equation's state?
55+
56+
To see how the system works, let's start the pendulum in the far right position,
57+
i.e. `x(0) = 1` and `y(0) = 0`. We can do this by:
58+
59+
```@example init
60+
prob = ODEProblem(pend, [x => 1, y => 0], (0.0, 1.5), [g => 1], guesses = [λ => 1])
61+
```
62+
63+
This solves via:
64+
65+
```@example init
66+
sol = solve(prob, Rodas5P())
67+
plot(sol, idxs = (x,y))
68+
```
69+
70+
and we can check it satisfies our conditions via:
71+
72+
```@example init
73+
conditions = getfield.(equations(pend)[3:end],:rhs)
74+
```
75+
76+
```@example init
77+
[sol[conditions][1]; sol[x][1] - 1; sol[y][1]]
78+
```
79+
80+
Notice that we set `[x => 1, y => 0]` as our initial conditions and `[λ => 1]` as our guess.
81+
The difference is that the initial conditions are **required to be satisfied**, while the
82+
guesses are simply a guess for what the initial value might be. Every variable must have
83+
either an initial condition or a guess, and thus since we did not know what `λ` would be
84+
we set it to 1 and let the initialization scheme find the correct value for λ. Indeed,
85+
the value for `λ` at the initial time is not 1:
86+
87+
```@example init
88+
sol[λ][1]
89+
```
90+
91+
We can similarly choose `λ = 0` and solve for `y` to start the system:
92+
93+
```@example init
94+
prob = ODEProblem(pend, [x => 1, λ => 0], (0.0, 1.5), [g => 1], guesses = [y => 1])
95+
sol = solve(prob, Rodas5P())
96+
plot(sol, idxs = (x,y))
97+
```
98+
99+
or choose to satisfy derivative conditions:
100+
101+
```@example init
102+
prob = ODEProblem(pend, [x => 1, D(y) => 0], (0.0, 1.5), [g => 1], guesses = [λ => 0, y => 1])
103+
sol = solve(prob, Rodas5P())
104+
plot(sol, idxs = (x,y))
105+
```
106+
107+
Notice that since a derivative condition is given, we are required to give a
108+
guess for `y`.
109+
110+
We can also directly give equations to be satisfied at the initial point by using
111+
the `initialization_eqs` keyword argument, for example:
112+
113+
```@example init
114+
prob = ODEProblem(pend, [x => 1], (0.0, 1.5), [g => 1], guesses = [λ => 0, y => 1],
115+
initialization_eqs = [y ~ 1])
116+
sol = solve(prob, Rodas5P())
117+
plot(sol, idxs = (x,y))
118+
```
119+
120+
## Determinability: Underdetermined and Overdetermined Systems
121+
122+
For this system we have 3 conditions to satisfy:
123+
124+
```@example init
125+
conditions = getfield.(equations(pend)[3:end],:rhs)
126+
```
127+
128+
when we initialize with
129+
130+
```@example init
131+
prob = ODEProblem(pend, [x => 1, y => 0], (0.0, 1.5), [g => 1], guesses = [y => 0, λ => 1])
132+
```
133+
134+
we have two extra conditions to satisfy, `x ~ 1` and `y ~ 0` at the initial point. That gives
135+
5 equations for 5 variables and thus the system is well-formed. What happens if that's not the
136+
case?
137+
138+
```@example init
139+
prob = ODEProblem(pend, [x => 1], (0.0, 1.5), [g => 1], guesses = [y => 0, λ => 1])
140+
```
141+
142+
Here we have 4 equations for 5 unknowns (note: the warning is post-simplification of the
143+
nonlinear system, which solves the trivial `x ~ 1` equation analytical and thus says
144+
3 equations for 4 unknowns). This warning thus lets you know the system is underdetermined
145+
and thus the solution is not necessarily unique. It can still be solved:
146+
147+
```@example init
148+
sol = solve(prob, Rodas5P())
149+
plot(sol, idxs = (x,y))
150+
```
151+
152+
and the found initial condition satisfies all constraints which were given. In the opposite
153+
direction, we may have an overdetermined system:
154+
155+
```@example init
156+
prob = ODEProblem(pend, [x => 1, y => 0.0, D(y) => 0], (0.0, 1.5), [g => 1], guesses = [λ => 1])
157+
```
158+
159+
Can that be solved?
160+
161+
```@example init
162+
sol = solve(prob, Rodas5P())
163+
plot(sol, idxs = (x,y))
164+
```
165+
166+
Indeed since we saw `D(y) = 0` at the initial point above, it turns out that this solution
167+
is solvable with the chosen initial conditions. However, for overdetermined systems we often
168+
aren't that lucky. If the set of initial conditions cannot be satisfied, then you will get
169+
a `SciMLBase.ReturnCode.InitialFailure`:
170+
171+
```@example init
172+
prob = ODEProblem(pend, [x => 1, y => 0.0, D(y) => 2.0, λ => 1], (0.0, 1.5), [g => 1], guesses = [λ => 1])
173+
sol = solve(prob, Rodas5P())
174+
```
175+
176+
What this means is that the initial condition finder failed to find an initial condition.
177+
While this can be sometimes due to numerical error (which is then helped by picking guesses closer
178+
to the correct value), most circumstances of this come from ill-formed models. Especially
179+
**if your system is overdetermined and you receive an InitialFailure, the initial conditions
180+
may not be analytically satisfiable!**. In our case here, if you sit down with a pen and paper
181+
long enough you will see that `λ = 0` is required for this equation, but since we chose
182+
`λ = 1` we end up with a set of equations that are impossible to satisfy.
183+
184+
## Diving Deeper: Constructing the Initialization System
185+
186+
To get a better sense of the initialization system and to help debug it, you can construct
187+
the initialization system directly. The initialization system is a NonlinearSystem
188+
which requires the system-level information and the additional nonlinear equations being
189+
tagged to the system.
190+
191+
```@example init
192+
isys = generate_initializesystem(pend, u0map = [x => 1.0, y => 0.0], guesses = [λ => 1])
193+
```
194+
195+
We can inspect what its equations and unknown values are:
196+
197+
```@example init
198+
equations(isys)
199+
```
200+
201+
```@example init
202+
unknowns(isys)
203+
```
204+
205+
Notice that all initial conditions are treated as initial equations. Additionally, for systems
206+
with observables, those observables are too treated as initial equations. We can see the
207+
resulting simplified system via the command:
208+
209+
```@example init
210+
isys = structural_simplify(isys; fully_determined=false)
211+
```
212+
213+
Note `fully_determined=false` allows for the simplification to occur when the number of equations
214+
does not match the number of unknowns, which we can use to investigate our overdetermined system:
215+
216+
```@example init
217+
isys = ModelingToolkit.generate_initializesystem(pend, u0map = [x => 1, y => 0.0, D(y) => 2.0, λ => 1], guesses = [λ => 1])
218+
```
219+
220+
```@example init
221+
isys = structural_simplify(isys; fully_determined=false)
222+
```
223+
224+
```@example init
225+
equations(isys)
226+
```
227+
228+
```@example init
229+
unknowns(isys)
230+
```
231+
232+
```@example init
233+
observed(isys)
234+
```
235+
236+
After simplification we see that we have 5 equatinos to solve with 3 variables, and the
237+
system that is given is not solvable.
238+
239+
## Numerical Isolation: InitializationProblem
240+
241+
To inspect the numerics of the initialization problem, we can use the `InitializationProblem`
242+
constructor which acts just like an `ODEProblem` or `NonlinearProblem` constructor, but
243+
creates the special initialization system for a given `sys`. This is done as follows:
244+
245+
```@example init
246+
iprob = ModelingToolkit.InitializationProblem(pend, 0.0,
247+
[x => 1, y => 0.0, D(y) => 2.0, λ => 1], [g => 1], guesses = [λ => 1])
248+
```
249+
250+
We can see that because the system is overdetermined we recieve a NonlinearLeastSquaresProblem,
251+
solvable by [NonlinearSolve.jl](https://docs.sciml.ai/NonlinearSolve/stable/). Using NonlinearSolve
252+
we can recreate the initialization solve directly:
253+
254+
```@example init
255+
using NonlinearSolve
256+
sol = solve(iprob)
257+
```
258+
259+
!!! note
260+
For more information on solving NonlinearProblems and NonlinearLeastSquaresProblems,
261+
check out the [NonlinearSolve.jl tutorials!](https://docs.sciml.ai/NonlinearSolve/stable/tutorials/getting_started/).
262+
263+
We can see that the default solver stalls
264+
265+
```@example init
266+
sol.stats
267+
```
268+
269+
after doing many iterations, showing that it tried to compute but could not find a valid solution.
270+
Trying other solvers:
271+
272+
```@example init
273+
sol = solve(iprob, GaussNewton())
274+
```
275+
276+
gives the same issue, indicating that the chosen initialization system is unsatisfiable. We can
277+
check the residuals:
278+
279+
```@example init
280+
sol.resid
281+
```
282+
283+
to see the problem is not equation 2 but other equations in the system. Meanwhile, changing
284+
some of the conditions:
285+
286+
```@example init
287+
iprob = ModelingToolkit.InitializationProblem(pend, 0.0,
288+
[x => 1, y => 0.0, D(y) => 0.0, λ => 0], [g => 1], guesses = [λ => 1])
289+
```
290+
291+
gives a NonlinearLeastSquaresProblem which can be solved:
292+
293+
```@example init
294+
sol = solve(iprob)
295+
```
296+
297+
```@example init
298+
sol.resid
299+
```
300+
301+
In comparison, if we have a well-conditioned system:
302+
303+
304+
```@example init
305+
iprob = ModelingToolkit.InitializationProblem(pend, 0.0,
306+
[x => 1, y => 0.0], [g => 1], guesses = [λ => 1])
307+
```
308+
309+
notice that we instead obtained a NonlinearSystem. In this case we have to use
310+
different solvers which can take advantage of the fact that the Jacobian is square.
311+
312+
```@example init
313+
sol = solve(iprob)
314+
```
315+
316+
```@example init
317+
sol = solve(iprob, TrustRegion())
318+
```
319+
320+
## More Features of the Initialization System: Steady-State and Observable Initialization
321+
322+
Let's take a Lotka-Volterra system:
323+
324+
```@example init
325+
@variables x(t) y(t) z(t)
326+
@parameters α=1.5 β=1.0 γ=3.0 δ=1.0
327+
328+
eqs = [D(x) ~ α * x - β * x * y
329+
D(y) ~ -γ * y + δ * x * y
330+
z ~ x + y]
331+
332+
@named sys = ODESystem(eqs, t)
333+
simpsys = structural_simplify(sys)
334+
tspan = (0.0, 10.0)
335+
```
336+
337+
Using the derivative initializations, we can set the ODE to start at the steady state
338+
by initializing the derivatives to zero:
339+
340+
```@example init
341+
prob = ODEProblem(simpsys, [D(x) => 0.0, D(y) => 0.0], tspan, guesses = [x => 1, y => 1])
342+
sol = solve(prob, Tsit5(), abstol= 1e-16)
343+
```
344+
345+
Notice that this is a "numerical zero", not an exact zero, and thus the solution will leave the
346+
steady state in this instance because it's an unstable steady state.
347+
348+
Additionally, notice that in this setup we have an observable `z ~ x + y`. If we instead know the
349+
initial condition for the observable we can use that directly:
350+
351+
```@example init
352+
prob = ODEProblem(simpsys, [D(x) => 0.0, z => 2.0], tspan, guesses = [x => 1, y => 1])
353+
sol = solve(prob, Tsit5())
354+
```
355+
356+
We can check that indeed the solution does satisfy that D(x) = 0 at the start:
357+
358+
```@example init
359+
sol[α * x - β * x * y]
360+
```
361+
362+
```@example init
363+
plot(sol)
364+
```

0 commit comments

Comments
 (0)