|  | 
|  | 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 | +        q̃(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 - q̃ # 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 | +        q̃ ~ (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 | +=# | 
0 commit comments