Getting Started with PowerDynamics.jl
This tutorial introduces the core ideas behind PowerDynamics.jl and its relationship to the SciML ecosystem.
This tutorial can be downloaded as a normal Julia script here.
PowerDynamics.jl is a tool for modeling and simulating dynamic powergrid models. Its main idea is to build equation-based, symbolic models for various dynamic components. Different components, such as shunts, generators or controllers are then connected to form dynamic models representing entire Buses or Lines. The dynamic Bus and Line models are then interconnected to form powergrids.
The most important distinction in contrast to other tools is that PowerDynamics.jl is a modeling framework rather than a simulation tool. At its core, a dynamic powergrid model is just a set of differential-algebraic equations (DAEs) that describe the evolution of the system over time. PowerDynamics.jl helps you to build these DAE models in a modular way, and then simulate them using the powerful solvers from the SciML ecosystem.
PowerDynamics.jl gives you direct access to the underlying DAE structure and purposely exposes you to the "raw" commands from the SciML Ecosystem, most importantly DifferentialEquations.jl. While this can be a bit overwhelming at first, it really pays off to learn the API of the underlying packages directly rather than wrapping them all up in a PowerDynamics-specific API.
This tight integration means, that it is much easier to transfer advanced SciML methods and concepts to systems defined with PowerDynamics.jl.
In this tutorial, we will model the same physical system, a Single-Machine-Infinite-Bus (SMIB), in two different ways. First, we'll build it as a "plain" ModelingToolkit model using pure SciML packages. Then, we'll model the same system using PowerDynamics' component-based approach. This side-by-side comparison highlights the parallels in nomenclature and workflow between the two approaches.
The workflow for both approaches looks like this:
โญโโโโโโโโโโโโโโโโโโโโโโโโโฎ โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
โ Pure MTK Model โ โ PowerDynamics.jl Model โ
โโโโโโโโโโโโโโโโโโโโโโโโโโก โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโก
โ Equation-based model โ โ Composite Model consisting โ
โ of the entire system. โ โ of equation-based MTK โ
โฐโโโโโโโโโโโโโโโโโโโโโโฌโโโฏ โ models for Buses and Lines โ
โ โ โญโโโโโโโโฎ โ
โ โ 2 โฏโโฟ โฟโโฏ 3 โ
โ โ โ โ โญโโโโฏ โ โ
โ โ โทโโฏโโท 1 โ
โ โ (~) โ
โ โฐโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโฏ
ModelingToolkit.jl โ โ PowerDynamics.jl
generates โพ โพ generates
โญโโโโดโโโโโโโโโโโโดโโโโฎ
โ DAE System โ
โ M ฬx = f(x, p, t) โ
โฐโโโโโโโโโโฌโโโโโโโโโโฏ
RHS function + Mass Matrix โพ
โญโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโฎ
โ SciML-ODEProblem โ
โ Data structure for time-domain simulation โ
โฐโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโฏ
OrdinaryDiffEq.jl solver โพ
โญโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโฎ
โ SciML-ODESolution โ
โ Solution object containing the time series โ
โ for all components โ
โฐโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโฏ
Symbolic Indexing โพ
โญโโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
โ Time-series Inspection โ
โ Symbolic indexing allows for easy access to all โ
โ states of all subcomponents for detailed analysis. โ
โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ
Top-level Packages:
- PowerDynamics.jl: The main package for building powergrid models. It provides a library and modeling tools specific to power systems, such as powerflow models and component libraries.
- NetworkDynamics.jl: Our backend package that provides most of the core functionality. It is general-purpose and can model any kind of networked dynamical system.
SciML Packages:
- ModelingToolkit.jl (MTK): A symbolic modeling framework for defining and manipulating differential equations. The key word here is symbolically โ you write equations, not numerical code. MTK automatically performs simplifications and generates efficient numerical code for simulation.
- DifferentialEquations.jl: Umbrella package for everything related to differential equations, including stochastic and delay differential equations. Since it's large, we typically import specific subpackages, i.e.:
- OrdinaryDiffEq.jl: Solvers for ordinary differential equations (ODEs and DAEs). You can reduce load time even further by only importing specific solver packages like OrdinaryDiffEqRosenbrock.jl or OrdinaryDiffEqTsit5.jl.
- NonlinearSolve.jl: Solvers for nonlinear systems of equations, used for powerflow calculations and DAE initialization.
Other Packages:
- Makie.jl: A powerful plotting package for visualizing results with its backends CairoMakie.jl for vector graphic output and GLMakie.jl/WGLMakie.jl for interactive visualizations.
Simple ModelingToolkit System
In this section we'll model the simplest Single-Machine-Infinite-Bus System (SMIB): a Swing equation connected to a slack bus.
ฯ,ฮธ
โคบ
Turbine Power Pโ ๐ญโโโ๐ญ Pโ Electrical Power
โโ ๐ญโโโ๐ญ โโ
H
The equations of the rotor connected to the infinite bus can be written as:
\[\begin{aligned} \dot{\theta} &= \omega\\ M\,\dot{\omega} &= P_\mathrm{m} - P_\mathrm{e} - D\,\omega&&\text{Swing Equation with}\\ P_\mathrm{e} &= \frac{1}{X}\sin{\theta}&&\text{connection to infinite bus with}\ ฮด=0 \end{aligned}\]
where $M$ is the inertia, $\omega_s$ is the synchronous speed, $P_m$ is the mechanical power input, and $P_e$ is the electrical power output. The state is described by the rotor angle $\theta$ and the angular velocity $\omega$ (deviation from synchronous speed). The ideal rotor is connected to a slack bus via a lossless transmission line with reactance $X$.
To simulate this system, we first need to import some packages...
using ModelingToolkit
using ModelingToolkit: t_nounits as t, D_nounits as Dt
using OrdinaryDiffEqRosenbrock
using CairoMakie
MTK: Model Definition & Simulation
Julia allows you to use unicode characters in variable names. In most Julia development environments you can insert them with LaTeX-like syntax: \alpha<TAB> โ ฮฑ
, \_e<TAB> โ โ
and \_+ โ โ
. Especially โ
is important as it is used as a separator in MTK.
After importing the packages, we can define the symbolic system using the @mtkmodel
macro:
@mtkmodel SwingInfiniteBus begin
@parameters begin
M = 1 # machine inertia
D = 1 # machine damping
Pโ = 1 # mechanical power
X = 0.1 # reactance of powerline
end
@variables begin
ฮธ(t) # rotor angle
ฯ(t) # angular velocity (rel to sync. speed)
Pโ(t) # electrical power (connection to IB)
end
@equations begin
Pโ ~ 1/X * sin(ฮธ)
Dt(ฮธ) ~ ฯ
M * Dt(ฯ) ~ Pโ - Pโ - D*ฯ
end
end
The definition of the system is quite straightforward. Note how we defined 3 states, including one for the electrical power $P_\mathrm{e}$. We can instantiate the system by calling its constructor SwingInfiniteBus()
:
@named symbolic_system = SwingInfiniteBus()
full_equations(symbolic_system) # show all equations
\[ \begin{align} \mathtt{P_e}\left( t \right) &= \frac{\sin\left( \theta\left( t \right) \right)}{X} \\ \frac{\mathrm{d} \theta\left( t \right)}{\mathrm{d}t} &= \omega\left( t \right) \\ M \frac{\mathrm{d} \omega\left( t \right)}{\mathrm{d}t} &= \mathtt{P_m} - \mathtt{P_e}\left( t \right) - D \omega\left( t \right) \end{align} \]
In order to simulate the system, we need to call mtkcompile
, which will essentially perform a symbolic simplification of the system:
compiled_system = mtkcompile(symbolic_system)
full_equations(compiled_system) # show all equations
\[ \begin{align} \frac{\mathrm{d} \omega\left( t \right)}{\mathrm{d}t} &= \frac{ - \mathtt{P_m} + \frac{\sin\left( \theta\left( t \right) \right)}{X} + D \omega\left( t \right)}{ - M} \\ \frac{\mathrm{d} \theta\left( t \right)}{\mathrm{d}t} &= \omega\left( t \right) \end{align} \]
You can see that the "compiled" system only consists of two states, $\theta$ and $\omega$. This is because $P_\mathrm{e}$ is not really a state of the system, but rather an intermediate variable, so it was thrown out. While trivial in this case, this is the symbolic simplification at work.
To simulate the system, we need to define initial conditions for the states $\theta$ and $\omega$. Also, we need to define a time span for the simulation.
u0 = [
compiled_system.ฮธ => 0.0,
compiled_system.ฯ => 0.0,
]
tspan = (0.0, 10.0)
Combining the compiled system, initial conditions, and time span, we can define a so-called ODEProblem
.
prob = ODEProblem(compiled_system, u0, tspan)
ODEProblem with uType Vector{Float64} and tType Float64. In-place: true
Initialization status: FULLY_DETERMINED
Non-trivial mass matrix: false
timespan: (0.0, 10.0)
u0: 2-element Vector{Float64}:
0.0
0.0
The ODEProblem contains all the information needed to simulate the system. We can simulate the system using any of the solvers from OrdinaryDiffEq.jl. In this case, we decided to use the Rodas5P
solver from OrdinaryDiffEqRosenbrock.jl
.
sol = solve(prob, Rodas5P())
MTK: Solution Handling
The solution object we get contains all the time series in the system. For low-level access, we can look at
sol.t
37-element Vector{Float64}:
0.0
9.999999999999999e-5
0.0010999999999999998
0.011099999999999997
0.04948942490065575
0.11326144381079117
0.19874440292127327
0.3120731700268947
0.453946017165926
0.6287173910305606
โฎ
7.010443455834744
7.368959713888967
7.790025606351214
8.21109149881346
8.632157391275706
9.067562297839844
9.502967204403982
9.947833955811438
10.0
to get an array of all the points in time the solver stepped to. While
sol.u
37-element Vector{Vector{Float64}}:
[0.0, 0.0]
[9.999499850007931e-5, 4.999833295834928e-9]
[0.0010993930046600358, 6.047776178843383e-7]
[0.011036355670577466, 6.13713955994444e-5]
[0.048087927994389795, 0.0012021989206479854]
[0.10480849085000353, 0.006113357565607139]
[0.1686149771857689, 0.017910739105420462]
[0.2267008386608886, 0.04061687237054282]
[0.2522804237032705, 0.07521841529439802]
[0.21637536997011117, 0.11714704213030243]
โฎ
[0.0015197707403184768, 0.10306570242988226]
[-0.006579919072431453, 0.10194659957842682]
[-0.004990999605295394, 0.09910662879643255]
[0.002243995103244743, 0.0985588876430202]
[0.004208143699585749, 0.10019511079357862]
[0.00012118288329842279, 0.10123748198102822]
[-0.0026809003480379396, 0.10051649468142382]
[-0.0009343202224257873, 0.09958573081796912]
[-0.0005817725033815394, 0.0995461772659495]
gives the full state of the system for each of the time points.
However, this is far from all we can do with the solution object! First off, since we use dense output by default, we can interpolate the solution at any point in time:
sol(2.5) # interpolate at t=2.5s (better than linear interpolation)
2-element Vector{Float64}:
0.0915931399515922
0.09362252539085167
The output, however, is still not very user friendly, since we only get a vector of values. This is where symbolic indexing comes to our help! Using the syntax
sol(1.0, idxs=compiled_system.ฮธ) # get ฮธ at t=1.0s
0.16079143211660155
we can extract a specific state at a specific time point. This syntax has lots of variants; for example, we can efficiently interpolate multiple states at multiple time points:
sol([0.0, 1.0], idxs=[compiled_system.ฮธ, compiled_system.ฯ]) # get ฮธ and ฯ at t=0.0s and t=1.0s
t: 2-element Vector{Float64}:
0.0
1.0
u: 2-element Vector{Vector{Float64}}:
[0.0, 0.0]
[0.16079143211660155, 0.00510049738675578]
Since ModelingToolkit keeps track of all of its simplifications, we can also extract so-called "observed" states, i.e., states that were part of the original symbolic system but got eliminated during compilation. For this example, we can get the electrical power $P_\mathrm{e}$ at any time point even though it is not part of the solution itself:
sol(0.5, idxs=compiled_system.Pโ) # get Pe at t=0.5s
0.8668372862402866
Finally, we can use the same symbolic indexing syntax in plotting commands. The example below uses Makie.jl; however, the commands are very similar in Plots.jl.
let
fig = Figure()
ax = Axis(fig[1,1], xlabel="Time (s)", ylabel="States")
lines!(sol, idxs=compiled_system.ฮธ, color=:darkred)
lines!(sol, idxs=compiled_system.ฯ, color=:darkblue)
lines!(sol, idxs=compiled_system.Pโ, color=:darkgreen)
axislegend(ax; position=:rt)
fig
end

Simple PowerDynamics System
Now, we're going to model the same physical system but this time using the component based approach of PowerDynamics.jl
This means, we'll define two buses with a pi-line (zero shunts) connecting them.
bus 1 bus 2
โป โป
(โ)โถโโโโโโโโโโโโโโโโโโโโโโโโด(~)
swing-eqs โน pi-line โน slack/infinite bus
First, we need to load the PowerDynamics.jl package:
using PowerDynamics
using PowerDynamics: Library
We start by loading a Swing model generator from the library.
@named symbolic_swing = Library.Swing(V=1)
full_equations(symbolic_swing) # show all equations
\[ \begin{align} \frac{\mathrm{d} \theta\left( t \right)}{\mathrm{d}t} &= \omega\left( t \right) - \mathtt{\omega\_ref} \\ M \frac{\mathrm{d} \omega\left( t \right)}{\mathrm{d}t} &= \mathtt{Pm} - \mathtt{Pel}\left( t \right) - D \left( \omega\left( t \right) - \mathtt{\omega\_ref} \right) \\ \mathtt{Pel}\left( t \right) &= \mathtt{terminal.i\_r}\left( t \right) \mathtt{terminal.u\_r}\left( t \right) + \mathtt{terminal.i\_i}\left( t \right) \mathtt{terminal.u\_i}\left( t \right) \\ \mathtt{terminal.u\_r}\left( t \right) &= V \cos\left( \theta\left( t \right) \right) \\ \mathtt{terminal.u\_i}\left( t \right) &= V \sin\left( \theta\left( t \right) \right) \end{align} \]
The equations represent a classic swing equation with no voltage dynamics. We passed the keyword argument V=1
to set the voltage magnitude to 1 p.u. So far, this is a "pure" MTK model, similar to the symbolic_system
from above. The equations are structurally identical to the ones we defined manually above only differing in some conventions.
We can then compile the model to get rid of intermediate variables and make it ready for simulation. We do so by calling compile_bus
. The additional call to MTKBus
can be ignored for now and is explained in further tutorials and the Modeling Concepts docs. Additionally, we give the bus model an index using the vidx
keyword (short for vertex index).
bus1 = compile_bus(MTKBus(symbolic_swing); vidx=1, name=:swing)
VertexModel :swing NoFeedForward() @ Vertex 1
โโ 2 inputs: [busbarโi_r, busbarโi_i]
โโ 2 states: [symbolic_swingโฯโ0, symbolic_swingโฮธโ0]
โโ 2 outputs: [busbarโu_r=1, busbarโu_i=0]
โโ 5 params: [symbolic_swingโฯ_ref=1, symbolic_swingโPmโ1, symbolic_swingโM=0.005, symbolic_swingโD=0.0001, symbolic_swingโV=1]
This object is a so-called VertexModel
. VertexModels (and EdgeModels) are the building blocks of systems in PowerDynamics.jl and NetworkDynamics.jl. From the printout you can already see that it has different variables/parameters with some default values and so on.
For the second bus, we use a slack bus (also called infinite bus), which maintains constant voltage magnitude and angle:
@named symbolic_slack = Library.VฮดConstraint(; V=1, ฮด=0)
bus2 = compile_bus(MTKBus(symbolic_slack); vidx=2, name=:slack)
VertexModel :slack NoFeedForward() @ Vertex 2
โโ 2 inputs: [busbarโi_r, busbarโi_i]
โโ 0 states: []
โโ 2 outputs: [busbarโu_r=1, busbarโu_i=0]
โโ 2 params: [symbolic_slackโV=1, symbolic_slackโฮด=0]
The VฮดConstraint
enforces $V=1$ p.u. and $\delta=0$ at all times, which is the mathematical definition of a slack/infinite bus.
And a powerline connecting the two:
@named symbolic_piline = Library.PiLine()
line = compile_line(MTKLine(symbolic_piline); src=1, dst=2)
EdgeModel :line PureFeedForward() @ Edge 1=>2
โโ 2/2 inputs: src=[srcโu_r, srcโu_i] dst=[dstโu_r, dstโu_i]
โโ 0 states: []
โโ 2/2 outputs: src=[srcโi_r, srcโi_i] dst=[dstโi_r, dstโi_i]
โโ 9 params: [symbolic_pilineโR=0, symbolic_pilineโX=0.1, symbolic_pilineโG_src=0, symbolic_pilineโB_src=0, symbolic_pilineโG_dst=0, symbolic_pilineโB_dst=0, symbolic_pilineโr_src=1, symbolic_pilineโr_dst=1, symbolic_pilineโactive=1]
The powerline got the src
and dst
keywords. This means our line is defined from bus 1 to bus 2.
Having defined all the components, we can now connect them to a network model.
nw = Network([bus1, bus2], line)
Network with 2 states and 16 parameters
โโ 2 vertices (2 unique types)
โโ 1 edges (1 unique type)
Edge-Aggregation using SequentialAggregator(+)
The nw
object is somewhat similar to the compiled_system
from above: it is a fully defined DAE system (ODE system in this case) that can be simulated. Similar to the compiled_system
, it not only contains the right-hand-side function but also contains information necessary for symbolic indexing, i.e., which component has which states/parameters under which names and so on.
PD: Symbolic Indexing
In contrast to the MTK example above, our symbolic indices are "hierarchical", i.e., we have to specify the component first and then the state/parameter name.
VIndex
objects are used to reference states/parameters of vertex-entities (buses, shunts, generators, loads, etc.),
VIndex( 1, :symbolic_swingโฯ)
VIndex(:swing, :symbolic_swingโฮธ)
โถโโฌโโโด โถโโโโโโโโฌโโโโโโโโด
โต โ
Index/name of vertex โ
โต
Name of parameter/state
while EIndex
objects are used to reference states/parameters of edge-entities (lines, transformers, etc.),
EIndex( 1, :srcโP )
EIndex( :edge, :srcโQ )
EIndex( 1 => 2, :srcโQ )
EIndex(:swing => :slack, :symbolic_pilineโR)
โถโโโโโโโฌโโโโโโโโด โถโโโโโโโโฌโโโโโโโโโด
โต โ
Index/name or src-dst pair โ
โต
Name of parameter/state
PD: Manual Definition of Initial Conditions
For large systems with possibly thousands of states and parameters, finding a suitable initial state is a hard problem which is covered in depth in later tutorials.
For now, our system is quite simple and we can find a suitable initial state by hand. We can create a "default" state by calling NWState
on the network object:
s0 = NWState(nw)
NWState{Vector{Float64}} of Network (2 vertices, 1 edges)
โโ VIndex(1, :symbolic_swingโฯ) => NaN
โโ VIndex(1, :symbolic_swingโฮธ) => NaN
p = NWParameter([1.0, NaN, 0.005, 0.0001, 1.0, 1.0, 0.0, 0.0, 0.1, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0])
t = nothing
This creates a state and parameter object, which is prefilled with all of the default values stored in the Network. The undefined states/parameters are set to NaN
.
!!! note Automatic State Reduction In the printout of s0
you see only two "real" states: $\omega$ and $\theta$ of the swing equation, everything else was simplified away, just like in the MTK example above.
Using the symbolic indexing syntax described above, we can now set the initial conditions for all states and parameters that are not already defined.
Similar to the example above, we start at 0 angle and a frequency of 1 p.u. (in contrast to the MTK example above, the swing model from the library is defined in terms of PU frequency not frequency deviation):
s0[VIndex(1, :symbolic_swingโฮธ)] = 0.0
s0[VIndex(:swing, :symbolic_swingโฯ)] = 1 # alternatively, reference vertex by unique name
Instead of using the symbolic indices explicitly, NWState
supports a more user-friendly syntax for accessing states. We set the mechanical power input, the inertia, and the damping of the machine at bus 1:
s0.v[1][:symbolic_swingโPm] = 1
s0.v[1][:symbolic_swingโM] = 1
s0.v[:swing][:symbolic_swingโD] = 1
It is important to understand that at its core, NWState
objects are just "wrappers" around flat arrays. Similar to the pure-MTK example above, where our state vector u
was just a vector of 2 plain values, the NWState
object contains a flat vector of all states and a flat vector of all parameters. The flat vectors can be accessed using the uflat
and pflat
functions:
uflat(s0)
2-element Vector{Float64}:
1.0
0.0
pflat(s0)
16-element Vector{Float64}:
1.0
1.0
1.0
1.0
1.0
1.0
0.0
0.0
0.1
0.0
0.0
0.0
0.0
1.0
1.0
1.0
By wrapping those flat vectors in a NWState
object we make them "human readable" by providing symbolic indexing.
There are lots of things you can do with NWState
objects. For example, once again it is possible to inspect "observed" statesโstates which are not actually part of the state vector but rather intermediate variables. For example, we can inspect the active power at both src and destination end.
s0.e[1=>2]([:srcโP, :dstโP])
FilteringProxy for NWState()
Component filter: EIndex(1 => 2)
State filter: [:srcโP, :dstโP]
Types: states โ parameters โ inputs โ outputs โ observables โ
Matching Indices:
โญ EIndex(1, :srcโP) 0 :line
โฐ EIndex(1, :dstโP) 0
Unsurprisingly, since we start at an angle of 0 with both slack and swing, there is no active power flow.
PD: Simulation of the System
Similar to before, we take our model and use it to define an ODEProblem
. We can then solve it using the Rodas5P
solver again.
The only notable difference here is, that we need to pass both flat vectors: states and parameters.
prob = ODEProblem(nw, uflat(s0), (0.0, 10.0), copy(pflat(s0)))
sol = solve(prob, Rodas5P())
PD: Solution Handling
The solution handling is analogous to the pure-MTK example above.
sol(1.0, idxs=VIndex(1, :symbolic_swingโฮธ)) # get ฮธ of bus 1 at t=1.0s
0.16079122457153705
For generating lists of symbolic indices at once, NetworkDynamics.jl provides the auxiliary functions vidxs
and eidxs
:
vidxs(nw, :, :busbarโu_arg) # create lists of VIndex objects
2-element Vector{NetworkDynamics.SymbolicIndex}:
VIndex(1, :busbarโu_arg)
VIndex(2, :busbarโu_arg)
sol(1.0, idxs=vidxs(nw, :, :busbarโu_arg)) # use vidxs get voltage angle of all buses at t=1.0s
2-element Vector{Float64}:
0.16079122457153708
0.0
Certain electrical "bus" states, such as :busbarโu_arg
or :busbarโu_mag
, are available at every bus regardless of the models attached to that bus. The full list of avialable symbols can be checked interatively using s0.v[1]
/s0.e[1=>2]
.
Sometimes, you want to get the full NWState
at a specific time point.
s10 = NWState(sol, 1.0) # get full NWState at t=1.0s
NWState{Vector{Float64}} of Network (2 vertices, 1 edges)
โโ VIndex(1, :symbolic_swingโฯ) => 1.0050995390965825
โโ VIndex(1, :symbolic_swingโฮธ) => 0.16079122457153705
p = NWParameter([1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.1, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0])
t = 1.0
which you can then inspect as before:
s10.e[1=>2]([:srcโP, :dstโP]) # get active power at line 1=>2 at t=1.0s
FilteringProxy for NWState()
Component filter: EIndex(1 => 2)
State filter: [:srcโP, :dstโP]
Types: states โ parameters โ inputs โ outputs โ observables โ
Matching Indices:
โญ EIndex(1, :srcโP) -1.6009928 :line
โฐ EIndex(1, :dstโP) 1.6009928
We can do some plotting as before:
let
fig = Figure()
ax = Axis(fig[1,1], xlabel="Time (s)", ylabel="Voltage Angles")
lines!(sol, idxs=VIndex(1, :symbolic_swingโฮธ), color=:darkred)
lines!(sol, idxs=VIndex(2, :busbarโu_arg), color=:darkblue)
axislegend(ax; position=:rt)
ax = Axis(fig[2,1], xlabel="Time (s)", ylabel="Frequency at Swing")
lines!(sol, idxs=VIndex(1, :symbolic_swingโฯ), color=:darkred)
axislegend(ax; position=:rt)
ax = Axis(fig[3,1], xlabel="Time (s)", ylabel="Active Power in Line")
lines!(sol, idxs=EIndex(1=>2, :srcโP), color=:darkgreen, label="P injected towards bus 1")
lines!(sol, idxs=EIndex(1=>2, :dstโP), color=:lightgreen, label="P injected towards bus 2")
axislegend(ax; position=:rt)
fig
end

We observe the expected behavior:
- as in the pure MTK example, the swing node accelerates and oscillates around until it settles at a new steady state, where the angle difference between bus 1 and the slack bus (with $\delta=0$) leads to a power flow of $P_\mathrm{e} = P_\mathrm{m} = 1$ p.u.
- in steady state, the active power injected at bus 1 is equal to the active power extracted at bus 2 (lossless line)
This page was generated using Literate.jl.