1
1
#=
2
- # Dynamic Flow in simple Gas Network
2
+ # Dynamic Flow in Simple Gas Network
3
3
4
- This Example is based on the paper
4
+ This example is based on the paper
5
5
6
6
> Albertus J. Malan, Lukas Rausche, Felix Strehle, Sören Hohmann,
7
7
> Port-Hamiltonian Modelling for Analysis and Control of Gas Networks,
8
8
> IFAC-PapersOnLine, Volume 56, Issue 2, 2023, https://doi.org/10.1016/j.ifacol.2023.10.193.
9
9
10
- and tries replicate a simple simulation of flow in a 3-node gas network.
10
+ and tries to replicate a simple simulation of flow in a 3-node gas network.
11
11
12
12
This example can be dowloaded as a normal Julia script [here](@__NAME__.jl). #md
13
13
@@ -19,21 +19,21 @@ using DynamicQuantities
19
19
using ModelingToolkit: D as Dt, t as t
20
20
using Test
21
21
using StaticArrays
22
- using LinearInterpolations
22
+ using DataInterpolations
23
23
using OrdinaryDiffEqTsit5
24
24
using CairoMakie
25
25
CairoMakie. activate! (type= " svg" ) # hide
26
26
27
27
#=
28
28
## Node Models
29
29
30
- In this example, we use the equation based modeling using `ModelingToolkit.jl`.
31
- To verify the equations on a basic level we also provide units to eveything to
30
+ In this example, we use equation- based modeling using `ModelingToolkit.jl`.
31
+ To verify the equations on a basic level, we also provide units to everything to
32
32
perform dimensionality checks.
33
33
34
34
There are 2 node models used in the paper. The first node type has a constant
35
35
pressure.
36
- Additionally, we ad some "internal" state `q̃_inj` which we want to plot later (see also [Observables](@ref)).
36
+ Additionally, we add some "internal" state `q̃_inj` which we want to plot later (see also [Observables](@ref)).
37
37
=#
38
38
@mtkmodel ConstantPressureNode begin
39
39
@parameters begin
@@ -55,12 +55,15 @@ nothing #hide
55
55
The second node model is a variable pressure node. It has one output state (the pressure) and one input state,
56
56
the aggregated flows from the connected pipes.
57
57
As an internal state we have the injected flow from our source/load.
58
- The source/load behaviour itself is provided via a time dependent function.
58
+ The source/load behavior itself is provided via a time- dependent function.
59
59
=#
60
60
@mtkmodel VariablePressureNode begin
61
61
@structural_parameters begin
62
62
load_profile # time dependent load profile
63
63
end
64
+ @constants begin
65
+ load_unit = 1 , [description= " unit of the load profile" , unit= u " m^3/s" ]
66
+ end
64
67
@parameters begin
65
68
C, [description= " Lumped capacitance of connected pipes" , unit= u " m^4 * s^2 / kg" ]
66
69
end
@@ -70,7 +73,7 @@ The source/load behaviour itself is provided via a time dependent function.
70
73
q̃_nw (t), [description= " aggregated flow from pipes into node" , unit= u " m^3/s" , input= true ]
71
74
end
72
75
@equations begin
73
- q̃_inj ~ load_profile (t)
76
+ q̃_inj ~ load_profile (t) * load_unit
74
77
C * Dt (p) ~ q̃_inj + q̃_nw # (30)
75
78
end
76
79
end
@@ -79,10 +82,10 @@ nothing #hide
79
82
#=
80
83
## Pipe Model
81
84
82
- The pipe is modeld as a first order ODE for the volumetric flow at the `dst` end. It has two inputs:
83
- the pressure at the source and and the pressure at the destination end.
85
+ The pipe is modeled as a first- order ODE for the volumetric flow at the `dst` end. It has two inputs:
86
+ the pressure at the source and the pressure at the destination end.
84
87
Later on, we'll specify the model to be antisymmetric, thus the flow is calculated explicitly for the
85
- destination end, but the source end will just recive just that times (-1).
88
+ destination end, but the source end will just receive that times (-1).
86
89
=#
87
90
@mtkmodel Pipe begin
88
91
@parameters begin
@@ -157,7 +160,6 @@ T = 278u"K" # simulation temperature
157
160
r = 0.012 u " mm" # pipe roughness
158
161
D = 0.6 u " m" # pipe diameter
159
162
160
- # # TODO : here is switched the lenths l12 and l13. The results are better. Is this a mistake in the paper?
161
163
L₁₂ = 90 u " km"
162
164
L₁₃ = 80 u " km"
163
165
L₂₃ = 100 u " km"
@@ -175,7 +177,7 @@ sinθ₂₃ = 0.0
175
177
nothing # hide
176
178
177
179
#=
178
- Lastly, we need to calculate the compressibility factor, the speed of sound and
180
+ Lastly, we need to calculate the compressibility factor, the speed of sound, and
179
181
the density at standard conditions:
180
182
=#
181
183
Z̃ = 1 - 3.52 * p̃/ pc * exp (- 2.26 * (T̃/ Tc)) + 0.274 * (p̃/ pc)^ 2 * exp (- 1.878 * (T̃/ Tc)) # (5)
@@ -188,56 +190,54 @@ nothing # hide
188
190
The equivalent "pressure capacity" at the nodes is calculated as a sum of the connected
189
191
pipe parameters according to (28).
190
192
191
- Here use defintions based on the speed and "standard" conditions.
193
+ Here we use definitions based on the speed and "standard" conditions.
192
194
=#
193
195
C₂ = L₁₂* A/ (2 * ρ̃* c̃^ 2 ) + L₂₃* A/ (2 * ρ̃* c̃^ 2 ) # (28)
194
196
C₃ = L₁₃* A/ (2 * ρ̃* c̃^ 2 ) + L₂₃* A/ (2 * ρ̃* c̃^ 2 ) # (28)
195
197
nothing # hide
196
198
197
199
#=
198
- Alternatively, we could calculate `Z2` and `Z3` based on the actuel pressure and simulation temperature.
199
- Then we could calculated the speed of sound for the "correct" conditions at the node.
200
- It seems to have very little effect on the actual results so I kept it simple.
200
+ Alternatively, we could calculate `Z2` and `Z3` based on the actual pressure and simulation temperature.
201
+ Then we could calculate the speed of sound for the "correct" conditions at the node.
202
+ It seems to have very little effect on the actual results, so I kept it simple.
201
203
=#
202
204
nothing # hide
203
205
204
206
#=
205
207
## Load Profile
206
208
207
- The paper specifies the load profile at two nodes. We use the package [`LinearInterpolations .jl`](https://github.com/jw3126/LinearInterpolations .jl)
208
- to get a callable object which represents this picewise linear interpolation.
209
+ The paper specifies the load profile at two nodes. We use the package [`DataInterpolations .jl`](https://github.com/SciML/DataInterpolations .jl)
210
+ to get a callable object which represents this piecewise linear interpolation.
209
211
210
- However, this function is not Symbolics.jl compatible, so we need to stop Symbolics.jl/ModelingToolkit.jl
211
- from tracing it. To do so, we use `@register_symbolic` to declare it as a symbolic function which is treated
212
- as a blackbox.
212
+ Currently, the linear interpolation does not support any units yet. To satisfy the static unit check,
213
+ we multipy the interpolation output by a constant 1 of that unit.
213
214
214
- Additionally, we need to tell ModelingToolkit about the units of this object. This is just used
215
- for the static unit check during construction of the model. Later one, when we generate the
216
- Julia code from the symbolic reepresentation all units will be stripped.
215
+ Note however, that the unit check is only performed at the construction of the
216
+ model. Later on, when the nummeric code will be generated from the symbolic
217
+ representation, all units will be stripped.
217
218
218
219
!!! note "Discontinuities in RHS"
219
- The picewise linear interpolated function creates discontinuities in the RHS of the system.
220
- However since we know the times exactly, we can handle this by simply giving a list of explicit
220
+ The piecewise linear interpolated function creates discontinuities in the RHS of the system.
221
+ However, since we know the times exactly, we can handle this by simply giving a list of explicit
221
222
tstops to the solve command, to make sure those are hit exactly.
222
223
=#
223
- load2 (t) = - Interpolate (SA[0 , 4 , 12 , 20 , 24 ]* 3600 , SA[20 , 30 , 10 , 30 , 20 ], extrapolate= LinearInterpolations. Constant (20 ))(t)
224
- load3 (t) = - Interpolate (SA[0 , 4 , 12 , 20 , 24 ]* 3600 , SA[40 , 50 , 30 , 50 , 40 ], extrapolate= LinearInterpolations. Constant (40 ))(t)
225
- @register_symbolic load2 (t)
226
- @register_symbolic load3 (t)
227
- ModelingToolkit. get_unit (op:: typeof (load2), _) = u " m^3/s"
228
- ModelingToolkit. get_unit (op:: typeof (load3), _) = u " m^3/s"
224
+ load2 = LinearInterpolation (- 1 * Float64[20 , 30 , 10 , 30 , 20 ], [0 , 4 , 12 , 20 , 24 ]* 3600.0 ; extrapolation= ExtrapolationType. Constant)
225
+ load3 = LinearInterpolation (- 1 * Float64[40 , 50 , 30 , 50 , 40 ], [0 , 4 , 12 , 20 , 24 ]* 3600.0 ; extrapolation= ExtrapolationType. Constant)
226
+ ModelingToolkit. get_unit (:: LinearInterpolation , _ ) = 1.0 # type piracy!
229
227
nothing # hide
230
228
231
229
#=
230
+ As a workaround we had to explicitly define `LinearInterpolations` as unitless, which is type piracy! Don't to this in any package code!
231
+
232
232
## Building the Network
233
233
234
- To bild the Network we first need to define the components. This is a two step process:
234
+ To build the network, we first need to define the components. This is a two- step process:
235
235
236
236
- first create the symbolic `ODESystem` using ModelingToolkit
237
237
- secondly build a NetworkDynamics component model ([`VertexModel`](@ref)/[`EdgeModel`](@ref)) based on the symbolic system.
238
238
239
239
In the first step we can use the keyword arguments to pass "default" values for our parameters and states.
240
- Those values will be automaticially transfered to the metadata of the component model the second step.
240
+ Those values will be automatically transferred to the metadata of the component model in the second step.
241
241
242
242
The second step requires to define the interface variables, i.e. what are the "input" states of your
243
243
component model and what are the "output" states.
@@ -255,10 +255,10 @@ v2 = VertexModel(v2_mtk, [:q̃_nw], [:p]; name=:v2, vidx=2)
255
255
v3 = VertexModel (v3_mtk, [:q̃_nw ], [:p ]; name= :v3 , vidx= 3 )
256
256
257
257
#=
258
- For the edge Model we have two inputs: the pressure on both source and destination end.
259
- There is a single output state: the volumetric flow. However we also need to tell
258
+ For the edge model we have two inputs: the pressure on both source and destination end.
259
+ There is a single output state: the volumetric flow. However, we also need to tell
260
260
NetworkDynamics about the coupling type. In this case we use `AntiSymmetric`, which
261
- meas that the source end will recieve the same flow, just inverted sign.
261
+ means that the source end will receive the same flow, just with inverted sign.
262
262
=#
263
263
@named e12_mtk = Pipe (; L= L₁₂, sinθ= sinθ₁₂, D, A, γ, η, r, g, T, Tc, pc, Rs, c̃, ρ̃, p̃)
264
264
@named e13_mtk = Pipe (; L= L₁₃, sinθ= sinθ₁₃, D, A, γ, η, r, g, T, Tc, pc, Rs, c̃, ρ̃, p̃)
@@ -271,10 +271,10 @@ e23 = EdgeModel(e23_mtk, [:p_src], [:p_dst], AntiSymmetric([:q̃]); name=:e23, s
271
271
#=
272
272
To build the network object we just need to pass the vertices and edges to the constructor.
273
273
274
- Note that we've used the `vidx` and `src`/`dst` keywords in the constructors to define for
275
- each component to which "part" of the network it belongs.
274
+ Note that we've used the `vidx` and `src`/`dst` keywords in the constructors to define
275
+ for each component to which "part" of the network it belongs.
276
276
277
- This means, the constructor can automaticially construct a graph based on those informations and
277
+ This means the constructor can automatically construct a graph based on that information and
278
278
we don't need to pass it explicitly.
279
279
=#
280
280
@@ -286,38 +286,28 @@ structurally different, because both functions capure a unique loadprofile funct
286
286
287
287
## Finding a Steady State
288
288
289
- To simulate the systme , we first need to find a steadystate . As a "guess" for that
289
+ To simulate the system , we first need to find a steady state . As a "guess" for that,
290
290
we create a `NWState` object from the network.
291
291
This will allocate flat arrays for states `u` and parameters `p` and fill them with the
292
292
default values.
293
293
=#
294
294
uguess = NWState (nw)
295
295
296
296
#=
297
- This is not a steadystate of the system however. To find a true steadystate we want to ensure
298
- that the lhs of the system is zero.
299
- We can solve for a steady state numerically by defining a Nonlinear Rootfind problem.
300
-
301
- To do so, we need to wrap the Network object in a closure.
302
- =#
303
- nwwrap = (du, u, p) -> begin
304
- nw (du, u, p, 0 )
305
- nothing
306
- end
307
- initprob = NonlinearProblem (nwwrap, uflat (uguess), pflat (uguess))
308
- initsol = solve (initprob)
297
+ This is not a steady state of the system however. To find a true steady state, we want to ensure
298
+ that the LHS of the system is zero.
309
299
310
- #=
311
- We can create a new `NWState` object by wrapping the solution from the nonlinear problem and the
312
- original prameters in a new `NWState` object.
300
+ We can use the [`find_fixpoint`](@ref) from `NetworkDynamics.jl` to initialize the system.
301
+ Internally, this uses a nummerical solve for the rootfind problem `0 = rhs`.
302
+ The result is automaticially wrapped as a [ `NWState`](@ref) object.
313
303
=#
314
- u0 = NWState (nw, initsol . u, uguess. p )
304
+ u0 = find_fixpoint (nw, uguess)
315
305
316
306
#=
317
307
## Solving the ODE
318
308
319
309
Using this as our initial state we can create the actual `ODEProblem`.
320
- Since the ode allways operates on flat state and aprameter arrays we use `uflat` and `pflat` to extract them.
310
+ Since the ODE always operates on flat state and parameter arrays, we use `uflat` and `pflat` to extract them.
321
311
=#
322
312
prob = ODEProblem (nw, uflat (u0), (0.0 ,24 * 3600 ), copy (pflat (u0)))
323
313
sol = solve (prob, Tsit5 (), tstops= [0 ,4 ,12 ,20 ,24 ]* 3600 )
@@ -358,8 +348,8 @@ Notably, the "internal" states defined in the symbolic models are not "states" i
358
348
For example, we captured the load profile in the `q̃_inj` state of the `VariablePressureNode`.
359
349
The only dynamic state of the model however is `p`.
360
350
Using the "observables" mechanism from SciML, which is implemented by NetworkDynamics, we can reconstruct
361
- those "optimized" states which have been removed symbolicially .
362
- Here we plot the reconstructed load profile of nodes 2 and 3. Also, we know that node 1 is infinetly stiff,
351
+ those "optimized" states which have been removed symbolically .
352
+ Here we plot the reconstructed load profile of nodes 2 and 3. Also, we know that node 1 is infinitely stiff,
363
353
acting as an infinite source of volumetric flow. We can reconstruct this flow too.
364
354
=#
365
355
fig = begin
0 commit comments