Skip to content

Commit dae5ef6

Browse files
authored
Merge pull request #222 from JuliaDynamics/hw/datainterp
use DataInterpolations in gas network example
2 parents c544c15 + e247d42 commit dae5ef6

File tree

7 files changed

+106
-72
lines changed

7 files changed

+106
-72
lines changed

docs/Project.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ name = "ND.jl docs"
33
[deps]
44
Bonito = "824d6782-a2ef-11e9-3a09-e5662e0c26f8"
55
CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0"
6+
DataInterpolations = "82cc6244-b520-54b8-b5a6-8a565e85f1d0"
67
DiffEqCallbacks = "459566f4-90b8-5000-8ac3-15dfb0a30def"
78
Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
89
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
@@ -12,7 +13,6 @@ Electron = "a1bb12fb-d4d1-54b4-b10a-ee7951ef7ad3"
1213
GraphMakie = "1ecd5474-83a3-4783-bb4f-06765db800d2"
1314
Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6"
1415
LaTeXStrings = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f"
15-
LinearInterpolations = "b20c7882-2490-4592-a606-fbbfe9e745e8"
1616
Literate = "98b081ad-f1c9-55d3-8b20-4c87d4299306"
1717
ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78"
1818
NetworkDynamics = "22e9dc34-2a0d-11e9-0de0-8588d035468b"
@@ -39,6 +39,7 @@ NetworkDynamicsInspector = {path = "../NetworkDynamicsInspector"}
3939
[compat]
4040
Bonito = "≥0.0.1"
4141
CairoMakie = "0.13.1"
42+
DataInterpolations = "7"
4243
DiffEqCallbacks = "4.2.2"
4344
Distributions = "0.25.117"
4445
Documenter = "1.8.0"
@@ -48,7 +49,6 @@ Electron = "≥0.0.1"
4849
GraphMakie = "0.5.13"
4950
Graphs = "≥0.0.1"
5051
LaTeXStrings = "1.4.0"
51-
LinearInterpolations = "0.1.4"
5252
Literate = "2.20.1"
5353
ModelingToolkit = "≥0.0.1"
5454
NetworkDynamics = "≥0.0.1"

docs/examples/gas_network.jl

Lines changed: 52 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
#=
2-
# Dynamic Flow in simple Gas Network
2+
# Dynamic Flow in Simple Gas Network
33
4-
This Example is based on the paper
4+
This example is based on the paper
55
66
> Albertus J. Malan, Lukas Rausche, Felix Strehle, Sören Hohmann,
77
> Port-Hamiltonian Modelling for Analysis and Control of Gas Networks,
88
> IFAC-PapersOnLine, Volume 56, Issue 2, 2023, https://doi.org/10.1016/j.ifacol.2023.10.193.
99
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.
1111
1212
This example can be dowloaded as a normal Julia script [here](@__NAME__.jl). #md
1313
@@ -19,21 +19,21 @@ using DynamicQuantities
1919
using ModelingToolkit: D as Dt, t as t
2020
using Test
2121
using StaticArrays
22-
using LinearInterpolations
22+
using DataInterpolations
2323
using OrdinaryDiffEqTsit5
2424
using CairoMakie
2525
CairoMakie.activate!(type="svg") #hide
2626

2727
#=
2828
## Node Models
2929
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
3232
perform dimensionality checks.
3333
3434
There are 2 node models used in the paper. The first node type has a constant
3535
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)).
3737
=#
3838
@mtkmodel ConstantPressureNode begin
3939
@parameters begin
@@ -55,12 +55,15 @@ nothing #hide
5555
The second node model is a variable pressure node. It has one output state (the pressure) and one input state,
5656
the aggregated flows from the connected pipes.
5757
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.
5959
=#
6060
@mtkmodel VariablePressureNode begin
6161
@structural_parameters begin
6262
load_profile # time dependent load profile
6363
end
64+
@constants begin
65+
load_unit = 1, [description="unit of the load profile", unit=u"m^3/s"]
66+
end
6467
@parameters begin
6568
C, [description="Lumped capacitance of connected pipes", unit=u"m^4 * s^2 / kg"]
6669
end
@@ -70,7 +73,7 @@ The source/load behaviour itself is provided via a time dependent function.
7073
q̃_nw(t), [description="aggregated flow from pipes into node", unit=u"m^3/s", input=true]
7174
end
7275
@equations begin
73-
q̃_inj ~ load_profile(t)
76+
q̃_inj ~ load_profile(t) * load_unit
7477
C * Dt(p) ~ q̃_inj + q̃_nw # (30)
7578
end
7679
end
@@ -79,10 +82,10 @@ nothing #hide
7982
#=
8083
## Pipe Model
8184
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.
8487
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).
8689
=#
8790
@mtkmodel Pipe begin
8891
@parameters begin
@@ -157,7 +160,6 @@ T = 278u"K" # simulation temperature
157160
r = 0.012u"mm" # pipe roughness
158161
D = 0.6u"m" # pipe diameter
159162

160-
## TODO: here is switched the lenths l12 and l13. The results are better. Is this a mistake in the paper?
161163
L₁₂ = 90u"km"
162164
L₁₃ = 80u"km"
163165
L₂₃ = 100u"km"
@@ -175,7 +177,7 @@ sinθ₂₃ = 0.0
175177
nothing # hide
176178

177179
#=
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
179181
the density at standard conditions:
180182
=#
181183
= 1 - 3.52 */pc * exp(-2.26*(T̃/Tc)) + 0.274 * (p̃/pc)^2 * exp(-1.878*(T̃/Tc)) # (5)
@@ -188,56 +190,54 @@ nothing # hide
188190
The equivalent "pressure capacity" at the nodes is calculated as a sum of the connected
189191
pipe parameters according to (28).
190192
191-
Here use defintions based on the speed and "standard" conditions.
193+
Here we use definitions based on the speed and "standard" conditions.
192194
=#
193195
C₂ = L₁₂*A/(2*ρ̃*^2) + L₂₃*A/(2*ρ̃*^2) # (28)
194196
C₃ = L₁₃*A/(2*ρ̃*^2) + L₂₃*A/(2*ρ̃*^2) # (28)
195197
nothing #hide
196198

197199
#=
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.
201203
=#
202204
nothing #hide
203205

204206
#=
205207
## Load Profile
206208
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.
209211
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.
213214
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.
217218
218219
!!! 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
221222
tstops to the solve command, to make sure those are hit exactly.
222223
=#
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!
229227
nothing #hide
230228

231229
#=
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+
232232
## Building the Network
233233
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:
235235
236236
- first create the symbolic `ODESystem` using ModelingToolkit
237237
- secondly build a NetworkDynamics component model ([`VertexModel`](@ref)/[`EdgeModel`](@ref)) based on the symbolic system.
238238
239239
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.
241241
242242
The second step requires to define the interface variables, i.e. what are the "input" states of your
243243
component model and what are the "output" states.
@@ -255,10 +255,10 @@ v2 = VertexModel(v2_mtk, [:q̃_nw], [:p]; name=:v2, vidx=2)
255255
v3 = VertexModel(v3_mtk, [:q̃_nw], [:p]; name=:v3, vidx=3)
256256

257257
#=
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
260260
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.
262262
=#
263263
@named e12_mtk = Pipe(; L=L₁₂, sinθ=sinθ₁₂, D, A, γ, η, r, g, T, Tc, pc, Rs, c̃, ρ̃, p̃)
264264
@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
271271
#=
272272
To build the network object we just need to pass the vertices and edges to the constructor.
273273
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.
276276
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
278278
we don't need to pass it explicitly.
279279
=#
280280

@@ -286,38 +286,28 @@ structurally different, because both functions capure a unique loadprofile funct
286286
287287
## Finding a Steady State
288288
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,
290290
we create a `NWState` object from the network.
291291
This will allocate flat arrays for states `u` and parameters `p` and fill them with the
292292
default values.
293293
=#
294294
uguess = NWState(nw)
295295

296296
#=
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.
309299
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.
313303
=#
314-
u0 = NWState(nw, initsol.u, uguess.p)
304+
u0 = find_fixpoint(nw, uguess)
315305

316306
#=
317307
## Solving the ODE
318308
319309
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.
321311
=#
322312
prob = ODEProblem(nw, uflat(u0), (0.0,24*3600), copy(pflat(u0)))
323313
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
358348
For example, we captured the load profile in the `q̃_inj` state of the `VariablePressureNode`.
359349
The only dynamic state of the model however is `p`.
360350
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,
363353
acting as an infinite source of volumetric flow. We can reconstruct this flow too.
364354
=#
365355
fig = begin

docs/src/mtk_integration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ as tearing of thousands of equations.
3636

3737
## RC-Circuit Example
3838
In good [MTK tradition](https://docs.sciml.ai/ModelingToolkit/stable/tutorials/acausal_components/), this feature will be explained along a simple RC circuit example.
39-
The [Dynamic Flow in simple Gas Network](@ref) example is another showcase of the MTK constructors.
39+
The [Dynamic Flow in Simple Gas Network](@ref) example is another showcase of the MTK constructors.
4040

4141
The system to model is 2 node, 1 edge network. The node output states are the voltage (to ground), the edge output sates are the currents at both ends.
4242
```

ext/MTKExt.jl

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ function generate_io_function(_sys, inputss::Tuple, outputss::Tuple;
298298
params = setdiff(allparams, Set(allinputs))
299299

300300
# extract the main equations and observed equations
301-
eqs::Vector{Equation} = full_equations(sys)
301+
eqs::Vector{Equation} = ModelingToolkit.subs_constants(full_equations(sys))
302302
fix_metadata!(eqs, sys);
303303

304304
# assert the ordering of states and equations
@@ -325,7 +325,8 @@ function generate_io_function(_sys, inputss::Tuple, outputss::Tuple;
325325
# extract observed equations. They might depend on eachother so resolve them
326326
obs_subs = Dict(eq.lhs => eq.rhs for eq in observed(sys))
327327
obseqs = map(observed(sys)) do eq
328-
eq.lhs ~ fixpoint_sub(eq.rhs, obs_subs)
328+
expanded_rhs = fixpoint_sub(eq.rhs, obs_subs)
329+
eq.lhs ~ ModelingToolkit.subs_constants(expanded_rhs)
329330
end
330331
fix_metadata!(obseqs, sys);
331332
# obs can only depend on parameters (including allinputs) or states

src/utils.jl

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,20 +118,28 @@ end
118118

119119
function rand_inputs_fg(rng, cf)
120120
@argcheck hasindim(cf) "ComponentModel has no specified input dimensions/syms"
121-
du = rand(rng, dim(cf))
121+
du = [NaN for _ in 1:dim(cf)]
122122
u = rand(rng, dim(cf))
123123
p = rand(rng, pdim(cf))
124124
ins = Tuple(rand(rng, l) for l in values(indim(cf)))
125125
if has_external_input(cf)
126126
ext = rand(rng, extdim(cf))
127127
ins = (ins..., ext)
128128
end
129-
outs = Tuple(rand(rng, l) for l in values(outdim(cf)))
129+
outs = Tuple([NaN for _ in 1:l] for l in values(outdim(cf)))
130130
t = NaN
131-
(outs, du, u, ins, p, t)
131+
(outs, du, u, ins, p, t) # fg extrancs outs and ints internally!
132132
end
133133
rand_inputs_fg(cf) = rand_inputs_fg(Random.default_rng(), cf)
134134

135+
function rand_inputs_obsf(rng, cf)
136+
(_, u, p, ins, p, t) = rand_inputs_fg(rng, cf)
137+
N = length(cf.obssym)
138+
outs = [NaN for _ in 1:N]
139+
(outs, u, ins..., p, t)
140+
end
141+
rand_inputs_obsf(cf) = rand_inputs_obsf(Random.default_rng(), cf)
142+
135143

136144
# abstract symbolic index types
137145
abstract type SymbolicIndex{C,S} end

test/MTK_test.jl

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,3 +257,38 @@ v = VertexModel(fullyimplicit, [:u], [:z])
257257
data = NetworkDynamics.rand_inputs_fg(v)
258258
b = @b $(NetworkDynamics.compfg(v))($data...)
259259
@test b.allocs == 0
260+
261+
@testset "Test constants in MTK models" begin
262+
@mtkmodel DQSwing_Constants begin
263+
@extend DQBus()
264+
@variables begin
265+
ω(t) = 0.0, [description = "Rotor frequency"]
266+
θ(t) = 0.0, [description = "Rotor angle"]
267+
Pel(t), [description = "Electrical Power"]
268+
end
269+
@constants begin
270+
V = 1, [description = "Voltage magnitude"]
271+
useless = 0
272+
end
273+
@parameters begin
274+
M = 1, [description = "Inertia"]
275+
D = 0.1, [description = "Damping"]
276+
Pmech, [description = "Mechanical Power"]
277+
end
278+
@equations begin
279+
Dt(θ) ~ ω
280+
Dt(ω) ~ 1/M * (Pmech - D*ω + Pel)
281+
Pel ~ real((u_r + im*u_i) * (i_r - im*i_i)) + useless
282+
u_r ~ V*cos(θ)
283+
u_i ~ V*sin(θ)
284+
end
285+
end
286+
@named withconst = DQSwing_Constants()
287+
v = VertexModel(withconst, [:i_r, :i_i], [:u_r, :u_i])
288+
289+
data = NetworkDynamics.rand_inputs_fg(v)
290+
NetworkDynamics.compfg(v)(data...) # no error
291+
292+
data = NetworkDynamics.rand_inputs_obsf(v)
293+
v.obsf(data...) # no error
294+
end

0 commit comments

Comments
 (0)