Skip to content

Commit 054adc3

Browse files
authored
Merge pull request #225 from JuliaDynamics/hw/init_tutorial
short initialization tutorial
2 parents ce5bab6 + 660bc59 commit 054adc3

File tree

9 files changed

+453
-12
lines changed

9 files changed

+453
-12
lines changed

docs/examples/init_tutorial.jl

Lines changed: 374 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,374 @@
1+
#=
2+
# [Tutorial on Stepwise Initialization of a Complex Model](@id init-tutorial)
3+
4+
This example demonstrates how to initialize a complex network model with both static
5+
and dynamic components. We'll create a simple gas network model with three nodes
6+
and pipes connecting them, and show how to:
7+
8+
1. Create static models for initialization
9+
2. Find a steady-state solution
10+
3. Create corresponding dynamic models
11+
4. Initialize the dynamic models with the steady-state solution
12+
5. Simulate the system with dynamic behavior
13+
14+
This script can be downloaded as a normal Julia script [here](@__NAME__.jl). #md
15+
16+
First, let's import the necessary packages:
17+
=#
18+
19+
using NetworkDynamics
20+
using ModelingToolkit
21+
using ModelingToolkit: t_nounits as t, D_nounits as D
22+
using OrdinaryDiffEqTsit5
23+
using CairoMakie
24+
nothing #hide
25+
26+
#=
27+
## Node Models
28+
29+
We'll start by defining our node models using ModelingToolkit.
30+
First, let's create a template for common states and equations in all gas nodes:
31+
=#
32+
@mtkmodel GasNode begin
33+
@variables begin
34+
p(t), [description="Pressure"] # node output
35+
q̃_nw(t), [description="aggregated flow from pipes into node"] # node input
36+
q̃_inj(t), [description="flow injected into the network"]
37+
end
38+
@equations begin
39+
q̃_inj ~ -q̃_nw
40+
end
41+
end
42+
nothing #hide
43+
44+
#=
45+
Now we'll define three specific node types:
46+
47+
**A) A constant pressure node that forces pressure to maintain a specific value**
48+
=#
49+
@mtkmodel ConstantPressureNode begin
50+
@extend GasNode()
51+
@parameters begin
52+
p_set, [description="Constant pressure setpoint"]
53+
end
54+
@equations begin
55+
p ~ p_set
56+
end
57+
end
58+
nothing #hide
59+
60+
#=
61+
**B) A static prosumer node which forces a certain flow (pressure is fully implicit)**
62+
=#
63+
@mtkmodel StaticProsumerNode begin
64+
@extend GasNode()
65+
@parameters begin
66+
q̃_prosumer, [description="flow injected by prosumer"]
67+
end
68+
@equations begin
69+
-q̃_nw ~ q̃_prosumer
70+
end
71+
end
72+
nothing #hide
73+
74+
#=
75+
**C) A dynamic prosumer node with compliance, which adds dynamics to the pressure state**
76+
=#
77+
@mtkmodel DynamicProsumerNode begin
78+
@extend GasNode()
79+
@parameters begin
80+
q̃_prosumer, [description="flow injected by prosumer"]
81+
C=0.1, [description="Compliance"]
82+
end
83+
@equations begin
84+
C*D(p) ~ q̃_prosumer + q̃_nw
85+
end
86+
end
87+
nothing #hide
88+
89+
#=
90+
**D) A pressure control node that tries to maintain a set pressure by adjusting its injection**
91+
=#
92+
@mtkmodel PressureControlNode begin
93+
@extend GasNode()
94+
@parameters begin
95+
p_set, [description="Pressure setpoint", guess=1]
96+
K_p=1, [description="Proportional gain"]
97+
K_i=1, [description="Integral gain"]
98+
C=0.1, [description="Compliance"]
99+
end
100+
@variables begin
101+
Δp(t), [description="Pressure error"]
102+
ξ(t), [description="Integral state", guess=0]
103+
q̃_prosumer(t), [description="flow injected by producer"]
104+
end
105+
@equations begin
106+
Δp ~ p_set - p
107+
D(ξ) ~ Δp
108+
q̃_prosumer ~ K_p*Δp + K_i*ξ
109+
C*D(p) ~ q̃_prosumer + q̃_nw
110+
end
111+
end
112+
nothing #hide
113+
114+
#=
115+
## Edge Models
116+
117+
Now we'll define our edge models, starting with a template for the pipe:
118+
=#
119+
@mtkmodel GasPipe begin
120+
@variables begin
121+
(t), [description="flow through pipe"] #output
122+
p_src(t), [description="pressure at start of pipe"] #input
123+
p_dst(t), [description="pressure at end of pipe"] #input
124+
end
125+
end
126+
nothing #hide
127+
128+
#=
129+
Next, we define a dynamic pipe with inertia (a simple delayed model):
130+
=#
131+
@mtkmodel DynamicPipe begin
132+
@extend GasPipe()
133+
@parameters begin
134+
R=0.1, [description="Resistance"]
135+
M=0.1, [description="Inertia"]
136+
end
137+
@equations begin
138+
M*D(q̃) ~ (p_src - p_dst)/R -# some simple delayed model
139+
end
140+
end
141+
nothing #hide
142+
143+
#=
144+
And finally a quasistatic pipe model for initialization purposes. This equals the
145+
dynamic model in steady state, making it ideal for finding initial conditions:
146+
=#
147+
@mtkmodel QuasistaticPipe begin
148+
@extend GasPipe()
149+
@parameters begin
150+
R=0.1, [description="Resistance"]
151+
end
152+
@equations begin
153+
~ (p_src - p_dst)/R
154+
end
155+
end
156+
nothing #hide
157+
158+
#=
159+
## Defining a Static Model for Initialization
160+
161+
Our first step is to define a static model that we'll use to find the steady-state solution.
162+
This is a crucial step for initializing complex dynamic models.
163+
164+
Step 1: Define all the components of our static model
165+
First, node 1 is our producer which will later be a controlled producer. For initialization, we use a static model:
166+
=#
167+
@named v1_mod_static = ConstantPressureNode(p_set=1)
168+
v1_static = VertexModel(v1_mod_static, [:q̃_nw], [:p], vidx=1)
169+
170+
## Nodes 2 and 3 are consumers. For them, we'll use static prosumer models:
171+
@named v2_mod_static = StaticProsumerNode(q̃_prosumer=-0.6) # consumer
172+
v2_static = VertexModel(v2_mod_static, [:q̃_nw], [:p], vidx=2)
173+
174+
@named v3_mod_static = StaticProsumerNode(q̃_prosumer=-0.4) # consumer
175+
v3_static = VertexModel(v3_mod_static, [:q̃_nw], [:p], vidx=3)
176+
nothing #hide
177+
178+
#=
179+
Now we define the static pipe models connecting our nodes:
180+
=#
181+
@named p_mod_static = QuasistaticPipe()
182+
p12_static = EdgeModel(p_mod_static, [:p_src], [:p_dst], AntiSymmetric([:q̃]), src=1, dst=2)
183+
p13_static = EdgeModel(p_mod_static, [:p_src], [:p_dst], AntiSymmetric([:q̃]), src=1, dst=3)
184+
p23_static = EdgeModel(p_mod_static, [:p_src], [:p_dst], AntiSymmetric([:q̃]), src=2, dst=3)
185+
nothing #hide
186+
#=
187+
Assemble all components into a static network:
188+
=#
189+
nw_static = Network([v1_static, v2_static, v3_static], [p12_static, p13_static, p23_static])
190+
191+
#=
192+
Create an initial guess for the steady state and modify it with reasonable values:
193+
=#
194+
u_static_guess = NWState(nw_static)
195+
u_static_guess.v[2, :p] = 1.0
196+
u_static_guess.v[3, :p] = 1.0
197+
nothing #hide
198+
199+
#=
200+
Find the steady-state solution using our initial guess:
201+
=#
202+
u_static = find_fixpoint(nw_static, u_static_guess)
203+
204+
#=
205+
## Defining a Dynamic Model
206+
207+
Now we'll define our dynamic model using more complex components:
208+
=#
209+
@named v1_mod_dyn = PressureControlNode()
210+
v1_dyn = VertexModel(v1_mod_dyn, [:q̃_nw], [:p], vidx=1)
211+
212+
@named v2_mod_dyn = DynamicProsumerNode(q̃_prosumer=-0.6)
213+
v2_dyn = VertexModel(v2_mod_dyn, [:q̃_nw], [:p], vidx=2)
214+
215+
@named v3_mod_dyn = DynamicProsumerNode(q̃_prosumer=-0.4)
216+
v3_dyn = VertexModel(v3_mod_dyn, [:q̃_nw], [:p], vidx=3)
217+
nothing #hide
218+
219+
#=
220+
Create dynamic pipe models with inertia:
221+
=#
222+
@named p_mod_dyn = DynamicPipe()
223+
p12_dyn = EdgeModel(p_mod_dyn, [:p_src], [:p_dst], AntiSymmetric([:q̃]), src=1, dst=2)
224+
p13_dyn = EdgeModel(p_mod_dyn, [:p_src], [:p_dst], AntiSymmetric([:q̃]), src=1, dst=3)
225+
p23_dyn = EdgeModel(p_mod_dyn, [:p_src], [:p_dst], AntiSymmetric([:q̃]), src=2, dst=3)
226+
nothing #hide
227+
228+
#=
229+
Assemble the dynamic network:
230+
=#
231+
nw_dyn = Network([v1_dyn, v2_dyn, v3_dyn], [p12_dyn, p13_dyn, p23_dyn])
232+
233+
#=
234+
## Initializing the Dynamic Model with the Static Solution
235+
236+
Now comes the important part: we need to initialize the interface values (pressures and flows)
237+
of the dynamic model with the results from the static model.
238+
239+
We can do this manually:
240+
=#
241+
## Vertex 1: output
242+
set_default!(nw_dyn[VIndex(1)], :p, u_static.v[1, :p])
243+
## Vertex 1: input
244+
set_default!(nw_dyn[VIndex(1)], :q̃_nw, u_static.v[1, :q̃_nw])
245+
nothing #hide
246+
247+
#=
248+
But there is also a built-in method [`set_interface_defaults!`](@ref) which we can use
249+
automatically:
250+
=#
251+
set_interface_defaults!(nw_dyn, u_static; verbose=true)
252+
nothing #hide
253+
254+
#=
255+
With the interfaces all set, we can "initialize" the internal states of the dynamic models.
256+
257+
For example, let's inspect the state of our first vertex:
258+
=#
259+
nw_dyn[VIndex(1)]
260+
261+
#=
262+
We observe that both the initial state `ξ` as well as the pressure setpoint `p_set`
263+
are left "free". Using [`initialize_component!`](@ref), we can try to find values for the
264+
"free" states and parameters such that the interface constraints are fulfilled.
265+
=#
266+
initialize_component!(nw_dyn[VIndex(1)])
267+
#=
268+
We may also use [`dump_initial_state`](@ref) to get a more detailed view of the state:
269+
=#
270+
dump_initial_state(nw_dyn[VIndex(1)])
271+
nothing #hide
272+
273+
#=
274+
We can also initialize the other two vertices, however it is unnecessary
275+
since their state is already completely determined by the fixed input/output:
276+
=#
277+
initialize_component!(nw_dyn[VIndex(2)])
278+
initialize_component!(nw_dyn[VIndex(3)])
279+
nothing #hide
280+
281+
#=
282+
Similarly, we can initialize the dynamic pipe models. However, since their dynamic state
283+
equals the output, once again there is nothing to initialize.
284+
=#
285+
initialize_component!(nw_dyn[EIndex(1)])
286+
initialize_component!(nw_dyn[EIndex(2)])
287+
initialize_component!(nw_dyn[EIndex(3)])
288+
nothing #hide
289+
290+
#=
291+
Now, everything is initialized, which means every input, output, state and parameter
292+
either has a `default` metadata or an `init` metadata. When constructing the `NWState`
293+
for this network, it will be filled with all those values which should now correspond
294+
to a steady state of the system:
295+
=#
296+
u0_dyn = NWState(nw_dyn)
297+
298+
#=
299+
Let's verify that our initialization is correct by checking that the derivatives are close to zero:
300+
=#
301+
du = ones(dim(nw_dyn))
302+
nw_dyn(du, uflat(u0_dyn), pflat(u0_dyn), 0.0)
303+
extrema(du .- zeros(dim(nw_dyn))) # very close to zero, confirming we have a steady state!
304+
305+
#=
306+
## Simulating the Dynamic Model
307+
308+
Now we can solve the dynamic model and add a disturbance to see how the system responds:
309+
=#
310+
affect = ComponentAffect([], [:q̃_prosumer]) do u, p, ctx
311+
@info "Increase consumer demand at t=$(ctx.t)"
312+
p[:q̃_prosumer] -= 0.1
313+
end
314+
cb = PresetTimeComponentCallback([1.0], affect)
315+
set_callback!(nw_dyn[VIndex(2)], cb) # attach disturbance to second node
316+
nothing #hide
317+
318+
#=
319+
Create and solve the ODE problem with the callback:
320+
=#
321+
prob = ODEProblem(nw_dyn, copy(uflat(u0_dyn)), (0, 7), copy(pflat(u0_dyn));
322+
callback=get_callbacks(nw_dyn))
323+
sol = solve(prob, Tsit5())
324+
nothing #hide
325+
326+
#=
327+
## Visualizing the Results
328+
329+
Finally, let's visualize the results of our simulation.
330+
The plots show how our gas network responds to the increased consumer demand at t=1:
331+
332+
1. **Pressure at nodes**: We see a pressure drop at all nodes after the disturbance before the pressure is stabilized by the controller.
333+
334+
2. **Injection by producer**: Node 1 increases its injection to compensate for the higher demand.
335+
336+
3. **Draw by consumers**: The solid lines show the actual flows at nodes 2 and 3, while the dashed lines show the set consumer demands. At t=1, we see the step change in consumer demand at node 2.
337+
338+
4. **Flows through pipes**: Shows how the flows in all pipes adjust to the new demand pattern.
339+
=#
340+
341+
let
342+
fig = Figure(size=(1000,1000))
343+
ax = Axis(fig[1, 1]; title="Pressure at nodes")
344+
for i in 1:3
345+
lines!(ax, sol; idxs=VIndex(i, :p), label="Node $i", color=Cycled(i))
346+
end
347+
348+
ax = Axis(fig[2, 1]; title="Injection by producer")
349+
lines!(ax, sol; idxs=VIndex(1, :q̃_inj), label="Node 1", color=Cycled(1))
350+
351+
ax = Axis(fig[3, 1]; title="Draw by consumers")
352+
for i in 2:3
353+
lines!(ax, sol; idxs=@obsex(-1*VIndex(i, :q̃_inj)), label="Node $i", color=Cycled(i))
354+
lines!(ax, sol; idxs=@obsex(-1*VIndex(i, :q̃_prosumer)), label="Node $i", linestyle=:dash, color=Cycled(i))
355+
end
356+
357+
ax = Axis(fig[4, 1]; title="Flows through pipes")
358+
for i in 1:3
359+
lines!(ax, sol; idxs=@obsex(abs(EIndex(i, :q̃))), label="Pipe $i", color=Cycled(i))
360+
end
361+
362+
fig
363+
end
364+
365+
#=
366+
## Interactive Visualization
367+
368+
You can also visualize the results interactively using NetworkDynamicsInspector:
369+
370+
```julia
371+
using NetworkDynamicsInspector
372+
inspect(sol)
373+
```
374+
=#

docs/make.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ kwargs = (;
6464
"Cascading Failure" => "generated/cascading_failure.md",
6565
"Stress on Truss" => "generated/stress_on_truss.md",
6666
"Gas Network" => "generated/gas_network.md",
67+
"Initialization" => "generated/init_tutorial.md",
6768
]
6869
],
6970
draft=false,

docs/src/API.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ init_residual
164164
get_initial_state
165165
dump_initial_state
166166
set_defaults!
167+
set_interface_defaults!
167168
```
168169

169170
## Execution Types

0 commit comments

Comments
 (0)