DPsim is a solver library for dynamic power system simulation.
It supports both the electromagnetic transient (EMT) and dynamic phasor (DP) domain for dynamic simulation.
A powerflow solver is included for standalone usage or to initialize dynamic simulations.
It provides a Python module which can be embedded in any Python 3 application / scripts.
The simulation core is implemented in highly-efficient C++ code.
It supports real-time execution with time-steps down to 50 uS.
It can load models in the IEC61970 Common Information Model (CIM) / Common Grid Model Exchange Standard (CGMES) XML format.
It can be interfaced to a variety of protocols and interfaces via VILLASnode.
Connect
Using or want to use DPsim? Find out more here:
GitHub Discussions - Ask questions, share ideas, and get community support. This is our main point of contact.
LF Energy Slack - Chat with other users and developers and get help in the #sogno or #sogno-dpsim channel.
For email inquiries, please contact the Institute for Automation of Complex Power Systems (ACS), which coordinates DPsim development: post_acs@eonerc.rwth-aachen.de.
How to contribute
If you want to get more involved with DPsim, we welcome contributions of all kinds, including code, documentation, examples, models, bug reports, feature requests, and reviews.
Please open a Pull Request or issue on GitHub, or start a discussion there to propose ideas and get feedback from the community.
If you use DPsim in your research, please cite it. See
how to cite for the software paper, a ready-made BibTeX
entry, and the papers covering specific methods.
1 - User Guide
Installing DPsim and running simulations with it.
For readers who want to run simulations: installation, building from source, and the tasks
that come up while setting up and running a study.
To change DPsim itself rather than use it, see the
developer guide. For the mathematics behind what the
solvers compute, see concepts.
1.1 - Install
Installing DPsim and its Python package.
DPsim is a Python module and C++ library for dynamic power system simulation.
The quickest route to a result is the Python module: install it, then work through the
tutorials, which build up one idea at a time from a source and a
resistor. If you would rather read a finished study than build one, the
example notebooks run complete scenarios and plot them.
Building from source is only needed for a platform without a published wheel, or to work on DPsim
itself; see build.
Try it without installing
The example notebooks run in the browser with no local installation:
Python package
DPsim is published on PyPI and installs like any other Python package:
Two limitations are worth knowing before you start.
Only Linux wheels are currently published, for CPython 3.9 through 3.13, so on Windows and macOS
you have to build from source for now.
The package also contains only the simulation core; the example notebooks additionally need
plotting and data handling packages, which are listed in the import section of each notebook.
Note that the image pins that access token in its startup command, so it is the same for
everyone who runs the image. Publish the port on localhost only, as above, and do not expose
it to an untrusted network.
To build the image yourself rather than pulling it, see
build.
1.2 - Examples
Runnable notebooks and C++ examples.
If you are starting out, start with the tutorials instead. They
work through one idea at a time in order, each as a complete script. This page is the inventory of
what else the repository carries, which is the right thing once you know what you are looking for.
The examples come in both languages. The Python notebooks run a scenario and plot the result in one
place, so they are the better way to see a complete study. The C++ examples are the better reference
for using DPsim as a library, and are what the real time and co-simulation scenarios are written in.
Under dpsim/examples/cxx,
built as part of a normal build and produced as executables in the build directory.
Directory
Contents
Circuits
Networks assembled directly in C++
Components
Single component scenarios
CIM
Reading network data from CIM and CGMES files
StateSpace
State-space extraction
RealTime
Scenarios run against the wall clock
DAE, signals, timer
Smaller scenarios for the DAE solver, signal models and timing
The target name is the source file name without its extension, regardless of which directory the
source sits in, and the executables are written flat into the build tree. So to build and run a
single example from your build directory:
The dpsim-villas
examples exchange data with other simulators or with hardware through VILLASnode. These need
WITH_VILLAS enabled and are therefore not available on Windows. See
interfaces for how the coupling works and
real time for running them against the wall
clock.
1.3 - Logging Results
Choosing what a simulation records, where it goes, and what it costs.
A simulation records nothing unless asked. Every quantity you want afterwards has to be named before
the run, because the solver keeps only what the current step needs and discards the rest.
dpsimpy.Logger is the one to reach for. It is the CSV data logger and it is what nearly every
example and notebook uses. There is also a real-time data logger, which is the right choice under a
real-time timer for the reason given below, and an interface for sending results somewhere other
than a file.
Only attributes can be logged
Watch out: only attributes can be logged
A logger records an attribute and nothing else. It cannot record an arbitrary expression, a
plain member variable, or a quantity a component computes internally without publishing.
The same constraint governs co-simulation and task scheduling, so a value that is not an attribute
cannot be logged, exchanged with another tool, or depended on by another task. Making it one is a
change to the model, not to the call site.
So the question is never “how do I log this value” but “is this value an attribute”. If it is, one
line records it. If it is not, no logger option will reach it, and the answer is to expose it as an
attribute in the model, which is a code change described under
attributes.
print_attribute_list() on any object prints what it publishes, which is the reliable way to find
out. Anything absent from that list cannot be logged.
The same fact explains a convenience: because attributes are the unit, a derived quantity that is
itself an attribute is logged the same way as a terminal voltage, with no special handling.
The three arguments are the column name you want in the output, the name of the attribute on the
object, and the object itself. The first is yours to choose and is what you will key on when reading
the file back; the second must match an attribute the object actually publishes.
Common attribute names are v on a node, and i_intf and v_intf on a component for the current
through it and the voltage across it. Components publish their own states as well: a machine offers
w_r, delta and Te, a converter its control states.
Naming an attribute that does not exist fails when the logger is set up, not at the end of the run,
so a typo costs a second rather than a simulation.
Where the file goes
By default the file is logs/<logger name>.csv under the working directory, where the name is the
one given to the Logger constructor.
Logger.set_log_dir changes that directory and Logger.get_log_dir reports it. Calling it is what
produces the nested logs/<something>/<name>.csv layout the example notebooks use, so a script that
does not call it gets the flat form. Setting it is worth doing when one script runs several
simulations, since two loggers with the same name otherwise write to the same file.
The same setting governs the diagnostic text log, which is why the two land side by side.
The file is plain CSV with a time column first and one column per logged attribute, in the order
they were registered. A three-phase quantity becomes three columns suffixed _0, _1, _2, and a
complex quantity in an envelope domain is written as a complex value that the reading side parses
back.
The keys are the column names you chose. Each value carries time and values arrays. In an
envelope domain the values are complex, so abs() gives the magnitude, and
frequency_shift_list recovers the waveform as shown in
comparing domains.
The first row is written before the first solve, so it holds the state the simulation started from
rather than a result. A plot that appears to begin at zero usually begins at that row.
What it costs
Logging is opt-in and costs nothing for what you do not ask for, but what you do ask for is written
every step. A run of 100 000 steps logging 50 attributes writes five million values, and on a large
network the file, not the solve, becomes the slow part.
Three things help. Log the attributes you will actually look at rather than everything available.
Prefer a specific attribute over a whole matrix; the rows_max and cols_max arguments to
log_attribute cap how much of a matrix quantity is written. And note that a large time step
reduces the file in exact proportion, so a study that only needs the envelope does not need the
step of one that needs the waveform.
Note: down-sampling is not reachable from Python
The C++ DataLogger additionally accepts a down-sampling factor, writing every n-th step. That
argument is not exposed on the Python Logger, which takes only a name, so from Python the step
size is the only control over how many rows you get.
Data logging against diagnostic logging
The word covers two unrelated things and they are easy to confuse.
What this page describes is the data logger: numerical results, CSV, opt-in per attribute. The
other is the diagnostic log, the text file recording what the solver did, controlled by
dpsimpy.LogLevel and passed to component constructors. Raising a component’s log level makes it
describe its own initialization and stamping in prose; it has no effect on the CSV.
They land in the same logs/ directory side by side, one as .csv and the other as .log, which
is why they get mistaken for each other. A component constructed with LogLevel.debug produces a
great deal of text and no additional results.
The real-time data logger mentioned at the top is a data logger like the first, not a third kind of
log. It records the same results and differs only in buffering them in memory and writing at the
end, because a disk write inside a real-time step has no bound on how long it takes. See
real-time.
1.4 - Real-Time
Running a simulation against a wall clock, and what that requires.
Normally a simulation runs as fast as it can: a one-second study finishes in whatever time the
solver needs. In a real-time simulation, one second of simulated time takes one second of wall clock
time, no faster and no slower.
When you need it
Only when something outside the simulation has its own clock. A controller running on real hardware,
another simulator you are coupled to, or a person turning a dial all move at their own pace, and the
simulation has to move with them. If nothing outside is waiting, running in real time only makes the
study slower.
This is why co-simulation and real-time usually appear together.
What it changes
Two things. The simulation waits at the end of each step until the wall clock catches up, so a run
takes as long as the time it simulates. And a step that takes longer than its own duration is an
overrun: the simulation has missed its deadline and can no longer claim to be in step with the
outside world.
Overruns are the whole difficulty. Everything else about real-time execution is arranging for them
not to happen: keeping the step’s work bounded, and keeping the operating system from interrupting
it.
Watch out: an overrun does not stop the simulation
A missed deadline is reported, not fatal. The run continues and its results remain numerically
correct; what is no longer true is that it kept pace with anything external. A run that overran
repeatedly is not a real-time run, however normal its output looks.
What it takes
A time step that comfortably exceeds the work done in it, models that do nothing slow inside the
step, and a host that will not interrupt at the wrong moment. Millisecond steps are undemanding;
microsecond steps need a tuned kernel and careful models, and are where most of the effort goes.
The requirements on the host and on the models are in
real-time execution. With that
tuning, steps as low as 5 us synchronised to an FPGA through VILLASnode have been achieved.
Running a Real-Time Simulation
Before running a simulation, you can run the following commands as root:
As a reference, real-time simulation examples are provided in the dpsim/examples/cxx and dpsim-villas/examples/cxx folder of the DPsim repository.
To benefit from the PREEMPT_RT feature and the isolated cores, the simulation has to be started using the chrt command to set the scheduling policy and priority, and the taskset command to pin the process to the isolated cores.
In the following example, we set the FIFO scheduling policy with the highest priority (99) and pin the execution of the simulation to CPU cores 9,11,13,15 which have been reserved previously (see above).
# the simple RT_DP_CS_R_1 simulationtaskset -c 9,11,13,15 chrt -f 99 build/dpsim/examples/cxx/RT_DP_CS_R_1
# Cosimulation using VILLASnode, FPGA synchronized time step, and exchanging data via Aurora interface.# Here we need sudo, to interact with the FPGA. We disable logging (log=false) and set the time step to 50 us (-t 0.00005).sudo taskset -c 9,11,13,15 chrt -f 99 build/dpsim-villas/examples/cxx/FpgaCosim3PhInfiniteBus -o log=false -t 0.00005 -d 10
1.5 - Co-simulation and Interfaces
Exchanging signals with other simulators, services or hardware during a run.
Interfaces can be used to exchange simulation signals between a DPsim simulation and other soft- or hardware, for example an MQTT-broker or an FPGA.
Simulation signals in the form of Attributes can be imported or exported once per simulation time step.
Interfaces are subclasses of Interface and implement the methods addExport and addImport, which add dependencies to the passed attribute that forward the attribute value from or to the interface.
This way, attributes that are imported are read from the interface before they are used in any DPsim component.
Attributes that are exported are written to the interface after they are set by a DPsim component.
Where the boundary is, and where to read further
This page documents only DPsim’s side of the interface: which attributes are exchanged, when they
are read and written relative to the time step, and how the simulation is synchronized. Everything
on the other side of the JSON configuration belongs to VILLASnode and is documented there rather
than here, so the two should be read together.
The parts most often needed are these:
Node types is the reference for the
type key and its per-type options. Which protocols are available, and what each one requires,
is decided here rather than in DPsim.
Nodes covers the surrounding configuration
structure that the node object sits in.
Hooks describe the processing that can be
applied to samples in transit, such as scaling, limiting or statistics. Anything that can be done
with a hook does not need to be done in the simulation, which is usually the better place for it.
Watch out: the signal mapping is positional, not by name
Two things about the split are worth knowing before configuring anything. The signal ordering in
the VILLASnode configuration must match the order in which attributes are exported and imported,
because the mapping is positional rather than by name; a mismatch produces a running simulation
exchanging the wrong quantities. And the queueless interface additionally reserves the first input
signal for a sequence number, so its signal list is offset by one relative to a queued
configuration carrying otherwise identical data. That is described with the rest of the
configuration under
interfaces.
2 - Tutorials
A ladder of worked simulations, each adding one idea to the one before.
Worked simulations in order of difficulty, starting from something trivial. Each one adds exactly
one new idea to the one before it, and each is a complete script you can run rather than a fragment
to assemble.
Every script on these pages was run before it was written up, and the numbers quoted are the numbers
it produced. Where a result is surprising, the page says so rather than leaving you to wonder
whether you typed something wrong.
Two tracks
Python is the track to start with, and the difference is not a matter of
taste. With the Python package installed you edit a script and run it; there is no build step
between a change and a result, and the loop is a few seconds long. That is the right way to learn
what the simulator does.
A C++ track covers the same ground for anyone embedding the solver in an application or writing a
new model, where the Python API is not the interface being used. It costs a compile and link on
every change, so it is the wrong place to learn the concepts and the right place to work once you
know them. It also requires a working build of DPsim itself rather than only the installed package;
see build. The
C++ examples are the
material it is being built from.
The two tracks describe the same simulator and share the concept pages behind them. Only the calling
code differs, so nothing learned in one track has to be relearned in the other.
If a tutorial is not what you want
The User Guide covers installation and individual features,
Concepts has the mathematics behind the models, the
Developer Guide covers changing DPsim rather than using it,
and Reference has the generated API and the model availability
tables.
2.1 - Python Tutorials
The ladder, worked in Python.
Each tutorial starts from the one before and adds exactly one new thing. Work through them in order;
each is a complete runnable script rather than a fragment.
Your first simulation. A source and a resistor. The shape of
a script, and how to read a result back.
Adding dynamics. An inductor, the transient it produces, and
how to choose a time step.
A rung on converters and their control belongs between the last two and is not written yet.
What you need
DPsim importable from Python. If it is not, see
install or
build.
Reading results back and plotting them uses the data processing package the example notebooks also
use. It is separate from DPsim and is imported as villas.dataprocessing.
2.1.1 - Your First Simulation
Build a network in Python, run it, and read the results back.
This page goes from nothing to a plotted result. It assumes DPsim is installed and importable; if
it is not, start with install or build.
The circuit is deliberately trivial, a voltage source feeding a resistor, so that nothing in it
distracts from the shape of the script. Every real simulation has the same five parts in the same
order.
Running it prints solver progress and writes logs/first_simulation.csv.
What each part is doing
Nodes come first because components connect to them, not to each other. SimNode.gnd is the
reference node and is shared; every network needs it. Nodes are chosen from a domain namespace,
dpsimpy.dp here, and a node from one domain cannot be connected to a component from another.
Components are created, then configured through their attributes.src.V_ref = complex(100, 0)
sets the source reference as a complex phasor, because this is the dynamic phasor domain and a
voltage is an envelope rather than an instantaneous value. In EMT the same field would carry a
different meaning; see dynamic phasors.
Watch out: connection order sets the sign
Connection order defines polarity.src.connect([gnd, n1]) means terminal 0 at ground and
terminal 1 at n1, so a positive current flows from terminal 0 to terminal 1 inside the component.
Reversing the list reverses the sign of everything that component reports. Nothing checks this for
you, and a sign error here produces a simulation that runs and is wrong.
Watch out: a component left out of the topology is ignored
The topology takes the system frequency first, then the nodes, then the components. Anything not
in those two lists is not simulated, even if it was created and connected. This is the most common
reason a component appears to have no effect.
Logging is opt-in, and takes three steps in order. Nothing is recorded unless a logger asks for
it.
logger=dpsimpy.Logger(name)# 1. create it; the name becomes the file namelogger.log_attribute("n1.v","v",n1)# 2. register each attribute you wantlogger.log_attribute("load.i_intf","i_intf",load)sim.add_logger(logger)# 3. attach it to the simulation, before run()
log_attribute takes the column name you want, the name of the attribute on the object, and the
object itself. "v" on a node is its voltage; "i_intf" on a component is the current through it.
The first argument is yours to choose and is the key you will use when reading the file back; the
second must be an attribute the object actually publishes, and print_attribute_list() on the
object shows what that is.
Watch out: only attributes can be logged
A value a component computes internally but does not publish as an attribute cannot be recorded by
any logger option. print_attribute_list() on an object shows what it publishes.
The order is what makes it work. A logger registers attributes before it is attached, and it must be
attached before run(), because the column header is written from whatever is registered when the
first row is written. A logger created but never passed to add_logger produces no file at all,
which is the usual reason for a run that appears to have logged nothing.
The keys are the column names given to log_attribute. Each value is a time series with time and
values arrays; in a dynamic phasor simulation the values are complex, and abs() gives the
envelope magnitude.
Note that the first sample is zero. The log is written before the first solve, so row zero is the
state the simulation started from rather than a result. From t = 0.001 onwards this circuit sits
at exactly 100 V and 10 A, which is what a 100 V source across 10 Ω should give.
The first argument is a figure number, so repeated calls with the same number overlay curves on one
axis. .abs() is needed for complex results; plotting a complex series directly is not meaningful.
Recovering the waveform from a dynamic phasor result
A dynamic phasor result is an envelope, not a waveform. To compare it against an instantaneous
result, shift it back onto the carrier:
Every key gains a _shift suffix, which is easy to miss and produces a KeyError that reads as
though the quantity were never logged. The result is a real waveform at the given carrier frequency,
and can be plotted or compared against an EMT run directly.
The script
The complete script for this page is 01_first_simulation.py under examples/Python/Tutorials. The numbers quoted above are the numbers it prints, so if the two ever disagree the page is the one that is wrong.
Next
This circuit has no dynamics at all: it is a source and a resistor, so it reaches its final value in
one step and stays there. The next step is to add an element that stores energy, which is where the
choice of time step starts to matter and where a result becomes worth plotting.
The examples work through larger networks, and the models used here are
derived under sources and
RLC elements.
2.1.2 - Adding Dynamics
An element that stores energy, the transient it produces, and how to choose a time step.
The circuit in your first simulation reaches its final value in
a single step. A resistor is a purely algebraic element: it stores no energy, so the circuit has no
state variable and its response to a change is instantaneous.
An inductor stores energy in its magnetic field, and its current cannot change instantaneously. That
current becomes a state variable, the circuit becomes first order, and it acquires a transient worth
looking at and a reason to care about the time step.
The circuit
A source, a resistor and an inductor in series. Only the inductor is new; everything else is the
same shape as before.
A second node appears because the resistor and the inductor meet somewhere, and that junction is a
node like any other. Components connect to nodes, never directly to each other, so a series chain of
two elements always needs the node between them.
What to expect before running it
Two numbers are worth working out first, because they are what the result should be checked against.
The steady-state current follows from the impedance at the system frequency,
and the transient decays with the time constant $\tau = L/R = 5$ ms, so the circuit settles after
roughly five of those, about 25 ms. The simulation runs for 50 ms, comfortably past that.
Getting 5.37 A at the end is the check that the circuit was built as intended. A wrong connection
order or a missing component usually shows up here rather than as an error.
Why the time step matters now
Run the same circuit twice, once at 0.1 ms and once at 5 ms, and compare the inductor current:
Time
0.1 ms step
5 ms step
5 ms
5.699 A
2.953 A
10 ms
6.105 A
6.256 A
20 ms
5.271 A
5.276 A
50 ms
5.371 A
5.366 A
At 5 ms the two disagree by nearly a factor of two. By 20 ms they agree to better than a tenth of a
percent, and both end at the right steady-state value.
Watch out: a bad step size hides in the final value
That pattern is the whole point. The coarse run is not uniformly wrong; it is wrong during the
transient and right afterwards. A step size equal to the time constant cannot resolve a change
that happens over one time constant, but it has no trouble with a value that is no longer changing.
Checking a simulation only at its final value will therefore not detect a step size that is far too
large.
The rule that follows: choose the step against the fastest thing you need to see, not against the
duration of the run or the value you expect at the end. Here the fastest thing is $\tau = 5$ ms, and
0.1 ms resolves it with room to spare.
What the values mean in this domain
The current is complex, and abs() gives the magnitude of the envelope rather than an instantaneous
current. In this domain the 50 Hz oscillation is not in the numbers at all: it has been moved into
the carrier and handled analytically, which is why a 0.1 ms step is generous here and would be
merely adequate for the same circuit solved as a waveform.
That difference is the subject of a later step. For now it is enough to know that a flat line in a
dynamic phasor result means a steady sinusoid, not a constant.
The script
The complete script for this page is 02_adding_dynamics.py under examples/Python/Tutorials. The numbers quoted above are the numbers it prints, so if the two ever disagree the page is the one that is wrong.
Next
The circuit still has one source and one branch. Next is a network with a line between two buses,
where the state the simulation starts from stops being obvious.
The elements used here are derived under
RLC elements, and the trapezoidal companion
models behind them under nodal analysis.
2.1.3 - A Network, and Where It Starts From
A line between two buses, and initializing the dynamic run from a powerflow.
The circuits so far started from nothing and settled. That is fine for a resistor and an inductor,
and useless for a network: a real system is already running when you start looking at it, and the
transient you care about is the one caused by an event, not by switching the whole grid on.
This tutorial builds a two-bus network, solves its steady state with a powerflow, and starts the
dynamic simulation from that solution.
Part one: the powerflow
A powerflow is a different kind of simulation. It has no time step in any meaningful sense; it
solves the algebraic steady state, iterating until the bus voltages are consistent with the
specified powers.
Four things here are new and none of them are optional.
The powerflow is built in the static phasor domain, dpsimpy.sp, whatever domain the dynamic
run will use. The solver is set to Solver.NRP, the Newton-Raphson powerflow solver, rather than
the default nodal solver.
modify_power_flow_bus_type is what makes the problem solvable. Every bus must declare which two of
its four quantities are known: VD fixes voltage magnitude and angle, and there must be exactly one
such bus, the slack, which absorbs whatever mismatch remains. PQ fixes active and reactive power,
which is what a load specifies. Without these the powerflow has no boundary conditions.
set_base_voltage is required on components because the solver works in per unit, and
do_init_from_nodes_and_terminals(False) tells the components not to try to initialize themselves
from node voltages that do not exist yet, since establishing those voltages is the job this
simulation is doing.
Running it gives 20 000 V at the slack and 20 075 V at the load bus. The load bus sitting above
nominal is not an error: the line’s shunt capacitance supplies more reactive power at this load than
the series impedance drops.
Part two: the dynamic run
The dynamic network is built separately, in the domain the simulation will actually use, and then
takes its initial state from the powerflow solution.
init_with_powerflow matches the two networks by node name and copies the solved voltages across,
which is why the node names must agree between the two topologies. It is the only line connecting
the two halves.
The result is the point of the whole exercise. The dynamic run starts at 20 074.8 V and ends at
20 074.9 V: it begins in steady state rather than settling into one. Without the powerflow it
would start from zero and spend the first several cycles charging the line, and any event applied
during that period would be mixed in with a startup transient that has nothing to do with the
system.
Parameter names differ between domains
The same component takes different keyword names in different domains. The static phasor line takes
R, L and C; the dynamic phasor line takes series_resistance, series_inductance and
parallel_capacitance. The load is Load with nominal_voltage in the powerflow and RXLoad with
volt in the dynamic run.
Watch out: parameter names differ between domains
This catches people, and the failure is loud rather than silent: passing the wrong keyword raises a
TypeError that lists the accepted signature. Read that list rather than guessing, and check the
generated reference when adding a component you have not used
before.
The script
The complete script for this page is 03_two_bus_network.py under examples/Python/Tutorials. The numbers quoted above are the numbers it prints, so if the two ever disagree the page is the one that is wrong.
Next
The network now starts where it should, so an event applied to it produces a clean response. Next is
applying one: a fault, using a switch.
The powerflow method is described under powerflow, and
the loads and lines used here under loads and
branches.
2.1.4 - Applying a Fault
Switching during a run, and why clearing a fault needs more care than applying one.
The network from the previous tutorial starts in steady state,
so anything that happens to it now is a response to the event rather than to startup. This tutorial
applies a fault at the load bus, clears it, and looks at what the clearing does.
Scheduling an event
A switch is an ordinary component. What makes it a fault is that its state is changed partway
through the run by an event.
fault=dpsimpy.dp.ph1.Switch("fault")fault.set_parameters(open_resistance=1e9,closed_resistance=10.0)fault.open()fault.connect([gnd,n2])# ... build the topology including `fault` ...sim.add_event(dpsimpy.event.SwitchEvent(0.1,fault,True))# applysim.add_event(dpsimpy.event.SwitchEvent(0.2,fault,False))# clear
The switch is created open and connected between the load bus and ground, so closing it puts a
10 Ω path to ground at that bus. SwitchEvent takes the time, the switch, and the state to move to:
True closes, False opens.
The switch must be in the topology’s component list like anything else. A switch that is created,
connected and given events but left out of the list produces a run with no fault and no error.
Set a time step small enough to resolve the event. At 0.1 ms the fault instant is captured within
one step; a millisecond step would smear it.
What happens
The load bus sits at 20 080 V, drops to 19 090 V while the fault is on, and recovers afterwards.
The drop is modest because a 10 Ω fault on a 20 kV bus is not a solid short and the source is stiff.
The interesting part is the instant of clearing:
Time
Bus voltage
0.1999 s
19 090 V
0.2000 s
22 684 V
0.2001 s
29 036 V
0.2002 s
33 103 V
0.2003 s
34 011 V
0.2005 s
26 726 V
0.2007 s
14 642 V
0.2009 s
9 081 V
The bus voltage rings between 34 kV and 9 kV within a millisecond, a 70% overshoot on a network that
was in steady state a moment earlier.
Why, and what to do about it
This is not the physical response of the circuit. Opening the switch asks the simulation to
interrupt the current flowing through the line inductance within one time step, and an inductor
current cannot change instantaneously. With the trapezoidal companion model the result is a
numerical oscillation that decays slowly, as explained under
switches.
A real breaker does not do this, because an arc forms across the opening contacts and dissipates the
stored energy over a short but finite interval. The variable-resistance switch reproduces that: it
raises its resistance over several steps rather than in one.
Two changes are needed, and the second is easy to forget:
fault=dpsimpy.dp.ph1.varResSwitch("fault")fault.set_parameters(open_resistance=1e9,closed_resistance=10.0)fault.open()fault.set_init_parameters(1e-4)# must match the simulation time step
Watch out: set_init_parameters must match your time step
set_init_parameters takes the time step and derives the rate at which the resistance is raised
from it. Without the call the component keeps a default rate that is correct only for a 1 ms step,
so a simulation at any other step size gets a transition of the wrong duration. Nothing warns you.
With the same fault at the same instant:
Time
Plain switch
Variable-resistance switch
0.1999 s
19 090 V
19 090 V
0.2001 s
29 036 V
19 595 V
0.2003 s
34 011 V
20 432 V
0.2006 s
20 511 V
20 943 V
0.2009 s
9 081 V
20 580 V
The oscillation is gone. The voltage rises smoothly to a 20 943 V peak, a 4% overshoot rather than
70%, and settles back to its pre-fault value.
Use the plain switch for switching that does not interrupt inductive current, and the
variable-resistance switch for faults, particularly at a machine terminal or a transformer winding.
The cost is that the system matrix changes on every step of the transition rather than once, so each
of those steps needs a refactorisation.
The script
The complete script for this page is 04_applying_a_fault.py under examples/Python/Tutorials. The numbers quoted above are the numbers it prints, so if the two ever disagree the page is the one that is wrong.
Next
The results so far have been envelopes. Next is running the same circuit as instantaneous waveforms
and comparing the two, which is where the domains stop being an abstraction.
2.1.5 - The Same Circuit in Two Domains
Running one circuit as waveforms and as envelopes, and comparing the two.
Every result so far has been an envelope, and the pages have said that an envelope is not a
waveform without showing what the difference costs. This tutorial runs the same circuit both ways
and puts the two on one axis.
The circuit is the RL branch from adding dynamics, unchanged.
Building the same circuit twice
Only the namespaces differ. dpsimpy.emt.ph1 instead of dpsimpy.dp.ph1, and Domain.EMT instead
of Domain.DP:
f_src is not the same quantity in the two domains, and this is the single easiest way to get a
wrong answer here.
Watch out: f_src means different things per domain
In EMT it is the absolute frequency of the source, so 50 Hz means 50 Hz. In DP and SP it is an
offset from the carrier, so passing 50 there gives a source at 100 Hz. Leave it at zero, or
omit it, when you want a source at the system frequency in an envelope domain.
Getting this wrong is not obvious from the output: the simulation runs, and the current is simply
smaller than it should be because the inductive reactance has doubled. In this circuit the wrong
setting gives 3.02 A instead of 5.37 A, which looks like a plausible number rather than an error.
The comparison
The dashed line is the dynamic phasor result shifted back onto the 50 Hz carrier. It lies on the EMT
waveform. The third curve is the envelope magnitude itself, which is what the DP simulation actually
computed: the smooth rise to 5.37 A that the oscillation is riding on.
Quantity
EMT
DP shifted back
Time step
50 µs
1 ms
Samples over 60 ms
1201
61
Peak current, steady state
5.3703 A
5.3603 A
Current at t = 60 ms
−2.8839 A
−2.8840 A
Twenty times fewer steps, and the same answer to four significant figures.
Why this works, and when it does not
The saving is not because the envelope model is coarser. For a single carrier the transform is
exact. The 50 Hz oscillation has been moved out of the integrated quantity and into a coefficient
handled analytically, so the step size is set by how fast the envelope changes rather than by the
carrier. Here the envelope settles with a 5 ms time constant, and 1 ms resolves it comfortably.
What an envelope domain cannot represent is content outside the band it retains around the carrier.
A harmonic, a fast switching transient, or a wideband disturbance is simply absent. That is the
trade, and it is the reason both domains exist rather than one being better.
frequency_shift_list appends _shift to every key, so the shifted series is i_l_shift and not
i_l. Asking for the original name after shifting raises a KeyError that reads like a missing
signal.
The script
The complete script for this page is 05_comparing_domains.py under examples/Python/Tutorials. The numbers quoted above are the numbers it prints, so if the two ever disagree the page is the one that is wrong.
Next
The circuits so far have been passive. Next is a synchronous machine, where the model order becomes
a choice and initialization from a powerflow stops being optional.
2.1.6 - Adding a Machine
A synchronous generator, initializing it correctly, and what the model order changes.
Everything so far has been passive. A synchronous machine brings two things that no previous
tutorial needed: it has mechanical state, so it can swing, and it has to be told the operating point
it starts from rather than deducing it.
The network is a machine feeding a strong grid through a line, with a fault applied at the machine
terminal for 100 ms.
Machine parameters
A machine is specified by operational parameters rather than winding data:
The inductances are in per unit on the machine’s own base and the time constants in seconds. H is
the inertia constant, and it sets how fast the machine can accelerate: a low H swings further for
the same disturbance.
Each model order takes a different parameter set, and the difference is exactly the states it keeps.
The third order model omits Lq_t and Tq0_t because it has no q-axis rotor state at all. The
sixth order model adds the subtransient set, Ld_s, Lq_s, Td0_s, Tq0_s and Taa. Passing the
wrong set raises a TypeError listing the accepted signature. The equations are derived under
reduced order machine models.
The machine base and the network around it
The machine parameters are per unit on the machine’s own base, while the line is given in ohms, so
the two only make sense together. The base impedance follows from the machine rating,
A line reactance of a few tenths of an ohm is therefore a few tenths per unit, which is an ordinary
transmission connection. The same line specified as 20 mH would be 7.5 Ω, above 7 per unit, and no
machine delivers rated power through that.
Watch out: an impossible operating point looks like instability
The consequence is worth knowing because it is not reported as an error. A machine asked to deliver
more power than the network can carry simply accelerates: the rotor speed climbs monotonically and
never returns, which looks like an unstable model rather than an impossible operating point.
Initializing the machine
The network is initialized from a powerflow exactly as in
the two-bus tutorial. The machine additionally needs its own
operating point:
system_dp.init_with_powerflow(systemPF=system_pf,domain=dpsimpy.Domain.DP)vterm=n1.initial_single_voltage()# from the powerflow, magnitude and anglegen.set_initial_values(init_complex_electrical_power=complex(300e6,0),init_mechanical_power=300e6,init_complex_terminal_voltage=vterm,)
Watch out: take the terminal voltage from the powerflow
Take the terminal voltage from the node rather than writing it out. It is tempting to pass
complex(24e3, 0) since that is the scheduled magnitude, but the generator bus is a PV bus and its
voltage leads the slack: here by 0.158 rad, about 9°. Supplying angle zero gives the machine a
rotor position inconsistent with the network it is connected to, and it starts by swinging into
agreement.
The difference is measurable. With the angle assumed zero, the rotor speed oscillates by ±0.33% for
the first half second, and a fault applied at 0.5 s lands on top of a transient that has nothing to
do with it. Taking the voltage from the node, the pre-fault speed is flat to 5 × 10⁻⁶ pu and the
only thing in the result is the fault.
That the mechanical power equals the scheduled electrical power is the other half of the same
condition: if they disagree, the machine accelerates or decelerates from the first step.
What the model order changes
The same fault, applied to the same machine at the same instant, with three model orders:
Model order
Peak speed
States
3rd
1.00362 pu
field winding only
4th
1.00308 pu
field and one q-axis damper
6a
1.00228 pu
transient and subtransient, both axes
All three peak at the clearing instant and all three recover, but the third order model swings
noticeably further. That is not numerical: omitting the q-axis rotor removes damping that is
physically present, so the third order machine is optimistic about how far it swings and
pessimistic about how well it settles.
The practical reading is that model order is a statement about which phenomena you intend to
capture. For a first-swing stability question the fourth order model is the usual choice. The
subtransient orders matter when the first cycles after the fault are the subject rather than the
envelope of the swing.
The script
The complete script for this page is 06_a_machine.py under examples/Python/Tutorials. The numbers quoted above are the numbers it prints, so if the two ever disagree the page is the one that is wrong.
Next
The machine is a source of energy with its own dynamics. Next is a converter, where the dynamics
are in the control rather than in a rotor.
2.1.7 - Exchanging Data With Another Tool
Handing a value out of a running simulation, and where the boundary lies.
Everything so far has ended with a CSV read after the run. This tutorial hands a value out while
the simulation runs, which is what co-simulation, hardware in the loop and any live coupling are
built on.
The far side here is a file, so nothing external has to be running. A file is a poor co-simulation
partner and an excellent first one: the mechanism is identical to an MQTT broker or an FPGA, and
only the configuration changes.
What is different about this run
sim=dpsimpy.RealTimeSimulation(name)...sim.run(1)
It is a RealTimeSimulation, not a Simulation, and run takes a start delay in seconds. An
exchange is paced by the wall clock rather than by how fast the solver can go, because the other
side is a real thing running in real time. A one-second simulation takes one second.
That also means the results carry wall-clock timestamps rather than simulation time, which is
visible in the output below.
This dictionary is not DPsim configuration. It is a VILLASnode node description, passed through
as JSON, and its keys are documented by VILLASnode rather than here. Changing type from file to
mqtt and giving a broker address is the entire difference between writing to disk and publishing
to a broker; nothing in the simulation changes.
Requires a build with VILLASnode
This tutorial needs dpsimpyvillas, a separate extension module from dpsimpy, which only exists
in a build configured with VILLASnode available. The rest of the ladder needs only the installed
Python package.
The exported quantity is an attribute, the same unit the logger works in. attr("i_intf") takes
the whole interface current, which is a matrix, and derive_coeff(0, 0) selects one element of it.
Without that you would be handing across a matrix where the far side expects a number.
Watch out: the mapping is positional, not by name
The second argument is a position, not a name. It is the index in the signal list on the
VILLASnode side, and the mapping between the two is entirely positional. Exporting two attributes in
one order and describing them in another produces a run that exchanges the wrong quantities without
any error at all, which is the failure to watch for.
What comes out
The file is written in VILLASnode’s sample format, not as a DPsim result CSV:
A wall-clock timestamp in seconds and nanoseconds, an offset, a sequence number, then one column per
exported signal. Over one second at a 10 ms step this run wrote 101 rows.
The offset column is nan here, and that is correct rather than a misconfiguration. It reports the
delay between when a sample was created and when it was received, so it only has a value on an
incoming path, where those two instants genuinely differ and the difference is the transport latency.
These samples originate in the simulation and go straight out, so there is no receive event to
measure against and the column is empty by construction.
The sequence number is what the far side uses to detect a missed sample. The queueless interface
relies on it directly, which is why it reserves the first imported signal for a sequence counter.
Reading in the other direction
import_attribute is the mirror of export_attribute and makes an incoming value drive something
in the simulation, typically the reference of a controlled source. Two options change the timing:
blockOnRead halts the simulation at the start of every step until a new value arrives, and
syncOnSimulationStart holds the whole run until the far side has produced its first value. Both
are described under co-simulation.
Without either, the simulation reads whatever arrived most recently and carries on, which is the
right behaviour when the far side is slower and the wrong one when the exchange must be lock-step.
The script
The complete script for this page is 08_exchanging_data.py under examples/Python/Tutorials. It needs a build with VILLASnode available.
Where to read further
DPsim documents its own side of the boundary: which attributes cross, and when they are read and
written relative to the step. Everything on the other side of that JSON belongs to VILLASnode and is
documented there. The co-simulation page collects
the links.
The theory of what a delay across a coupling costs is under
the ideal transformer model, which is
the same argument whether the two sides are two solvers or two machines.
3 - Developer Guide
How the simulator is built, for readers extending or debugging it.
These pages describe how DPsim is built rather than what it computes. They are the background
for adding a component, changing a solver, or working out why a simulation behaves as it does.
For the physics and the numerical methods, see concepts.
Two ideas run through the codebase and are worth reading first. Attributes expose component
parameters and state to the logger, the Python bindings and the scheduler. Tasks carry declared
attribute dependencies, and those declarations are what the scheduler uses to order and
parallelise a timestep.
3.1 - Architecture and Conventions
What DPsim is built from, and the rules the code follows.
Start here. These pages describe the shape of the library, the modules it divides into and the
class hierarchy underneath them, together with the conventions any change is expected to follow.
3.1.1 - Architecture
The modules DPsim is built from and the class hierarchy underneath them.
Modules and Dependencies
The figure below shows the main components of the DPsim library and their dependencies on other software projects.
All functionality is implemented in the C++ core, which can be used standalone or together with the Python interface.
The Python interface is a thin wrapper of the C++ core.
Jupyter notebooks can either use the DPsim Python interface to run simulations or call executables implemented in C++.
The data analysis and plotting is always done in Python using common libraries like Matplotlib.
To collect the simulation results from within Python, one can use the villas-dataprocessing Python package.
Another approach to get data in or out of DPsim is the VILLASnode interface, which does not depend on Python at all.
The main purpose of the VILLASnode interface is to exchange data during the simulation runtime, for example, in real-time simulation experiments.
The data could be send to other simulators, hardware or other software components like databases.
Storing the data in databases can be another way of managing (also offline) simulation results if the Python CSV method is not desireable.
The CIM reader is based on the CIM++ library and provides a comfortable alternative to defining the grid manually in C++ or Python.
In principle, it calls the same functions to create elements, which are also used in the C++ defined example scenarios, but automatically.
DPsim also provides a way to visualize the defined networks before simulation.
The main solver of DPsim is currently the MNA solver because it enables a rather deterministic computation time per simulation time step, which is necessary for real-time simulation.
Apart from that, it is also well established in offline circuit simulation.
The only dependency of the MNA solver is the linear algebra library Eigen.
For some component models, it is possible to use the Sundials ODE solver in combination with the MNA solver. In that case, the component is solved by the ODE solver whereas the network is still handled by the MNA solver.
A DAE solver is currently under development.
Its main purpose will be offline simulation, for example, to provide reference results where simulation runtime and real-time execution are not relevant.
The component models depend mostly on the Eigen library.
Even if components are used in combination with Sundials ODE / DAE solvers, we try to keep the specific functions required by these solvers independent of the Sundials package.
Class Hierarchy
The Simulation class holds references to instances of Interface, Solver, Logger and SystemTopology.
For a simulation scenario, the minimum description would include a SystemTopology and a solver type.
The Solver instance is then created by the Simulation.
An important function of the Simulation is to collect all tasks, which have to be executed during the simulation.
These tasks include computation steps of the individual power system component models as well as read and write tasks of the interfaces and logging variables etc.
Before the scheduling is done, Simulation calls getTasks() to retrieve the tasks from three of these classes: Solver, Interface and Logger.
The power system component and signal-model tasks are collected by the Solver instances and relayed to the Simulation, while interfaces and loggers contribute their own tasks directly.
All power system element classes inherit from the IdentifiedObject class.
This class corresponds with the IdentifiedObject of the IEC61970 CIM and has a uid and name attribute as well.
The next layer of specialization includes information on the topological connection between network elements.
An electrical bus and network nodes in general are represented by the TopologiclaNode class.
The connection of electrical components, TopologicalPowerComp, is managed via terminals of type TopologicalTerminal.
These three types describe the electrical connections of the network, which are bidirectional and include voltages and currents.
The signal type elements, TopologicalSignalComp, can only have unidirectional components, which are not expressed using node and terminals.
Instead, the attribute system is used to define signal type connections.
3.1.2 - Build
Building DPsim from source, with and without the optional features.
All builds start from a checkout of the repository. To build and read the code, cloning over
HTTPS needs no account:
git clone https://github.com/sogno-platform/dpsim.git
cd dpsim
If you intend to contribute, clone your own fork over SSH instead, since contributions are
accepted from forks only and pushing needs an authenticated remote:
git clone git@github.com:<your-user>/dpsim.git
cd dpsim
git remote add upstream https://github.com/sogno-platform/dpsim.git
The container route below is the most reproducible, because the image already carries every
dependency at the version CI uses. The native routes need those dependencies installed by hand.
Container based
The commands below use docker, but the images are ordinary OCI images, so podman works as a
drop-in replacement throughout. On Fedora and Rocky, podman is usually the one already
installed. Substitute podman for docker in every command if you prefer it.
The repository ships a development image with all required dependencies:
Alternatively, pull the prebuilt image instead of building it:
docker pull sogno/dpsim:dev
Then start an interactive session with the working copy mounted into the container:
docker run -it -p 8888:8888 -v $(pwd):/dpsim --privileged sogno/dpsim:dev bash
The -p option maps port 8888 so a JupyterLab instance inside the container is reachable from
the host. The --privileged option is required for debug builds. On Windows, the current
directory is spelled differently:
docker run -it -p 8888:8888 -v ${pwd}:/dpsim --privileged sogno/dpsim:dev bash
Inside the container, the C++ and Python libraries build as follows:
Targets that are not built by default have to be named explicitly, for example:
cmake --build . --target dpsimpy dpsimpyvillas
To build everything:
cmake --build .
Optional features are enabled through the CMake options defined in the CMakeLists.txt files,
for example:
cmake .. -DWITH_GSL=ON
To use the freshly built Python package without installing it, put both the compiled extension
and the pure Python package on the path:
cd /dpsim/build
exportPYTHONPATH=$(pwd):$(pwd)/../python/src
This is the setup most contributors work with, since it picks up a rebuild immediately without
any reinstall step.
Do not use pip install -e . for this. An editable install only links the pure Python sources;
dpsimpy is a compiled extension, so edits to the C++ are not picked up and you keep running
whatever binary was built at install time. The failure is silent, since the import still
succeeds and simply gives you stale behaviour. Either rebuild and rely on PYTHONPATH as above,
or reinstall the package after every C++ change.
To summarise the three ways to get DPsim, in increasing order of involvement: pip install dpsim
for a released Linux wheel, a native build plus PYTHONPATH for development, and make install
to place a build system wide.
If you develop inside a conda environment, the equivalent is to register the same two
directories from within the active environment. This needs conda-build installed:
cd /dpsim/build
conda develop $(pwd)&& conda develop $(pwd)/../python/src
Note that this writes into the environment, so it becomes specific to your setup.
To run JupyterLab against it:
cd /dpsim
jupyter lab --ip="0.0.0.0" --allow-root --no-browser
To install DPsim system wide instead:
cd /dpsim/build
sudo make install
CMake for Linux
The authoritative dependency list is whatever the Dockerfiles install, since that is what CI
builds against. See packaging/Docker/Dockerfile.dev for the Fedora set, and
install-fedora-deps.sh
or install-ubuntu-deps.sh
for scripts that install them.
Both libcimpp and villas-node are optional. Neither needs to be built from source, though
the images do not yet take the same route for both.
libcimpp publishes prebuilt .deb and .rpm packages per CIM version as release assets. The
Fedora and Debian images install those directly, while the Rocky image still builds it from
source:
# Pick the package matching your distribution and the CIM version you need.wget https://github.com/sogno-platform/libcimpp/releases/download/release%2Fv2.2.0/libcimpp_CGMES_2.4.15_16FEB2016-2.2.0-Linux.deb
sudo apt-get install -y ./libcimpp_CGMES_2.4.15_16FEB2016-2.2.0-Linux.deb
sudo ldconfig
VILLASnode is served from the package repositories at https://packages.fein-aachen.org, which
carry both debian/ and redhat/. Note that the images currently still build it from source,
pinned to a specific commit, so the packaged version is the more convenient route for a local
build but is not what CI exercises.
Building either from source remains supported, and the deps scripts above do that, which is what
you want when you need a specific commit rather than a release.
Sundials is only needed for the DAE solver. If your distribution does not package it, the
version CI uses is:
Cloning, building and installing then work exactly as in the container section above.
CMake for Windows
Windows is built in CI on windows-latest, so the recipe below mirrors what
.github/workflows/build_test_windows.yaml runs. You need Visual Studio with the C++ desktop
development workload, CMake and
Git for Windows. For Python support, install Python 3 and
add it to your PATH. Let CMake pick the default generator rather than naming a Visual Studio
version, so the build follows whichever Visual Studio you have.
If CMake rejects the spdlog dependency because of its minimum policy version, add
-DCMAKE_POLICY_VERSION_MINIMUM=3.5, which is what CI currently does as a workaround.
The dpsim-villas library is not available on Windows, since it requires VILLASnode, which does
not build there. WITH_VILLAS therefore stays off and the dpsimpyvillas target does not exist,
so co-simulation examples cannot be built on Windows. The CIM reader is likewise not part of the
CI Windows build, as libcimpp is not installed there.
CMake for macOS
macOS is not covered by CI, so treat this as a starting point rather than a supported path.
Install the dependencies with Homebrew:
Then build as in the container section. Building on Apple Silicon is known to fail while building
libcimpp, see issue #609. Configure with
-DWITH_CIM=OFF if you do not need the CIM reader.
Python package
Wheels are produced by cibuildwheel in the publish_to_pypi workflow, currently for
manylinux x86_64 and CPython 3.9 through 3.13. To build a source distribution locally:
python3 -m build --sdist
Nix
DPsim can be built using Nix, a declarative package manager for
reproducible builds. The following steps require a working single-user or multi-user
installation of Nix, but not necessarily NixOS.
DPsim uses the Flakes feature, which has to be enabled:
The result is placed in the result folder of the current directory. For development, a local
environment can be set up with:
nix develop github:sogno-platform/dpsim
The Flake reference above can be replaced by a local path such as . when the repository is
already checked out.
Documentation
The Python and C++ references are generated by separate CMake targets. Both are also built and
published by the documentation workflow on every push to master.
The result is generated in build/docs/sphinx/html/. Note that this target requires the Python
bindings, so it is only available when configured with -DWITH_PYBIND=ON.
The result is generated in build/docs/doxygen/html/.
Website
The surrounding website is a Hugo site under docs/hugo. It needs the Hugo version pinned in
the documentation workflow, since the theme does not build with arbitrary versions:
cd docs/hugo
npm ci
hugo --minify
3.1.3 - Coding Conventions
Scaling of quantities and logging rules that code in DPsim has to follow.
Conventions that apply across the codebase. For the process of getting a change merged, see
contributing.
This is a summary of general guidelines for the development of DPsim.
Scaling of Voltages and Currents
Voltage quantities are expressed either as phase-to-phase RMS values (denominated as RMS3PH) or as phase-to-ground peak values (denominated as PEAK1PH):
Initialisation quantities (e.g. initialSingleVoltage of SimPowerComp) as RMS3PH values
Simulation quantities in both SP and DP domain (e.g. mIntfVoltage of DP::Ph1::PiLine) as RMS3PH values
Simulation quantities in the EMT domain (e.g. mIntfVoltage of EMT::Ph3::Transformer) as PEAK1PH values
Current quantities are expressed either as RMS or as PEAK values:
Simulation quantities in both SP and DP domain (e.g. mIntfCurrent of DP::Ph1::PiLine) as RMS values
Simulation quantities in the EMT domain (e.g. mIntfCurrent of EMT::Ph3::Transformer) as PEAK values
Logging
Debug or trace should be the default log level for information that might be nice to have but not necessary for every simulation case.
Calls to the logger that might occur during simulation must use spdlog macros, like SPDLOG_LOGGER_INFO.
3.2 - Attributes and Scheduling
The attribute system, and how it decides the order everything runs in.
Attributes are the unit of state in DPsim, and they are not only a way to expose a value: the
scheduler builds the execution order from the dependencies that components declare over them. The
two subjects are one subject, which is why they sit together.
This is also what logging and the co-simulation
interfaces operate on, so a quantity that is not an attribute cannot be recorded or exchanged.
3.2.1 - Attributes
The attribute system that carries component state and drives task scheduling.
In DPsim, an attribute is a special kind of variable which usually stores a scalar or matrix value used in the simulation.
Examples for attributes are the voltage of a node, the reference current of a current source, or the left and right vectors of the MNA matrix system.
In general, attributes are instances of the Attribute<T> class, but they are usually stored and accessed through a custom smart pointer of type
const AttributeBase::Ptr (which expands to const AttributePointer<AttributeBase>).
Through the template parameter T of the Attribute<T> class, attributes can have different value types, most commonly Real, Complex, Matrix, or MatrixComp. Additionally, attributes can fall into one of two categories:
Static attributes have a fixed value which can only be changed explicitly through the attribute’s set-method or through a mutable reference obtained through get.
Dynamic attributes on the other hand can dynamically re-compute their value from other attributes every time they are read. This can for example be used to create a scalar attribute of type Real whose value always contains the magnitude of another, different attribute of type Complex.
Any simulation component or class which inherits from IdentifiedObject contains an instance of an AttributeList.
This list can be used to store all the attributes present in this component and later access them via a String instead of having to use the member variable directly.
For reasons of code clarity and runtime safety, the member variables should still be used whenever possible.
Creating and Storing Attributes
Normally, a new attribute is created by using the create or createDynamic method of an AttributeList object.
These two methods will create a new attribute of the given type and insert it into the AttributeList under the given name. After the name, create can take an additional parameter of type T which will be used as the initial value for this attribute.
Afterwards, a pointer to the attribute is returned which can then be stored in a component’s member variable. Usually this is done in the
component’s constructor in an initialization list:
/// Component class Base::Ph1::PiLine
public:// Definition of attributes
constAttribute<Real>::PtrmSeriesRes;constAttribute<Real>::PtrmSeriesInd;constAttribute<Real>::PtrmParallelCap;constAttribute<Real>::PtrmParallelCond;// Component constructor: Initializes the attributes in the initialization list
Base::Ph1::PiLine(CPS::AttributeList::PtrattributeList):mSeriesRes(attributeList->create<Real>("R_series")),mSeriesInd(attributeList->create<Real>("L_series")),mParallelCap(attributeList->create<Real>("C_parallel")),mParallelCond(attributeList->create<Real>("G_parallel")){};
When a class has no access to an AttributeList object (for example the Simulation class), attributes can instead be created through the
make methods on AttributeStatic<T> and AttributeDynamic<T>:
// Simulation class
Simulation::Simulation(Stringname,Logger::LevellogLevel):mName(AttributeStatic<String>::make(name)),mFinalTime(AttributeStatic<Real>::make(0.001)),mTimeStep(AttributeStatic<Real>::make(0.001)),mSplitSubnets(AttributeStatic<Bool>::make(true)),mSteadyStateInit(AttributeStatic<Bool>::make(false)),//...
{// ...
}
Working with Static Attributes
As stated above, the value of a static attribute can only be changed through the attribute’s set-method or by writing its value through a mutable reference obtained by calling get. This means that the value will not change between consecutive reads. Because of the performance benefits static
attributes provide over dynamic attributes, attributes should be static whenever possible.
The value of a static attribute can be read by using the attribute’s get-function (i.e. attr->get) or by applying the * operator on the already dereferenced pointer (i.e. **attr), which is overloaded to also call the get function. Both methods return a mutable reference to the attribute’s value of type T&:
In general, dynamic attributes can be accessed via the same get and set-methods described above for static attributes. However,
dynamic attributes can additionally have dependencies on other attributes which affect the behavior of these methods.
Usually, this is used to dynamically compute the attribute’s value from the value of another attribute. In the simplest case, a dynamic
attribute can be set to reference another (static or dynamic) attribute using the setReference-method. After this method has been called,
the dynamic attribute’s value will always reflect the value of the attribute it references:
When working with references between multiple dynamic attributes, the direction in which the references are defined can be important:
References should always be set in such a way that the reference relationships form a one-way chain. Only the last attribute in such a reference chain (which itself does not reference anything) should be modified by external code (i.e. through mutable references or the set-method). This ensures that changes are always reflected in all attributes in the chain. For example, the following setup might lead to errors because it overwrites an existing reference:
// Overwriting an existing reference relationship
AttributeBase::PtrA=AttributeDynamic<Real>::make();AttributeBase::PtrB=AttributeDynamic<Real>::make();AttributeBase::PtrC=AttributeDynamic<Real>::make();B->setReference(A);// Current chain: B -> A
B->setReference(C);// Current chain: B -> C, reference on A is overwritten
**C=0.1;// Change will not be reflected in A
Correct implementation:
AttributeBase::PtrA=AttributeDynamic<Real>::make();AttributeBase::PtrB=AttributeDynamic<Real>::make();AttributeBase::PtrC=AttributeDynamic<Real>::make();B->setReference(A);// Current chain: B -> A
C->setReference(B);// Current chain: C -> B -> A
**A=0.1;// Updating the last attribute in the chain will update A, B, and C
Aside from setting references, it is also possible to completely recompute a dynamic attribute’s value every time it is read. This can for example be used to create attributes which reference a single matrix coefficient of another attribute, or which represent the magnitude or phase of a complex attribute.
Dynamic attributes which depend on one other attribute in this way are also called derived attributes, and they can be created by calling one
of the various derive... methods on the original attribute:
There is also a general derive-method which can take a custom getter and setter lambda function for computing the derived attribute from its dependency.
For more complex cases involving dependencies on multiple attributes, the AttributeDynamic class has a method called addTask which can be used to add arbitrary computation tasks which are executed when the attribute is read or written to. For more information, check the method comments in Attribute.h.
Using Attributes for Logging and Interfacing
When setting up a simulation, there are some methods which require an instance of AttributeBase::Ptr as a parameter. Examples for this
are the logger methods (e.g. DataLogger::logAttribute) and interface methods (e.g. InterfaceVillas::exportAttribute). To obtain the
required attribute pointer, one can either directly access the public member variables of the component the attribute belongs to, or use the component’s attribute(String name) method which will look up the attribute in the component’s AttributeList:
autor1=DP::Ph1::Resistor::make("r_1");r1->setParameters(5);autologger=DataLogger::make("simName");// Access the attribute through the member variable
logger->logAttribute("i12",r1->mIntfCurrent);autointf=std::make_shared<InterfaceVillas>(config);// Access the attribute through the AttributeList
intf->exportAttribute(r1->attribute('i_intf'),0,true,true);// Access the attribute through the member variable and use deriveCoeff to convert it to a scalar value
intf->exportAttribute(r1->mIntfVoltage->deriveCoeff<Complex>(0,0),0,true);
When creating a simulation in Python, the component’s member variables are usually not accessible, so the attr-method has to be used for all accesses:
Attributes are also used to determine dependencies of tasks on data, which is information required by the scheduler.
For the usual MNAPreStep and MNAPostStep tasks, these dependencies are configured in the mnaAddPreStepDependencies and mnaAddPostStepDependencies methods:
Here, the MNA post step depends on the solution vector of the system, leftVector, and modifies mIntfVoltage and mIntfCurrent.
Therefore, this task needs to be scheduled after the system solution that computes leftVector and before tasks that require the voltage and current interface vectors of the inductance, e.g. the task logging these values.
3.2.2 - Attribute Usage Guidelines
When a model variable should be an attribute and when a plain member is enough.
This page gives practical rules for deciding when a model variable should be a DPsim attribute. For details on the attribute mechanism itself, see attributes.
Rule of Thumb
Use an attribute if the value must be visible to DPsim infrastructure, for example logging, interfaces, Python access, string-based lookup, or scheduling.
Otherwise, prefer a normal C++ member variable or a local variable.
Quick Decision Checklist
Before adding a new attribute, ask:
Does it need to be logged?
Does it need to be imported or exported?
Does it need to be accessed from Python or by name?
Is it used as a scheduler dependency?
Is it an externally relevant model input, output, state, or setpoint?
Is a normal C++ variable insufficient?
If the answer to all questions is no, do not make it an attribute.
Use an Attribute For
Use an attribute if the value:
should be logged
should be imported or exported through an interface
should be accessed from Python or generic code by name
is read or modified by scheduled tasks
is an externally relevant model input, output, state, or setpoint
is a derived view of another attribute, for example one matrix coefficient
Typical examples are interface voltages and currents, source references, controller setpoints, and values exchanged through VILLASnode.
Do Not Use an Attribute For
Prefer a normal C++ variable if the value:
is only used inside one method
is a temporary intermediate result
is a cached coefficient or solver helper
is a fixed implementation detail
duplicates another existing attribute
never needs logging, interface access, Python access, or scheduling
Do not create attributes for every variable in the model equations.
Choose the Simplest Attribute Type
If decided that a value should be an attribute, choose the simplest suitable attribute type.
Prefer a static attribute when the value is stored directly by the component:
When assigning a new value, prefer set() if update tasks should be triggered or if the assignment should be explicit. Direct mutable access through **attribute can be used for simple static attributes, but it does not express this intent as clearly.
Examples
Private member variable: no need to use an attribute
This value is stored as a member because it is used by several functions of the class. It is still internal to the implementation: it does not need to be logged, imported or exported, used by the scheduler, or accessed by name.
classMyComponent:publicIdentifiedObject{private:RealmConductance=0.0;// used internally by several methods
};
Externally visible output: use an attribute
This value is a model output. It may be useful for logging, plotting, interfaces, tests, or Python access, so it should be registered as an attribute.
Configuring a VILLASnode interface, and the tasks and threads behind it.
Why you would exchange data at all, and what it costs, is under
co-simulation. This page is how it is configured
and what it does to the task graph.
Choosing and configuring an interface
Requires a build with VILLASnode
This feature requires DPsim compiled with the WITH_VILLAS flag. Using the interface from Python
additionally needs the dpsimpyvillas target built alongside the normal dpsimpy package.
The VILLASnode interface is designed to make use of the various node types and protocols supported by the VILLASframework.
By utilizing the nodes provided by VILLASnode, it can be configured to import and export attributes using a wide range of protocols.
There are two interface implementations for VILLASnode: InterfaceVillas, which is queued, and
InterfaceVillasQueueless.
Watch out: only the queued interface is available from Python
dpsimpyvillas exposes InterfaceVillas and nothing else, so InterfaceVillasQueueless can only be
used from C++. A Python script needing the unbuffered path has no way to reach it today.
InterfaceVillas uses a ring buffer to store signal data between DPsim and VILLASnode to allow the protocol used in VILLASnode to operate at a different rate and non-synchronized to the DPsim time step.
InterfaceVillasQueueless uses direct communication with a VILLASnode node type implementing a specific protocol without using a buffer, thus enabling significantly lower latency communication.
With InterfaceVillasQueueless, the protocol operates at the time step of DPsim, i.e., an attribute update directly triggers a write() call to the connected VILLASnode node type.
InterfaceVillas should be used when using non- or soft real-time protocols or communication mediums, such as MQTT or connections via the internet.
InterfaceVillasQueueless should be used when communicating using reliable, low latency, real-time protocols, e.g., with FPGAs, via dedicated fibre networks, or with local real-time applications.
To create and configure one of the VILLASnode interface instance, create a new shared pointer of type InterfaceVillas or InterfaceVillasQueueless and supply it with a configuration string in the first constructor argument.
This configuration must be a valid JSON object containing the settings for the VILLASnode node type that should be used for data import and export.
This means that the JSON contains a type key describing what node type to use, as well as any additional configuration options required for this node type.
The valid configuration keys can be found in the VILLASnode documentation.
Watch out: the queueless interface reserves the first signal
The queueless interface expects the first input signal in the VILLASnode configuration to be a
sequence number incremented every time step. If it does not increase by one between consecutive
steps, an overrun is detected. Because logging can cause large delays and overruns should not be
reported spuriously, the interface only warns once a large number of them occur.
After the object is created, the exportAttribute and importAttribute methods can be used to set up the data exchange between the DPsim simulation and the configured node.
The attributes given as the first parameter to these methods are attributes belonging to components in the simulation which should be read or updated by the interface.
As an example, for exporting and importing attributes via the MQTT protocol, the VILLASnode interfaces can be configured as follows:
Using C++:
// JSON configuration adhering to the VILLASnode documentation
std::stringmqttConfig=R"STRING({"type":"mqtt","format":"json","host":"mqtt","in":{"subscribe":"/mqtt-dpsim"},"out":{"publish":"/dpsim-mqtt"}})STRING";// Creating a new InterfaceVillas object
std::shared_ptr<InterfaceVillas>intf=std::make_shared<InterfaceVillas>(mqttConfig);// Configuring the InterfaceVillas to import and export attributes
intf->importAttribute(evs->mVoltageRef,0,true,true);intf->exportAttribute(r12->mIntfCurrent->deriveCoeff<Complex>(0,0),1,true,"v_load");
Using Python:
# JSON configuration adhering to the VILLASnode documentationmqtt_config='''{
"type": "mqtt",
"format": "json",
"host": "mqtt",
"in": {
"subscribe": "/mqtt-dpsim"
},
"out": {
"publish": "/dpsim-mqtt"
}
}'''# Creating a new InterfaceVillas objectintf=dpsimpyvillas.InterfaceVillas(name='dpsim-mqtt',config=mqtt_config)# Configuring the InterfaceVillas to import and export attributesintf.import_attribute(evs.attr('V_ref'),0,True)intf.export_attribute(r12.attr('i_intf').derive_coeff(0,0),0)
Adding an Interface to the Simulation
After a new interface has been created and configured, it can be added to a simulation using the Simulation::addInterface method:
// Create and configure simulation
RealTimeSimulationsim(simName);sim.setSystem(sys);sim.setTimeStep(timeStep);sim.setFinalTime(10.0);// Create and configure interface
autointf=//...
// Add interface to simulation
sim.addInterface(intf);
Adding an interface also adds two tasks to the simulation, one before the step and one after, so an
imported value is in place before anything reads it and an exported one is sent after everything
that could change it. The transfer itself happens on separate threads, so a slow far side does not
hold up the solver. How that is arranged, and why it matters, is under
how an interface is scheduled.
Synchronizing the Simulation with the Environment
To allow for synchronizing the DPsim simulation with external services, the Interface class provides some additional configuration options in the importAttribute and exportAttribute methods. For imports, setting the blockOnRead parameter will completely halt the simulation at the start of
every time step until a new value for this attribute was read from the environment. Additionally, the syncOnSimulationStart parameter can be set for every
import to indicate that this attribute is used to synchronize the start of the simulation. When a simulation contains any interfaces importing attributes
which have syncOnSimulationStart set, the Simulation::sync will be called before the first time step. This method will:
write out all attributes configured for export to the environment
block until all attributes with syncOnSimulationStart set have been read from the environment at least once
write out all exported attributes again
Note that this setting operates independently of the blockOnRead flag. This means that with both flags set, the simulation will block again after the synchronization at the start of the first time step until another value is received for the attribute in question.
The two tasks
Adding an interface adds a PreStep and a PostStep task.
PreStep is declared to modify every attribute imported from the environment, so the scheduler
places it before any task that depends on those attributes. An imported value is therefore in place
before anything reads it.
PostStep is declared to depend on every attribute exported to the environment, so it runs after
anything that might modify them.
Why PostStep declares a modified attribute
PostStep modifies nothing in the simulation: it only sends values outward. The scheduler prunes
tasks whose outputs nothing needs, so a task that modifies nothing is dropped, and the export would
silently never happen.
To prevent that, PostStep is declared to modify Scheduler::external. That attribute exists to
make a task reachable when its real effect is outside the simulation.
Watch out: a task that modifies nothing is pruned
This is the general rule, not a quirk of interfaces. The scheduler keeps a task only if something
needs what it produces, so any task whose effect leaves the simulation must declare a modified
attribute or it will be dropped without warning. The same mechanism explains why logging an
attribute can change which tasks run; see
adding tasks to a component.
Task execution is not the moment of transfer
When these tasks execute is not when the data actually crosses the boundary. The interface spawns a
reader thread and a writer thread and communicates with them over a lock-free queue.
The consequence is the useful part: a slow import or export does not block the solver. The simulation
hands a value to the queue and continues. That is what makes an interface to a slow or unreliable
far side usable at all, and it is also why a value read this step may have been produced some time
ago.
Blocking is opt-in through blockOnRead and syncOnSimulationStart on the import, described on the
co-simulation page. Those are the only ways the exchange paces the simulation.
3.2.4 - Task Scheduling
How DPsim builds, orders, and executes the task graph each timestep.
Within each simulation timestep, DPsim executes a set of tasks: discrete units of computation contributed by components, the solver, interfaces, and loggers.
Before the first timestep the scheduler collects all tasks, resolves their data dependencies into a directed acyclic graph, and produces an ordered schedule.
That schedule is then replayed on every timestep with no further graph analysis.
Tasks
The Task base class
Every task is an instance of a class that inherits from CPS::Task
(dpsim-models/include/dpsim-models/Task.h).
Each subclass implements one member function:
virtualvoidexecute(Realtime,InttimeStepCount)=0;
To participate in scheduling, a task declares its data dependencies through three attribute lists that are populated in the task’s constructor:
List
Meaning
mAttributeDependencies
Attributes this task reads in execute()
mModifiedAttributes
Attributes this task writes in execute()
mPrevStepDependencies
Attributes whose value from the previous timestep this task needs
All three lists hold AttributeBase::Ptr objects, the same pointers used throughout the component model.
See Attributes for details on the attribute system.
Watch out: only attributes can leave a component
Only attributes can participate in scheduling. Plain C++ member variables, a Real, a Matrix or
an internal state struct, are invisible to the scheduler, so no dependency edge can be formed around
them.
The same constraint governs recording and exchange: DataLogger and RealTimeDataLogger both
implement DataLoggerInterface, whose logAttribute() accepts only an AttributeBase::Ptr, and the
VILLASnode interface works the same way.
So any value that must cross a task boundary, be written to a result file, or be exchanged with
another tool has to be stored in an Attribute<T>. Deciding that late means changing the component
rather than the call site.
The component text logger (CPS::Logger, backed by spdlog) is a separate mechanism used for human-readable debug and diagnostic output.
It is not part of the scheduling system and can print any value regardless of whether it is an attribute.
For practical rules on when a variable should be an attribute versus a plain member variable, see Attribute Usage Guidelines.
Common component task conventions
The names below are component and solver conventions, not scheduler-level concepts.
The scheduler only sees the attribute dependencies a task declares; it has no notion of a “PreStep” or “PostStep” and never orders tasks by these names.
MNA components typically define two task classes per component:
Task
Typical responsibility
MnaPreStep
Component-specific preparation before the matrix solve, often updating internal state and stamping the right-hand-side contribution
MnaPostStep
Component-specific update after the matrix solve, often reading the solution vector to update interface voltages and currents
This is a common pattern rather than a fixed rule; the exact work each task does is component-specific.
Signal-domain components (regulators, governors, control blocks) define their own task list via getTasks(); many separate previous-step state handling from output updates, for example a PreStep that copies state from the previous step and a Step that updates the block outputs.
The solver itself contributes a task that solves the MNA system; individual components do not depend on it by name, they depend on leftVector instead (see below).
Building the schedule
Task collection
Simulation::prepSchedule() collects all tasks before the first timestep from three top-level sources:
Solvers: each solver contributes its task list via Solver::getTasks(). For MNA solvers this list bundles:
the matrix-solve task,
MNA component pre-/post-step tasks from MNASimPowerComp::mnaTasks() (built during solver initialization via mnaAddPreStepDependencies() / mnaAddPostStepDependencies()),
signal-domain component tasks returned by SimSignalComp::getTasks(),
optional solver-side tasks, such as state-space extraction, when enabled.
Interfaces: each interface contributes its own tasks via Interface::getTasks(). These typically depend on the attributes exchanged with external systems.
Loggers: each logger contributes a logging task via Logger::getTask(), depending on the logged attributes so values are written after the producing tasks have run.
All tasks are placed in a flat Task::List and handed to the scheduler.
Dependency resolution
Scheduler::resolveDeps() (dpsim/src/Scheduler.cpp) translates the attribute-level declarations into directed edges between tasks.
For every attribute in mModifiedAttributes, it finds all tasks that list that attribute in their mAttributeDependencies and adds an edge:
graph LR
A["Task A modifies attr_X"] -->|attr_X| B["Task B depends on attr_X"]
Task A modifies attr_X and task B depends on it, so the edge runs A to B and the scheduler must
place A first.
A special Root sentinel task is inserted as a sink for all mPrevStepDependencies entries.
Its role is explained in the pruning step below.
Topological sort and pruning
Scheduler::topologicalSort() first runs a backward breadth-first search (BFS) from Root, marking every task that transitively contributes to a simulation output.
Tasks not reachable in this pass are dropped from the schedule because they produce data no downstream consumer reads in the current timestep.
Kahn’s algorithm then processes the remaining tasks in dependency order and appends them to the schedule.
The result is a flat, ordered list in which every task appears after all of its current-step predecessors.
The Root sentinel matters here: it holds a reference to an external attribute updated by an interface or by the solver, so the backward BFS reaches it and keeps every task that writes previous-timestep state, even when that output is only consumed in the next timestep.
Level scheduling
For parallel execution the ordered list is converted into levels by Scheduler::levelSchedule().
Each task is assigned to the level one greater than the highest-level task it depends on:
graph TD
subgraph L0["level 0: no dependencies, all start at once"]
T1; T2; T3
end
subgraph L1["level 1: depend only on level 0"]
T4; T5
end
subgraph L2["level 2"]
T6
end
T1 --> T4
T2 --> T4
T3 --> T5
T4 --> T6
T5 --> T6
Tasks within the same level have no data dependencies between them and can execute in parallel.
The scheduler guarantees that all tasks in level k finish before any task in level k+1 starts.
Scheduler variants
Class
Parallelism strategy
SequentialScheduler
Single-threaded; follows topological order
ThreadLevelScheduler
Distributes each level across N worker threads
ThreadListScheduler
Distributes tasks greedily across N threads
OpenMPLevelScheduler
Uses #pragma omp parallel for per level
The scheduler is chosen at Simulation construction time; SequentialScheduler is the default.
Per-timestep execution
Scheduler::step(time, timeStepCount) is called once per timestep.
For the sequential scheduler:
Parallel schedulers distribute tasks across threads within each level and synchronize with a barrier before advancing to the next level.
This page describes how the scheduler works. For how to give a component tasks of its own, see
adding tasks to a component.
3.2.5 - Adding Tasks to a Component
Giving a component pre-step and post-step tasks and declaring their dependencies.
How to attach tasks to a component. For how the scheduler consumes them, see
scheduling.
Signal components
Signal components inherit from SimSignalComp and return their tasks from getTasks().
The usual pattern is to define inner Task classes whose constructors populate the dependency lists, then instantiate them in getTasks():
classMyComponent:publicSimSignalComp{public:constAttribute<Real>::PtrmInput;// written by upstream component
constAttribute<Real>::PtrmOutput;// read by downstream component
constAttribute<Real>::PtrmOutputPrev;// state carried across timesteps
classPreStep:publicTask{public:explicitPreStep(MyComponent&comp):Task(**comp.mName+".PreStep"),mComp(comp){mPrevStepDependencies.push_back(mComp.mOutput);mModifiedAttributes.push_back(mComp.mOutputPrev);}voidexecute(Realtime,InttimeStepCount)override{**mComp.mOutputPrev=**mComp.mOutput;}private:MyComponent&mComp;};classStep:publicTask{public:explicitStep(MyComponent&comp):Task(**comp.mName+".Step"),mComp(comp){mAttributeDependencies.push_back(mComp.mInput);mModifiedAttributes.push_back(mComp.mOutput);}voidexecute(Realtime,InttimeStepCount)override{mComp.signalStep(time,timeStepCount);}private:MyComponent&mComp;};Task::ListgetTasks()override{return{std::make_shared<PreStep>(*this),std::make_shared<Step>(*this)};}};
PreStep uses mPrevStepDependencies for mOutput because it reads the value produced last timestep, not the value that Step will produce this timestep.
Using mAttributeDependencies here would create a same-step dependency on Step and force PreStep after Step, which is backwards.
MNA power components
MNA components inherit from MNASimPowerComp<VarType>.
Instead of getTasks(), they implement two hook functions that MNASimPowerComp calls when it builds the MnaPreStep and MnaPostStep tasks during solver initialization.
voidDP::Ph1::MyComponent::mnaAddPreStepDependencies(AttributeBase::List&prevStepDependencies,AttributeBase::List&attributeDependencies,AttributeBase::List&modifiedAttributes){prevStepDependencies.push_back(mIntfCurrent);// read from previous step
modifiedAttributes.push_back(mRightVector);// stamp right-hand side
}voidDP::Ph1::MyComponent::mnaAddPostStepDependencies(AttributeBase::List&prevStepDependencies,AttributeBase::List&attributeDependencies,AttributeBase::List&modifiedAttributes,Attribute<Matrix>::Ptr&leftVector){attributeDependencies.push_back(leftVector);// wait for matrix solve
modifiedAttributes.push_back(mIntfVoltage);modifiedAttributes.push_back(mIntfCurrent);}
PostStep must always list leftVector in attributeDependencies.
This creates the edge from the solver’s matrix-solve task to every component’s PostStep, ensuring the solution vector is available before voltages and currents are extracted.
Dependency declaration checklist
Every attribute read inside execute() must appear in mAttributeDependencies or mPrevStepDependencies.
Every attribute written inside execute() must appear in mModifiedAttributes.
State carried from the previous timestep goes in mPrevStepDependencies, not mAttributeDependencies.
MnaPostStep must list leftVector in attributeDependencies.
No attribute should appear in both mAttributeDependencies and mPrevStepDependencies for the same task.
Watch out: a missing declaration produces wrong results, not a crash
Missing a declaration does not always cause a crash; it silently produces incorrect results or a wrong execution order, which is harder to debug.
Two common failure modes follow from the pruning step:
A PreStep or PostStep task is dropped entirely because none of its declared modified attributes is needed by another task, a logger, an interface, or a previous-step dependency. The simulation then runs but its results are always wrong.
The same task appears to work only when a particular variable is logged or exchanged by an interface, because that logger or interface adds a dependency on the attribute and keeps the producing task reachable. The results then depend on logger or interface configuration even though the physical model did not change.
Declare dependencies conservatively.
3.3 - Writing a Model
Adding a component, interfacing it with the solver, and finding out why it is wrong.
The path from an empty file to a working component: what to declare, which hooks the solver calls
and in what order, how a component built from other components is assembled, and how to debug one
that runs but produces the wrong answer.
For the equations a model should implement, see Concepts.
For worked examples of finished models, see
model implementations.
3.3.1 - Component and Solver Initialization
How DPsim initializes components and solvers before the first simulation timestep.
Initialization is the phase between constructing the system topology and running the first timestep.
Its job is to size the system matrices, derive initial state from power-flow results, register MNA tasks, and stamp static conductances.
Two constraints drive its structure:
The system matrix size depends on the total number of simulation nodes, including virtual nodes declared by composite components and their sub-components. All virtual nodes must therefore be known before the matrices are allocated.
Component parameter values (impedances, initial phasors) depend on terminal voltages and powers, which are only available after a power-flow solve.
These two constraints impose an ordering that is captured in the solver’s initialization sequence.
MNA Solver Initialization Sequence
MnaSolver::initialize() executes the following steps in order.
flowchart TD
start([Simulation::run]) --> init[MnaSolver::initialize]
init --> s1["S1: identifyTopologyObjects()\nSort into mMNAComponents,\nmSimSignalComps, ..."]
s1 --> s2["S2: createSubComponents() pre-pass\nRecursively instantiate sub-components\nso all virtual nodes exist"]
s2 --> s3["S3: collectVirtualNodes()\nassignMatrixNodeIndices()\nMatrix size is now fixed"]
s3 --> s4["S4: createEmptyVectors()\ncreateEmptySystemMatrix()"]
s4 --> s5a["S5a: initializeFromNodesAndTerminals(freq)\nfor each SimPowerComp"]
s5a --> s5b["S5b: initialize(omega, dt)\nfor each SimSignalComp"]
s5b --> s5c["S5c: mnaInitialize(omega, dt, v)\nfor each MNAInterface component"]
s5c --> cond{mSteadyStateInit?}
cond -- yes --> s6["S6: steadyStateInitialization()\nIterate MNA until phasors converge"]
s6 --> s7
cond -- no --> s7["S7: setBehaviour(MNASimulation)\non all components"]
s7 --> s8["S8: initializeSystem()\nStamp static elements,\ncompute LU factorizations"]
s8 --> done([Ready for timesteps])
Step 1 — Identify topology objects
identifyTopologyObjects() iterates over SystemTopology::mComponents and sorts each component into one of four lists:
List
Contents
mMNAComponents
Static MNA power components
mMNAIntfVariableComps
Variable-stamp MNA components (e.g. under MNAVariableCompInterface)
mMNAIntfSwitches
Components with a switch interface
mSimSignalComps
Signal components (SimSignalComp)
Ground nodes are excluded here.
Step 2 — Create sub-components (pre-pass)
Before the matrix can be sized, every composite component’s sub-component tree must be fully instantiated so that all virtual nodes are visible.
The solver calls createSubComponents() recursively on every MNA component:
Only sub-components newly registered by this call are recursed into, because eagerly-constructed sub-components (created in the constructor before connect() has run) are not yet safe to recurse into.
This step is a pre-pass only — it must not set parameter values derived from terminal data or frequency.
For details on the three-stage composite lifecycle (createSubComponents, initializeParentFromNodesAndTerminals, mnaCompInitialize), see Subcomponent Handling.
Step 3 — Collect virtual nodes and assign indices
collectVirtualNodes() visits every component and calls virtualNodes() to collect all virtual SimNode objects, then appends them to the solver’s node list.
assignMatrixNodeIndices() then assigns a contiguous integer index to every simulation node (real and virtual), which determines the row/column layout of the system matrices.
After this step the matrix size is fixed.
Step 4 — Allocate empty matrices
createEmptyVectors() and createEmptySystemMatrix() allocate the left-side vector, right-side vector, system matrix (dense or sparse depending on the solver variant), and switch-variant copies.
For sparse solvers, mBaseSystemMatrix and mLuFactorizations are also allocated here, with one variant per switch combination.
If mInitFromNodesAndTerminals is set (the default), initializeFromNodesAndTerminals(mSystem.mSystemFrequency) is called.
This is where components read their terminal voltages and powers and derive physical parameters (impedances, initial phasor values, per-unit quantities).
For composite components initializeFromNodesAndTerminals() is final in CompositePowerComp and sequences the three lifecycle stages automatically; non-composite power components override it directly.
5b — Signal components: initialize(omega, timeStep)
Each SimSignalComp in mSimSignalComps receives initialize(mSystem.mSystemOmega, mTimeStep).
This is the hook for signal-domain components (regulators, governors, PSS blocks) to allocate their state buffers, set initial values, and wire up attribute connections.
Watch out: do not name a hook initialize(Real)
Do not use initialize(Real) or initialize(Real, Real) as a user-facing initialization hook
for power components. Those signatures match the solver’s signal-component hook, so the solver
calls them rather than the component author’s intent. Use initializeFromNodesAndTerminals()
or a named method such as initializeStates() instead.
5c — MNA components: mnaInitialize
Each MNA component (including switches) receives mnaInitialize(omega, timeStep, leftVector).
In MNASimPowerComp this method:
Clears and re-registers MNAPreStep / MNAPostStep tasks according to the hasPreStep / hasPostStep flags.
Initializes mRightVector to zero with the correct size.
Calls mnaCompInitialize(omega, timeStep, leftVector) on the component.
In mnaCompInitialize, component classes call updateMatrixNodeIndices() and perform any one-time MNA setup that requires the final node layout (e.g. allocating per-component history vectors sized to the system).
Nodes are initialized last via SimNode::initialize(), which zeros the node voltage.
Step 6 — Optional steady-state initialization
If mSteadyStateInit is set, steadyStateInitialization() iterates the MNA solve until the phasor solution converges.
The flag mIsInInitialization is set to true for this sub-phase so that components can distinguish initialization solves from simulation solves via mBehaviour (see below).
Step 7 — Set simulation behaviour
After initialization solves are complete, the solver calls setBehaviour(TopologicalPowerComp::Behaviour::MNASimulation) on every TopologicalPowerComp and setBehaviour(SimSignalComp::Behaviour::Simulation) on every SimSignalComp.
The Behaviour enum (defined in TopologicalPowerComp) has three values:
Value
When active
Typical use
Behaviour::Initialization
During PF steady-state init pass
Components may disable transient update equations
Behaviour::PFSimulation
During PFSolver run
Activates power-flow-specific stamping
Behaviour::MNASimulation
After initialize() completes
Normal simulation; components should be in their run-time mode
Components that need different behaviour between initialization and simulation check mBehaviour in their pre/post-step methods or in mnaCompPreStep.
Step 8 — Initialize system matrices (initializeSystem)
initializeSystem() selects one of three paths:
Parallel frequencies (initializeSystemWithParallelFrequencies): stamps each frequency into a separate thread.
Variable matrix (initializeSystemWithVariableMatrix): used by MnaSolverSysRecomp; saves static switch matrices as base matrices and adds variable elements on top.
Precomputed matrices (initializeSystemWithPrecomputedMatrices): the common path. Calls switchedMatrixStamp() for each switch combination, which iterates over all static MNA components and calls mnaApplySystemMatrixStamp() and mnaApplyRightSideVectorStamp(). LU factorizations are computed for each variant.
After this step the solver is ready to execute timesteps.
Component Class Hierarchy and Init Hooks
The following diagram shows which initialization methods live in which class, and the override points for component authors.
classDiagram
class TopologicalPowerComp {
+Behaviour mBehaviour
+setBehaviour(b)
}
class SimPowerComp~T~ {
+initialize(Matrix frequencies)
+initializeFromNodesAndTerminals(Real freq)
+virtualNodes()
}
class MNASimPowerComp~T~ {
+mnaInitialize(omega, dt, v) final
+mnaCompInitialize(omega, dt, v)*
+mnaCompApplySystemMatrixStamp()*
+mnaCompPreStep()*
+mnaCompPostStep()*
}
class CompositePowerComp~T~ {
+createSubComponents()*
+initializeFromNodesAndTerminals(freq) final
+initializeParentFromNodesAndTerminals(freq)*
+mnaParentInitialize(omega, dt, v)*
+mnaParentPreStep()*
+mnaParentPostStep()*
}
class SimSignalComp {
+initialize(Real omega, Real dt)*
}
TopologicalPowerComp <|-- SimPowerComp
SimPowerComp <|-- MNASimPowerComp
MNASimPowerComp <|-- CompositePowerComp
Methods marked * are the virtual override points for component authors.
Methods marked final must not be overridden; the base class sequences them correctly.
Component Method Contracts
The table below summarizes which initialization method has which responsibilities. A tick means the operation belongs in that method; a cross means it must not appear there.
Responsibility
Constructor / setParameters
createSubComponents
initializeFromNodesAndTerminals
mnaCompInitialize
Declare virtual node count
✓
—
—
—
Allocate sub-component objects
—
✓
—
—
connect() sub-components to virtual nodes
—
✓
—
—
addMNASubComponent() registration
—
✓
—
—
Read terminal voltage / power
✗
✗
✓
—
Read system frequency
✗
✗
✓ (via argument)
✓ (via omega)
Compute impedance / admittance
—
✗
✓
—
Call setParameters() on sub-components
—
—
✓
—
Call updateMatrixNodeIndices()
—
—
—
✓
Allocate per-step vectors (history, right vector)
—
—
—
✓
Register MNA tasks (handled by base class)
—
—
—
✓ (via mnaCompInitialize)
Common pitfalls
Accessing terminals in the constructor or createSubComponents: terminal data (initial voltage, connected power) is not yet populated. The topology is set up but power-flow has not run.
Accessing mFrequencies(0,0) in createSubComponents: the system frequency matrix is set on SimPowerComp via initialize(Matrix) which only runs later. Use the frequency argument passed to initializeParentFromNodesAndTerminals or the omega argument in mnaCompInitialize.
Zero-valued shunt branches: a capacitor or reactor with zero admittance injects a zero row/column into the system matrix, which makes the LU factorization singular. Guard with a strict > 0 check and omit the branch rather than inserting a zero stamp.
Watch out: virtual nodes must exist before the solver collects them
A virtual node created for the first time aftercollectVirtualNodes() (step 3) never gets a
matrix index, and the solver then crashes or silently produces wrong results. Declare every virtual
node in the constructor or in setParameters().
Composite Component Initialization Sequence
The following diagram shows how the solver and a composite component interact during initialization. For further details see Subcomponent Handling.
PFSolver::initialize() follows a simpler sequence because it operates only on single-phase SP components with no sub-component tree and does not need a createSubComponents pre-pass.
PFSolver::setSolverAndComponentBehaviour() is the equivalent of Step 7 for the MNA solver: it calls setBehaviour(Behaviour::PFSimulation) or setBehaviour(Behaviour::Initialization) on all components to allow them to switch stamping modes.
Known Design Issues (issue #59)
The following areas were identified in GitHub issue #59 as needing improvement.
SimPowerComp<T>::initialize(Matrix frequencies) is called by the solver to propagate frequency information down the component tree.
Watch out: overriding this hook makes you responsible for the base call
It is not a hook for component authors — a component that overrides it takes over responsibility for calling the base class version, which is easy to forget.
The recommended path is:
For power components, use initializeFromNodesAndTerminals() or initializeParentFromNodesAndTerminals().
For signal components, use the initialize(Real omega, Real timeStep) hook provided by SimSignalComp.
For anything else (e.g. setting up state-space matrices), add a named helper called from one of the above.
The base implementation of SimPowerComp::initialize(Matrix) should be renamed to something that cannot be accidentally overridden (e.g. propagateFrequencies()), and an override guard should be added to catch accidental overrides.
Sub-component construction in constructors
Some components create and register sub-components eagerly in their constructor before connect() has been called on those sub-components.
This works today because the solver’s createSubComponents pre-pass skips already-registered sub-components, but it couples topology creation to object construction and makes components harder to reason about.
The long-term goal is to migrate all sub-component construction to createSubComponents(), giving a clear rule: the constructor only allocates and the topology stage wires.
Signal component initialize not sequenced with power flow
Signal components receive initialize(omega, timeStep)afterinitializeFromNodesAndTerminals on power components but before the MNA tasks are registered.
If a signal component’s initial state depends on the power-flow solution (e.g. an exciter initializing to match the generator terminal voltage), it must read the relevant attribute values directly — there is no formal mechanism today to express this dependency in the initialization sequence.
A future improvement would be to give signal components access to the settled power-flow solution before their initialize is called.
3.3.2 - Real-Time Execution
Tuning the host and writing a model that can hold a deadline.
Why you would run in real time, and how to start such a run, is under
real-time simulation. This page is what has to be
true of the host and of the models for a deadline to be met.
DPsim runs in real time on any system, but without tuning the smallest reliable step is nowhere near
microseconds, because operating system noise and other processes interfere. With the tuning below,
steps as low as 5 us synchronised to an FPGA through VILLASnode have been achieved.
Operating System and Kernel
A kernel built with PREEMPT_RT improves latency when issuing system calls and enables the FIFO
scheduler that avoids preemption during the run.
This used to mean tracking down an out-of-tree patch set. It no longer does: PREEMPT_RT was merged
into the mainline Linux kernel in 6.12, so a recent kernel can be built with it directly and a
growing number of distributions ship or package one. Check what you already have before installing
anything:
More aggressive tuning can involve isolating a set of cores for exclusive use by the real-time simulation.
This way, the kernel will not schedule any processes on these cores.
Add the kernel parameters isolcpus and nohz_full using, for example, grubby:
Real time capable models cannot issue any system calls during simulation as the context switch to the kernel introduces unacceptable latencies.
This means models cannot allocate memory, use mutexes or other interrupt-driven synchronization primitives, read or write data from files.
You should turn off logging, when time steps in the low milliseconds are desired.
There is a RealTimeDataLogger that can be used to output simulation results in these cases.
Note however, that this logger pre-allocated the memory required for all of the logging required during simulations.
Your machine may run out of memory, when the simulation is long or you log too many signals.
You can increase the performance of your simulation by adding the -flto and -march=native compiler flags:
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 8801cbe8d..4a2843269 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -79,7 +79,7 @@ include(CheckSymbolExists)
check_symbol_exists(timerfd_create sys/timerfd.h HAVE_TIMERFD)
check_symbol_exists(getopt_long getopt.h HAVE_GETOPT)
if(CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")
- add_compile_options(-Ofast)
+ add_compile_options(-Ofast -flto -march=native)
endif()
# Get version info and buildid from Git
Where the time step comes from
By default the simulation paces itself against the host clock, which is enough for almost everything.
Synchronising the step to an external source instead is only necessary when the accuracy of the step
itself matters at the nanosecond level, which in practice means hardware in the loop against
equipment with its own clock.
That distinction is worth making before reaching for it: locking to an external source constrains the
whole run and is not a general improvement, only the answer to a specific requirement.
Writing a model that can hold a deadline
A real-time capable model must issue no system calls during simulation: the context switch into the
kernel costs more than the deadline allows.
Watch out: a single allocation can miss a deadline
No allocating memory, no mutexes or other interrupt-driven synchronisation, and no reading or
writing files inside the step. Any one of these can block for longer than the step, and the failure
appears as an occasional overrun rather than as an error, so it is easy to miss in a short test run.
Turn logging off when the step is in the low milliseconds. RealTimeDataLogger exists for the cases
that still need results: it buffers in memory and writes at the end rather than touching the disk
inside the step.
Watch out: the real-time logger preallocates everything
RealTimeDataLogger allocates the memory for the entire run up front, which is what keeps it off the
critical path. A long run, or too many logged attributes, can therefore exhaust memory before the
simulation starts. See loggers.
3.3.3 - Interfacing with the MNA Solver
The hooks a component implements to take part in the nodal solve.
The various solver classes based on MNASolver are used to perform Nodal Analysis during a DPsim simulation. For components to be able to influence the input variables of the MNA, they have to implement certain methods defined in the MNAInterface interface class. While it is possible to individually implement MNAInterface for every
component, the behavior of many components can be unified in a common base class. This base class is called MNASimPowerComp<T>.
Currently, it is the only class which directly implements MNAInterface and in turn all MNA components inherit from this class.
Much like the CompositePowerComp class for Composite Components, the MNASimPowerComp class
provides some common behavior for all MNA components, e.g. the creation and registration of the MNAPreStep and MNAPostStep tasks.
Additionally, MNASimPowerComp provides a set of virtual methods prefixed mnaComp... which can be implemented by the child component classes to provide their own MNA behavior. These methods are:
MNASimPowerComp provides empty default implementations for all of these methods, so component classes are not forced to implement any of them.
Controlling Common Base Class Behavior
Child component classes can control the behavior of the base class through the constructor arguments of MNASimPowerComp.
The two boolean variables hasPreStep and hasPostStep can be used to control whether the MNAPreStep and MNAPostStep tasks will be created and registered.
If these tasks are created, the mnaCompPreStep / mnaCompPostStep and mnaCompAddPreStepDependencies / mnaCompAddPostStepDependencies methods will be called during the component’s lifecycle.
If the tasks are not created, these methods are superfluous and should not be implemented in the child class.
Currently, the MNASimPowerComp base class only exhibits additional behavior over the mnaComp... methods in the mnaInitialize method. In this method, the list of MNA tasks is cleared, and the new tasks are added according to the hasPreStep and hasPostStep parameters. Additionally, the right vector attribute mRightVector required by MNAInterface is set to a zero-vector with its length equal to that of the system leftVector.
If this behavior is not desired, e.g. for resistors which have no influence on the system right vector, the right vector can be re-set to have zero size in the mnaCompInitialize method:
For all other MNA methods, the MNASimPowerComp base class will just call the associated mnaComp... method. For more details, take a look at the implementations in MNASimPowerComp.cpp.
3.3.4 - Subcomponent Handling
Building a component out of other components with CompositePowerComp.
In DPsim, there are many components which can be broken down into individual subcomponents. Examples are the PiLine, consisting of an inductor, three resistors, and two capacitors, or the NetworkInjection which contains a voltage source.
On the C++ class level, these subcomponents are represented by member variables within the larger component class. In this guide, all components which have subcomponents are called composite components.
Creating Composite Components
While normal components are usually subclasses of SimPowerComp<T> or MNASimPowerComp<T>, there exists a special base class for composite
components called CompositePowerComp<T>. This class provides multiple methods and parameters for configuring how the subcomponents should be
handled with respect to the MNAPreStep and MNAPostStep tasks.
The main idea here is that the subcomponents do not register their own MNA tasks, but instead their MNA methods like mnaPreStep and mnaPostStep are called explicitly in the tasks of the composite component.
In the constructor of CompositePowerComp<T>, the parameters hasPreStep and hasPostStep can
be set to automatically create and register a MNAPreStep or MNAPostStep task that will call the mnaCompPreStep or mnaCompPostStep method on execution.
Additionally, all subcomponents should be registered as soon as they are created using the addMNASubComponent-method. This method takes
multiple parameters defining how and in what order the subcomponent’s pre- and post- steps should be called, as well as if the subcomponent
should be stamped into the system rightVector.
Initialization lifecycle
Composite components are initialized in three stages, each with a defined role. (These are distinct from the electrical phases A/B/C of a three-phase component.)
Topology stage (createSubComponents()). Decides which sub-components exist and how they are wired: make_shared, connect() to network/virtual nodes, and addMNASubComponent(). This runs in a pre-pass before the MNA system matrix is sized, so any virtual nodes owned by sub-components are visible to collectVirtualNodes(). Because it runs before power-flow results or the simulation frequency are guaranteed to be available, createSubComponents() must not read terminal data (initialSingleVoltage(), singleActivePower(), …), system frequency (mFrequencies(0,0)), or compute any power-/impedance-derived value. It must be idempotent — guard the body with mSubCompCreated (a protected field inherited from CompositePowerComp).
Parameterization stage (initializeParentFromNodesAndTerminals(Real frequency)). Sets the values the sub-components created in stage 1 will use. This is where terminal reads, frequency-dependent impedance/admittance calculations, and setParameters() calls on sub-components belong. The simulation frequency is passed in as a direct argument, so there is no need to access mFrequencies(0,0). This is the hook concrete composites must implement — do not override initializeFromNodesAndTerminals() directly; the base class owns that method and calls this hook at the right time.
MNA-init stage (mnaCompInitialize()). Unchanged; already recurses into sub-components.
CompositePowerComp<VarType>::initializeFromNodesAndTerminals() is final and sequences these stages:
voidinitializeFromNodesAndTerminals(Realfrequency)final{createSubComponents();// idempotent safety net for paths
// that reach this composite without
// the solver's pre-pass having run
initializeParentFromNodesAndTerminals(frequency);// parent derives values,
// setParameters() on subs
for(autosubComp:mSubComponents){subComp->initialize(mFrequencies);// propagate frequencies down
subComp->initializeFromNodesAndTerminals(frequency);}}
The loop re-enters this same final wrapper for any sub-component that is itself a composite, so the whole tree initializes correctly without each level manually calling initialize()/initializeFromNodesAndTerminals() on its children.
A sub-component whose very existence (not just its value) depends on a parameterization-stage value — e.g. picking an inductor vs. a capacitor based on the sign of computed reactive power — cannot be registered in createSubComponents(). Create and register it directly inside initializeParentFromNodesAndTerminals() instead. This is safe because the MNA-registered sub-component list is not consumed until MnaSolver::initialize() finishes the parameterization stage for all components. The one constraint: the late-registered sub-component must not introduce new virtual nodes — those must be declared in the constructor or setParameters(), before collectVirtualNodes() runs.
Watch out: a degenerate parameter injects NaN across the whole matrix
Value derivation in the parameterization stage must be numerically safe at degenerate inputs. Sub-component values come from terminal power, voltage, and frequency, so a zero rated power, zero reactance, or zero capacitance can produce a division by zero and inject NaN/inf into the system matrix or source vector, which then persists for the rest of the simulation. Guard such expressions: use admittance form (Y = jwC, branch current V * Y) rather than impedance form (V / Z), and create optional shunt branches only when their value is strictly positive — a zero-valued shunt is just an open circuit. A degenerate value that slips through will produce a wrong but non-crashing topology, so an explicit check with a log message is better than assuming the value is nonzero.
// DP_Ph1_PiLine.cpp
DP::Ph1::PiLine::PiLine(Stringuid,Stringname,Logger::LevellogLevel):Base::Ph1::PiLine(mAttributes),// Call the constructor of CompositePowerComp and enable automatic pre- and post-step creation
CompositePowerComp<Complex>(uid,name,true,true,logLevel){//...
}voidDP::Ph1::PiLine::createSubComponents(){if(mSubCompCreated)return;mSubCompCreated=true;// Create series sub components
mSubSeriesResistor=std::make_shared<DP::Ph1::Resistor>(**mName+"_res",mLogLevel);// Setup mSubSeriesResistor... (only from values already known from this
// component's own setParameters()/constructor/Attributes - no terminal or
// frequency reads here)
// Register the resistor as a subcomponent. The resistor's pre- and post-step will be called before the pre- and post-step of the parent,
// and the resistor does not contribute to the `rightVector`.
addMNASubComponent(mSubSeriesResistor,MNA_SUBCOMP_TASK_ORDER::TASK_BEFORE_PARENT,MNA_SUBCOMP_TASK_ORDER::TASK_BEFORE_PARENT,false);mSubSeriesInductor=std::make_shared<DP::Ph1::Inductor>(**mName+"_ind",mLogLevel);// Setup mSubSeriesInductor...
// Register the inductor as a subcomponent. The inductor's pre- and post-step will be called before the pre- and post-step of the parent,
// and the inductor does contribute to the `rightVector`.
addMNASubComponent(mSubSeriesInductor,MNA_SUBCOMP_TASK_ORDER::TASK_BEFORE_PARENT,MNA_SUBCOMP_TASK_ORDER::TASK_BEFORE_PARENT,true);//...
}voidDP::Ph1::PiLine::initializeParentFromNodesAndTerminals(Realfrequency){//...
// Frequency-dependent values go here, not in createSubComponents().
Realomega=2.*PI*frequency;Compleximpedance={**mSeriesRes,omega***mSeriesInd};//...
}
Orchestrating MNA Method Calls
By choosing which methods to override in the composite component class, subcomponent handling can either be offloaded to the CompositePowerComp base class or manually implemented in the new component class. By default, CompositePowerComp provides all
methods demanded by MNAInterface in such a way that the subcomponents’ MNA-methods are properly called. To also allow for the composite
component class to perform further actions in these MNA-methods, there exist multiple methods prefixed with mnaParent, e.g. mnaParentPreStep or mnaParentAddPostStepDependencies.
These parent methods will usually be called after the respective method has been called on the subcomponents. For the mnaPreStep and
mnaPostStep methods, this behavior can be set explicitly in the addMNASubComponent method.
If a composite component requires a completely custom implementation of some MNA-method, e.g. for skipping certain subcomponents or for
calling the subcomponent’s methods in a different order, the composite component class can still override the original MNA-method with the mnaComp prefix instead of the
mnaParent prefix. This will prevent the CompositePowerComp base class from doing any subcomponent handling in this specific MNA-method,
so the subcomponent method calls have to be performed explicitly if desired. Given this, the following two implementations of the mnaAddPreStepDependencies method are equivalent:
voidDP::Ph1::PiLine::mnaParentAddPreStepDependencies(AttributeBase::List&prevStepDependencies,AttributeBase::List&attributeDependencies,AttributeBase::List&modifiedAttributes){// Only add the dependencies of the composite component, the subcomponent's dependencies are handled by the base class
prevStepDependencies.push_back(mIntfCurrent);prevStepDependencies.push_back(mIntfVoltage);modifiedAttributes.push_back(mRightVector);}
voidDP::Ph1::PiLine::mnaCompAddPreStepDependencies(AttributeBase::List&prevStepDependencies,AttributeBase::List&attributeDependencies,AttributeBase::List&modifiedAttributes){// Manually add pre-step dependencies of subcomponents
for(autosubComp:mSubcomponentsMNA){subComp->mnaAddPreStepDependencies(prevStepDependencies,attributeDependencies,modifiedAttributes);}// Add pre-step dependencies of component itself
prevStepDependencies.push_back(mIntfCurrent);prevStepDependencies.push_back(mIntfVoltage);modifiedAttributes.push_back(mRightVector);}
3.3.5 - Add New Model
Extending the simulator with new component or control models.
This page walks through adding a component model, using a three phase dynamic phasor inductor as
the example.
Where the code lives
Component models live in the dpsim-models subproject, which builds the CPS library. Headers
and sources are separate trees, both organised by domain:
dpsim-models
|- include
| \ dpsim-models
| |- Base shared base classes, one per component family
| |- DP dynamic phasor
| |- EMT electromagnetic transient
| |- SP static phasor
| \ Signal domain independent control and signal models
\- src
|- Base
|- DP
|- EMT
|- SP
\ Signal
Namespaces follow the same shape, with the phase count nested inside the domain:
DPsim supports several solvers, and each requires certain member functions on the component.
Which ones you implement is determined by the interfaces you inherit rather than by the solver
itself.
For an MNA component, derive from MNASimPowerComp<VarType>, with Complex as the variable
type in the DP and SP domains and Real in EMT. The MNA hooks are declared on
MNAInterface, which MNASimPowerComp implements, so that is where to look for the full set.
If the model is naturally expressed as several existing components wired together rather than as
a single stamp, derive from CompositePowerComp and add subcomponents instead. The pi-line is a
worked example. See subcomponents.
If the component is better described by its own state-space model coupled to the network, see
state-space nodal for that alternative.
Attributes
Every component exposes its parameters and state through attributes, declared in the class and
registered in the constructor. Attributes are what make a value visible to the logger, to the
Python bindings and to the task scheduler.
Pre-step and post-step functions are registered as tasks, and the scheduler derives the order in
which they may run from the attributes each task reads and writes. Declaring those dependencies
correctly matters: a task that modifies an attribute without declaring it can be scheduled in
the wrong order, or in parallel with a reader.
How tasks are built and how the dependency graph is derived is described under
scheduling.
Registering the new component
A new component is not picked up automatically. Three places have to be updated:
dpsim-models/src/CMakeLists.txt, adding the new source file to the list, for example
DP/DP_Ph3_Inductor.cpp
dpsim-models/include/dpsim-models/Components.h, adding the header so that including that one
file gives access to every component
dpsim/src/pybind/DPComponents.cpp, or the matching EMTComponents.cpp, SPComponents.cpp
or SignalComponents.cpp, to expose the class to Python
The Python binding follows the existing pattern in those files:
Name the arguments using the "R"_a form shown above. Without it the Python signature and the
generated reference fall back to positional placeholders such as arg0, and callers cannot use
keyword arguments.
Initialization
Components are initialized either from power flow results or from explicitly set initial values,
and the solver calls into the component in a defined order. Do not add an initialize(Real)
overload of your own for user-facing initialization; use the documented hooks instead.
See initialization for the full sequence, and
note the scaling conventions in guidelines,
since initialization quantities are RMS3PH while EMT simulation quantities are PEAK1PH.
3.3.6 - Create New Simulation
Using DPsim for a new simulation scenario.
Here, we will show the implementation of a new simulation scenario defined in C++, which is using DPsim as a library.
Directory Structure
In the end, your directory structure should look like as follows:
cd my-project
mkdir build &&cd build
cmake ..
make my-scenario
3.3.7 - Debugging
Finding the cause when a simulation runs but produces the wrong answer.
Mixed Python C++ Debugging
Prerequisites
Your vscode launch.json should have two configurations, one to launch the python process and one to attach gdb:
{"version":"0.2.0","configurations":[{"name":"Python: Current File","type":"python","request":"launch","program":"${file}","console":"integratedTerminal","stopOnEntry":true,"env":{"PYTHONPATH":"${workspaceFolder}/build${pathSeparator}${env:PYTHONPATH}"}},{"name":"(gdb) Attach","type":"cppdbg","request":"attach","program":"/usr/bin/python","processId":"${command:pickProcess}","MIMode":"gdb","setupCommands":[{"description":"Enable pretty-printing for gdb","text":"-enable-pretty-printing","ignoreFailures":true}]}]}
The python debugger will stop on entry (“stopOnEntry”: true).
Make sure to adapt your PYTHONPATH variable if necessary.
The C++ code has to be build in debug mode
cmake .. -DCMAKE_BUILD_TYPE=Debug
Attaching C++ Debugger
open the python example to be debugged
go to the debug menu and select / run the “Python: Current File” configuration
the python debugger should stop at entry
set C++ breakpoints
go to the debug menu and run the “(gdb) Attach” configuration
select a process… choose the python process with the “—adapter-access-token” part
you can view the whole description when you hover over the process with the mouse
press play to continue Python debugging… the c++ debugger will stop at the next breakpoint
You can automate this by using the vscode extension “Python C++ Debugger” and by adding this configuration to the launch.json above:
{"name":"Python C++ Debugger","type":"pythoncpp","request":"launch","pythonConfig":"custom","pythonLaunchName":"Python: Current File","cppConfig":"default (gdb) Attach"}
This will automatically run both debuggers and select the current process.
It can take a while before the debugger hits the C++ breakpoints.
C++ Debugging
Use the following launch.json for vscode and set the program path:
{"version":"0.2.0","configurations":[{"name":"(gdb) Launch","type":"cppdbg","request":"launch","program":"${workspaceFolder}/dpsim/build/Examples/Cxx/example","args":[],"stopAtEntry":true,"cwd":"${workspaceFolder}","environment":[],"externalConsole":false,"MIMode":"gdb","setupCommands":[{"description":"Enable pretty-printing for gdb","text":"-enable-pretty-printing","ignoreFailures":true}]}]}
3.3.8 - Logger Implementation
The classes called a logger, and the seam for adding another.
Using the loggers is covered under logging results.
This page covers the classes.
Distinct things share the name
Class
Purpose
DPsim::DataLogger
Numerical results to CSV, one row per step
DPsim::RealTimeDataLogger
The same results, buffered in memory for real-time runs
DPsim::DataLoggerInterface
The seam both implement, and the one to implement for a new sink
CPS::Logger
The diagnostic text log, controlled by LogLevel
Only the data loggers have anything to do with results. CPS::Logger is a different subsystem that
happens to share the word, and conflating the two is the most common confusion here. A component
constructed with Logger::Level::debug writes prose about its own initialization and contributes
nothing to any CSV.
DataLogger
Holds a map from column name to attribute and appends a row per step. log(Real time, Int timeStepCount) returns early when the logger is disabled or when
timeStepCount % mDownsampling != 0, so down-sampling is a modulo on the step counter rather than a
time comparison, and it is exact regardless of step size.
The header is written lazily on the first row, by testing mLogFile.tellp() == 0. That means the
column set is fixed by whatever was registered before the first log call; registering an attribute
afterwards would produce rows that no longer match the header.
Values are written with std::scientific in fixed-width columns, which is what makes the output
readable as a table and also what makes it larger than a minimal CSV would be.
The constructor takes (name, enabled, downsampling). The Python binding exposes only the name, so
enabled and downsampling are unreachable from Python. A binding that took all three would make
down-sampling available to notebook users, who currently have only the time step.
RealTimeDataLogger
Exists because writing to disk inside a real-time step is not acceptable: the file system offers no
bound on how long a write takes, and one slow write overruns the step. It preallocates
mAttributeData from either the final time and step size or an explicit row count, fills it during
the run, and writes at the end.
The preallocation is the point, and it is also the constraint: the row count must be known before
the run, so a real-time simulation of indefinite length needs a different arrangement.
DataLoggerInterface
The abstract seam. Implement it to send results somewhere other than a file, which is what the
co-simulation interfaces do rather than logging and re-reading. Simulation::addLogger accepts
anything implementing it.
Scheduling
A logger contributes a task like any other component, so the scheduler places it by its declared
attribute dependencies. A logged attribute therefore keeps alive the task that produces it, which
has a consequence worth knowing: logging an attribute can change which tasks the scheduler considers
reachable. A model whose results change when a logger is added is exhibiting a missing dependency
declaration elsewhere, not a logging bug. See
adding tasks to a component.
Source
dpsim/src/DataLogger.cpp, dpsim/src/RealTimeDataLogger.cpp,
dpsim/include/dpsim/DataLoggerInterface.h, and dpsim-models/include/dpsim-models/Logger.h for
the unrelated diagnostic logger.
3.4 - Solvers
The solvers below the nodal one, the linear backends, and state-space extraction.
The MNA solver is the default and the one almost every simulation
uses; a component’s side of it is under
interfacing with the MNA solver.
The remaining pages cover the powerflow solver, the alternatives to nodal analysis, the linear
algebra backends, and the state-space model that can be recovered from a running simulation.
The default solver: how it assembles, factorises and steps the system.
MnaSolver<VarType> is the solver almost every simulation uses. The method it implements is derived
under nodal analysis; what a component must
provide to take part is under
interfacing with the MNA solver. This page is
the solver itself.
Note the spelling: the class is MnaSolver even though the file is MNASolver.h.
Setting up
initialize runs the sequence described under
component and solver initialization:
identify the topology objects, create sub-components, collect virtual nodes,
assignMatrixNodeIndices, size the matrices, initialize the components, then assemble.
Which assembly function runs depends on the network:
initializeSystemWithPrecomputedMatrices when the switch combinations are few enough to
enumerate. Every combination gets its own factorised matrix up front, so a switching event
becomes a lookup rather than a refactorisation.
initializeSystemWithVariableMatrix when a component changes its own stamp continuously and
enumeration is impossible.
initializeSystemWithParallelFrequencies for a harmonic study, where several frequencies are
solved side by side.
resolveSystemMatrixRecomputationMode chooses between them when the mode is Auto;
SystemMatrixRecomputationMode::Enabled and Disabled force it either way.
Stepping
solve does the same four things every step.
It zeroes the right-hand side and sums the stamps the components’ pre-step tasks produced, which is
why a component that fails to declare its dependencies can find its contribution missing rather
than wrong. It calls updateSwitchStatus, which produces an index into the precomputed matrices.
It solves through the linear solver for that index. Then it hands the solution to the components'
post-step tasks.
The switch index is the point of the precomputed strategy: with the factorisations already built,
a switching event costs a different lookup rather than new numerical work. That is what makes a
network with frequent switching affordable, and it is why the number of switches is bounded in
practice, since the enumeration grows as two to the power of that number.
solveWithSystemMatrixRecomputation is the other path. It asks hasVariableComponentChanged each
step and rebuilds and refactorises only when something reports a change, which is the expensive but
general case used by variable components such as the SSN models.
Iterative components
After the solve, the solver checks whether any synchronous generator reports requiresIteration.
If so it repeats the solve step until none does, which is how the predictor-corrector and two-stage
machine models reach the implicit solution rather than its explicit approximation. Models that do
not request iteration cost nothing here.
This loop is the reason a machine model can be iterative without the whole solver being iterative.
Linear backends
The solver does not implement its own factorisation; it selects an adapter through
MnaSolverFactory. The choices and their tuning are described under
alternative solvers, which also covers the ordering and
partial-refactorisation options that matter most when the matrix changes every step.
Instrumentation
Solver::mLogSolveTimes records the wall-clock duration of each solve into mSolveTimes, which is
the measurement to use when comparing backends or step sizes rather than timing the whole run.
Source
dpsim/src/MNASolver.cpp, dpsim/src/MNASolverDirect.cpp, and
dpsim/include/dpsim/MNASolverFactory.h.
3.4.2 - Power Flow Solvers
The Newton-Raphson implementations DPsim ships and how they are configured.
What DPsim implements. For the underlying formulation, the mismatch function and the Jacobian,
see power flow.
Solver Implementations
DPsim ships two implementations of the Newton-Raphson power flow solver with power
mismatch and polar coordinates. Both produce identical results (to round-off); they
differ only in how the Jacobian is stored and factorized:
PFSolverPowerPolar (dense): assembles a dense Jacobian and computes a fresh
factorization every Newton iteration. This is the default.
PFSolverPowerPolarSparse (sparse): assembles the Jacobian into a sparse matrix
whose sparsity pattern is fixed (derived once from the network admittance matrix).
The symbolic factorization (ordering) is analyzed once and reused; only the numeric
values are recomputed each Newton iteration. The first iteration of every power flow
solve does a full factorization with pivoting, and subsequent iterations refactorize
while reusing that ordering (via KLU when available). This scales better on large,
sparse grids.
The dense solver is used by default. To opt in to the sparse solver:
sim.set_pf_solver_use_sparse(True)
The flag is ignored and the dense solver is used if DPsim was built without a sparse
linear solver. The benchmark notebook
examples/Notebooks/Grids/PF_Sparse_vs_Dense.ipynb runs a range of network sizes both
ways, verifies the converged voltages match, and compares run time.
Generator Reactive Power Limits
A PV bus assumes its generator can produce whatever reactive power the Newton-Raphson
solution asks for, holding $\vert V_k \vert$ at its setpoint. Real generators cannot: Q
is bounded by $Q_{min}$ and $Q_{max}$. DPsim can enforce these bounds with a
bidirectional PV↔PQ outer loop:
Run the inner Newton-Raphson solve to convergence (as described above).
For every PV bus, compute the generator’s actual reactive output. If it exceeds
$Q_{max}$ or falls below $Q_{min}$, pin the injection at the violated limit and
convert the bus to PQ.
For every bus pinned this way in an earlier pass, check whether the constraint has
relaxed: if $\vert V_k \vert$ has moved past its original setpoint in the releasing
direction, restore voltage control and convert the bus back to PV.
Repeat from step 1 until no bus switches, an outer-iteration cap is hit, or a
per-bus switch counter trips (an anti-oscillation guard, since a bus can otherwise
toggle PV↔PQ indefinitely near the boundary).
Enforcement is opt-in and defaults off, so a system with no limits configured behaves
exactly as before:
sim.set_pf_solver_enforce_q_limits(True)
$Q_{min}$/$Q_{max}$ are set per generator via SynchronGenerator.set_parameters(..., q_limit_max=..., q_limit_min=...); the defaults are $\pm\infty$ (unlimited).
Generators sharing a bus have their limits summed. The two limits are enforced
independently, with no assumption about sign or relative magnitude: asymmetric bounds
(e.g. $Q_{max}=150$ MVAr, $Q_{min}=-30$ MVAr) and same-sign bounds (e.g. a generator
restricted to $Q \in [20, 150]$ MVAr, always producing, or $Q \in [-150, -20]$ MVAr,
always absorbing) are both enforced correctly.
Limitation: no P-dependent capability curve. $Q_{min}$ and $Q_{max}$ are constants
set once per generator, not a function of active power output $P$. A real synchronous
generator’s reactive capability is a “D-curve” bounded by three physically distinct
mechanisms: the stator (armature) current limit $\sqrt{P^2+Q^2} \le S_{rated}$, the
rotor (field) current / heating limit on the over-excited (Q-providing) side, and the
under-excitation limiter (UEL) / steady-state stability limit on the under-excited
(Q-absorbing) side. All three tighten as $P$ approaches rated output, and the over- and
under-excited bounds come from unrelated physical limits, so the true feasible region is
neither symmetric in $Q$ nor independent of $P$. DPsim does not model this curve; a
generator’s $Q$ headroom is the same regardless of how much $P$ it is producing at the
time. Flat per-generator limits are a common baseline in power-flow tools generally, so
this is not a regression, but a P-dependent capability curve is not currently
implemented.
The notebook examples/Notebooks/Grids/PF_Generator_Qlimits.ipynb validates the
switching behavior on a small hand-wired case (binding and non-binding limits, dense vs.
sparse agreement).
3.4.3 - Alternative Solver Implementation
The DAE, ODE and diakoptics solvers, and how the linear backend under MNA is chosen.
Wraps the IDA integrator from Sundials. initialize builds the state and derivative vectors,
registers each component’s residual function, then creates the solver with IDACreate, passes the
solver instance as user data so the residual callbacks can reach it, and sets scalar relative and
absolute tolerances with IDASStolerances.
Components take part by implementing DAEInterface and contributing their residual. The offset
vector recorded at the top of the file defines how each component’s block is laid out within the
global residual.
Two practical notes. The solver chooses its own steps, so a run’s cost is not predictable and it
cannot be used under a real-time timer. And several std::cout calls remain in the initialization
path, so it prints to standard output independently of the logger.
ODESolver and ODEintSolver
ODESolver wraps CVODE from Sundials for a single component, sizing the problem from
mOdePreState and attaching a dense linear solver. ODEintSolver does the same job with boost’s
odeint, calling comp->odeint(y, ydot, t).
Both integrate one component across a network step while the network itself stays on its fixed step,
so the coupling is staggered and first-order accurate regardless of the inner integrator’s order.
DiakopticsSolver
Constructed with the system and an explicit list of components to tear, which must implement
MNATearInterface. system.splitSubnets performs the partition, initSubnets builds the
per-subnetwork node and component lists, and mNodeSubnetMap records which subnetwork owns each
node.
createTearMatrices is specialised per value type, and the sizes differ in a way worth noting: the
Real specialisation allocates tearComponents * phaseMultiplier, while the Complex one
allocates twice that, because a complex quantity is carried as a real-augmented pair. The phase
multiplier is 3 when the subnetwork phase type is ABC and 1 otherwise, taken from the first node
of the system.
The removed-branch system is dense and small. A comment in the source notes that the reduction could
still be sped up by exploiting the block diagonal structure of the inverse, so the present
implementation is correct rather than optimal.
Linear backends under MNA
Requires the matching build options
The nodal solver does not implement its factorisation. MNASolverFactory selects an adapter, and
mSupportedSolverImpls is compiled conditionally, so which of the implementations below exist
depends entirely on how DPsim was configured. The GPU adapters need a CUDA build.
Implementation
Adapter
Notes
KLU
KLUAdapter
Default, and the fallback when the choice is Undef
SparseLU
SparseLUAdapter
Eigen’s sparse LU
DenseLU
DenseLUAdapter
Dense, for small systems
CUDADense
GpuDenseAdapter
Requires a CUDA build
CUDASparse
GpuSparseAdapter
Requires a CUDA build
CUDAMagma
GpuMagmaAdapter
Requires a CUDA build with Magma
Plugin
loaded at runtime
For a solver outside the tree
DirectLinearSolverConfiguration tunes the chosen backend, and not every option applies to every
one:
SCALING_METHOD: none, sum or max
FILL_IN_REDUCTION_METHOD: AMD, AMD_NV, AMD_RA or COLAMD. The NV and RA variants take
the set of time-varying entries into account when ordering, which is what makes partial
refactorization effective for a network with switching elements.
PARTIAL_REFACTORIZATION_METHOD: none, factorization path, or refactorization restart. This is the
lever that matters when a switch or a variable component changes the matrix every step.
USE_BTF: block triangular form on or off
The defaults are chosen for a general network. The combination of an ordering that knows about
varying entries with partial refactorization is what makes repeated switching affordable, and it is
inert if the matrix never changes.
Source
Under dpsim/src/: DAESolver.cpp, ODESolver.cpp, ODEintSolver.cpp, DiakopticsSolver.cpp,
and the six *Adapter.cpp files.
3.4.4 - State-Space Extraction
Enabling extraction, and running modal analysis on what it produces.
The method itself, what the extracted model means and where it is valid, is derived under
state-space extraction.
This page covers enabling it and reading the result.
State-space extraction is optional and can be enabled through the Simulation API. During simulation setup, the MNA solver creates an MNAStateSpaceExtractor. During the solver task flow, a state-space extraction task uses the active direct linear solver to update the extracted discrete-time state matrix.
Main classes
The implementation is organized around three main parts:
MNAStateSpaceExtractor assembles and stores the extracted discrete-time state matrix.
MNAStateSpaceContributor represents the state-space contribution of one supported component.
MNAStateSpaceContributorFactory creates contributors for supported MNA components.
The extractor is owned by the MNA solver. Component contributors are created during solver initialization and are used to stamp the local matrices needed for the MNA-coupled state-space formulation.
StateSpaceModalAnalysis is constructed from an MNAStateSpaceExtractor and computes the modes of
whatever the extractor last produced. The method is described under
modal analysis.
update() runs Eigen::EigenSolver on the discrete state matrix and throws if it does not converge.
It then maps each discrete eigenvalue to the continuous plane with 2 / dt * (z - 1) / (z + 1) and
keeps both sets, retrievable through getDiscreteEigenvalues and getContinuousEigenvalues.
Participation factors are the elementwise product of the right eigenvectors with the transpose of the
left ones. They require inverting the right eigenvector matrix, so update() throws with an explicit
message when that matrix is singular. That happens for a defective state matrix, which is a property
of the system rather than a numerical problem; the eigenvalues are still valid in that case, only the
participation factors are unavailable.
setAnalysisFrame selects between StateSpaceAnalysisFrame::Native, which analyses the states as the
components hold them, and GlobalDQ0, which transforms into one common frame first. The second needs
setGlobalDq0Frame(omega, theta0). getStateNames returns names matching the frame in use, so a
participation factor can be attributed to a named state rather than to an index.
How each model family is arranged in code, paired with its equations under Concepts.
One page per model family, covering the class hierarchy, how the component interfaces with the
solver, its attributes and state layout, and the traps in configuring it. The equations behind each
are under Concepts, which names no class; these pages name
nothing else.
Which domains implement which model is in
model availability, generated from the
headers.
3.5.1 - Reduced Order Generator Implementation
How the reduced order machine equations are arranged in code and stamped into the solver.
Base::ReducedOrderSynchronGenerator<VarType> holds everything independent of domain and of order:
the per unit base values, the operational parameters, the mechanical states, the controller
attachments and the discretisation coefficients. It is templated on Real for EMT and Complex
for DP and SP, which is why the axis frame quantities appear twice, as mVdq0/mIdq0 in the real
specialisation and mVdq/mIdq in the complex one.
Each domain then provides a ReducedOrderSynchronGeneratorVBR layer holding the frame transform,
and each order a concrete class. The order is recorded in mSGOrder, which selects which
coefficients are computed.
Network interface
setModelAsNortonSource chooses between the two interface forms. The default is the Norton
equivalent, in which the machine contributes only to the right hand side vector and requests no
virtual nodes. The Thevenin form requests two virtual nodes instead. Both represent the same model;
the Norton form is cheaper because it leaves the system matrix untouched between steps
[Wang2010].
Watch out: call setModelAsNortonSource before connecting
Note that setModelAsNortonSource calls setVirtualNodeNumber, so it must be called before the
component is connected.
Coefficients
calculateAuxiliarConstants computes the discretisation coefficients once, since they depend only
on the parameters and the step size. The member names map to the symbols on the theory page as
follows.
Member
Symbol
mAd_t, mBd_t
$A_d’$, $B_d'$
mAq_t, mBq_t, mDq_t
$A_q’$, $B_q’$, $D_q'$
mAd_s, mBq_s, mCd_s, mCq_s, mAq_s
subtransient coefficients
mYd, mYq
$Y_d$, $Y_q$, non-zero only for the 6a variant
The naming looks wrong at first and is not. Zd_t is built from $L_q - L_q’$ and Zq_t from
$L_d - L_d’$, because each is named for the axis whose coefficient it feeds rather than for the
parameters it is assembled from. That follows the physics: the d-axis internal voltage arises from
q-axis rotor flux and decays with $T_{q0}’$, so mAd_t correctly combines $L_q - L_q’$ with
$T_{q0}’$ and multiplies the q-axis current.
Read a coefficient’s use rather than its assignment line before concluding an axis is swapped.
Step sequence
mnaCompPreStep runs before the network solve and does three things in order. It advances the
controllers, saving mEf_prev and mMechTorque_prev first because the trapezoidal history terms
need the previous values. It calls stepInPerUnit, which updates the frame transforms from
mThetaMech, recomputes the axis frame state from the terminal quantities, and evaluates the
history voltage into mEh_vbr. It then stamps the result into the right hand side vector.
Each concrete order implements only specificInitialization and stepInPerUnit. Everything else is
inherited.
Initialization
Initialization runs from the powerflow solution, not from user supplied states. The base class
computes the load angle as the phase of $V + j L_q I$, projects the terminal voltage and current
onto the axis frame, and derives the field voltage from the no-load relation. Only then does
specificInitialization set the order specific states, which is why a concrete class can assume
mVdq and mIdq are already populated.
Attached controllers are initialized afterwards from the machine’s own initial values, so an
exciter or governor never needs its own operating point.
Controllers
Excitation, governor, turbine and power system stabilizer attach through the base class and are
optional, guarded by mHasExciter, mHasGovernorAndTurbine, mHasTurbineGovernor and mHasPSS.
The stabilizer output feeds the exciter within the same step, and the governor output feeds the
turbine, so the order of the calls in mnaCompPreStep is load bearing.
[Wang2010] IEEE Xplore document 5411963. Cited in the machine model pages as the basis for interfacing a machine to a nodal solver through a current source that leaves the system matrix unchanged.
3.5.2 - Switch and Load Implementation
How the switch and load models are arranged in code, and the traps in configuring them.
The models are derived under switches and
loads. This page covers only their arrangement in
code. Availability per domain is in
model availability.
Switches
Switch implements Base::Ph1::Switch and stamps one admittance chosen by mIsClosed, using
MNAStampUtils::stampAdmittance so the grounded-terminal cases are handled centrally.
SeriesSwitch folds a series resistance into the same branch.
varResSwitch additionally implements MNAVariableCompInterface, which is what allows it to change
the system matrix during a run. Its hasParameterChanged is called each step and drives the
transition:
Opening multiplies the resistance by mDeltaResOpen each step until it passes the target open
value, then clamps to it and reports the transition finished.
Closing uses mDeltaResClosed, which is 0, so the first step takes the resistance to zero, the
clamp catches it and sets the closed value. Closing is therefore immediate by construction, not by
a separate code path.
Watch out: setInitParameters is mandatory
setInitParameters(timestep) must be called before the simulation, because the growth factor is
derived from the step size as 0.5 * timestep / 0.001 + 1. It also captures the configured
resistances as the transition targets, since the live attributes are overwritten during the ramp. If
it is not called, mDeltaResOpen keeps its default of 1.5, which is the value for a 1 ms step and
wrong for any other.
Its initializeFromNodesAndTerminals carries a comment saying it is not used.
Loads
RXLoad is a CompositePowerComp. In initializeFromNodesAndTerminals it converts the powers to
element values and builds sub-components:
a resistor, only if the active power is non-zero
an inductor if the reactance is positive, a capacitor if negative, and nothing if the reactive
power is zero
Watch out: a zero power silently drops a branch
Each is registered with addMNASubComponent and connected between ground and the load terminal. The
conditionals are the trap: a load configured with P or Q at zero silently omits that branch. It
does not error, and the missing branch is only visible as a load that draws less than expected.
PQLoadCS wraps a current source and sets its reference in updateSetPoint from
conj(S / mNomVoltage). The nominal voltage, not the terminal voltage, is deliberate; the line
using the terminal voltage is present but commented out. Changing it would make the component
nonlinear and require an iterative solve.
Shunt takes a conductance and a susceptance directly and additionally carries per-unit attributes,
since it is the form the powerflow solver consumes.
Source
Switches: {SP,DP,EMT}_Ph{1,3}_Switch, DP_Ph3_SeriesSwitch, EMT_Ph3_SeriesSwitch, {DP,SP}_Ph1_varResSwitch under dpsim-models/src/
How the source components stamp, and the quirks in the non-ideal ones.
The models are derived under sources. This page
covers only the code.
Ideal sources
VoltageSource requests one virtual node, which carries the source current as the extra unknown,
and stamps the constraint rows that fix the terminal voltage difference. CurrentSource requests
none and contributes only to the right hand side.
ControlledVoltageSource and ControlledCurrentSource are the same components with their reference
supplied as an attribute rather than a parameter, so another component or an interface can drive
them. The reference is read during the pre-step, which is why it is the previous step’s value.
VoltageSourceNorton
Stamps directly rather than through a virtual node. mnaCompApplySystemMatrixStamp adds
mConductance to both diagonal entries and subtracts it from the two off-diagonal entries, guarded
by terminalNotGrounded, and mnaCompApplyRightSideVectorStamp sets the equivalent current
mIntfVoltage / mResistance with opposite signs at the two terminals.
mConductance is computed in setParameters as 1 / resistance, so calling setParameters is
mandatory before the run and a zero resistance is a division by zero rather than an ideal source.
Watch out: EMT::Ph3 must set the phase type first
The EMT::Ph3 variant was missing mPhaseType = PhaseType::ABC in its constructor until 2026-07-31.
Without it SimPowerComp::initialize sized the interface matrices to one row and the component
aborted the process on an Eigen bounds assertion when it wrote rows 1 and 2. The general rule that
came out of it is on the reduced order generator page and
applies to any EMT::Ph3 component: set the phase type in the constructor, before
setVirtualNodeNumber.
VoltageSourceRamp
A composite wrapping a VoltageSource whose reference it rewrites each step in updateState(time).
Three regimes: before mSwitchTime the reference is unchanged; during mRampTime the added voltage
is interpolated linearly while the added frequency is blended by a raised sine
0.5 + 0.5 * sin(pi * t / T - pi/2); afterwards both are fully applied.
The two are blended differently on purpose. A linear frequency interpolation applied as a phase
offset would step the phase at both ends of the ramp; the raised sine has zero derivative at both
ends, so the frequency contribution enters and leaves smoothly. The consequence is that the
instantaneous frequency during the ramp is not the linear interpolation between the two values, and
reading mAddSrcFreq as “the frequency at the midpoint” is wrong.
Note also that the added frequency term is applied as mAddSrcFreq * time, using absolute
simulation time rather than time since the switch, so the phase contribution depends on when in the
run the ramp occurs.
ProfileVoltageSource
Holds a std::filesystem::path, a sample vector and an index, and reads the file in readFromFile
at construction. It implements DAEInterface in addition to the MNA hooks.
The samples are stepped by index rather than interpolated against simulation time, so the profile’s
sample rate and the simulation step must match for the waveform to have the intended duration. It is
bound in Python and constructing it with a file that is not a readable sample list raises rather
than crashing, which is covered by a test.
Source
Under dpsim-models/src/{SP,DP,EMT}/. Availability per domain is in
model availability.
3.5.4 - Injection and Compensation Implementation
How the external network, the static compensator and the solid state transformer are built.
A CompositePowerComp wrapping a single VoltageSource sub-component. It owns no equations of its
own; it exists so that the external network is a named component rather than a bare source, and so
that the driving waveform can be swapped without changing the network description.
setParameters is overloaded by the kind of generator wanted behind it: a constant phasor for a
fixed source, a start frequency with a rate of change for a ramp, and an initial phasor with a
modulation frequency for a modulated one. Which overload is called determines which
SignalGenerator the sub-source is given; see
signal component implementation.
Because the source is ideal, adding an impedance to represent a finite short circuit level is the
caller’s job. Nothing in the component does it.
SVC
Not composite. It computes a susceptance each step and realises it by reconfiguring an internal
reactive element, so it implements the variable-component interface and forces a refactorisation
whenever the value changes.
updateSusceptance performs both lags with the trapezoidal rule, using precomputed constants
Fac1 = dt / (2 Tr), Fac2 = dt Kr / (2 Tr) and Fac3 = dt / (2 Tm). The measurement lag is
applied first, then the error is formed in per unit against mNomVolt, then the susceptance follows
from the previous value and the present and previous error.
The result is clamped to mBMax and mBMin before use, and the internal element is only rebuilt
when the value actually changed. The sign of the clamped susceptance selects which element is
formed: positive gives an inductance 1 / (omega * B * mBN), negative a capacitance
B * mBN / (-omega). mBN is the base susceptance, so B is per unit.
Watch out: mMechMode selects a different control law
mMechMode switches the component to the discrete branch entirely. That path ignores the continuous
regulator and instead moves mTapPos by one step when the error exceeds mDeadband, bounded by
mMinPos and mMaxPos. The two modes share the component but not the control law, so a parameter
that matters in one is inert in the other.
Suspected defect: magnitude taken from the real part only
Note that the voltage magnitude is taken as abs(real(V)) of the interface voltage rather than the
magnitude of the complex envelope. For a dynamic phasor quantity those differ, and the difference is
not negligible when the envelope has a significant imaginary part.
SolidStateTransformer
A CompositePowerComp that represents each side as a current source rather than as a coupled
winding pair. setParameters(nomV1, nomV2, Pref, Q1ref, Q2ref) takes the two nominal voltages and
three power set points; the active power is common to both sides, while the reactive powers are set
per side.
Values are held in per unit internally, so the nominal voltages are the base rather than a turns
ratio. There is no magnetising branch, no leakage impedance and no angle dependence, which is the
representation the concept page describes and not an omission.
SSNComp holds the continuous matrices, the discrete pair, the equivalent admittance mW, the
history vector mYHist and the state attribute x. Two branches specialise it by which quantity is
the input:
VTypeSSNComp takes voltage in and gives current out, so it stamps as an admittance
ITypeSSNComp is the dual
Terminal-count layers sit on top: TwoTerminalVTypeSSNComp, TwoTerminalITypeSSNComp and
FourTerminalVTypeSSNComp handle the mapping from terminal quantities to the model input, and the
Variable layers add re-forming of the model between steps. Every EMT::Ph3 base sets
PhaseType::ABC in its constructor, which the concrete components rely on.
What a component provides
A fixed-model component only calls SSNComp::setParameters(A, B, C, D) with its chosen state,
input and output. EMT::Ph3::SSN::Inductor is the whole pattern:
MatrixaMatrix=Matrix::Zero(3,3);// x = i_abc
MatrixbMatrix=inductance.inverse();// u = v_abc
MatrixcMatrix=Matrix::Identity(3,3);MatrixdMatrix=Matrix::Zero(3,3);SSNComp::setParameters(aMatrix,bMatrix,cMatrix,dMatrix);
The base does the rest: recomputeDiscreteModel calls
Math::calculateStateSpaceTrapezoidalMatrices and sets mW = mC * mdB + mD,
calculateHistoryVector returns mC * (mdA * x + mdB * u), and the post step updates the state
from the old and new input.
A varying component additionally overrides updateStateSpaceModel (a no-op for linear components)
and, for the variable bases, updateComponentParameters to report whether the model changed. Only
when it reports a change is the system matrix refactorised.
Domain differences
The formulation differs by domain, and so does the code path. The theory is under
SSN across domains.
A component supplies the same real(A, B, C, D) in either domain. EMT::SSNComp discretises
them directly. DP::SSNComp does not: buildAugmentedA(omega) assembles the real-augmented
2n x 2n matrix with A on both diagonal blocks and +wI / -wI off-diagonal,
buildAugmentedB places B on both diagonal blocks, and the result goes through the sameMath::calculateStateSpaceTrapezoidalMatrices helper as EMT. The discrete blocks are then folded
back into complex form as topLeft + j * bottomLeft, which is the inverse of the
[[P, -Q], [Q, P]] representation. mW and the history vector are complex as a result.
recomputeDiscreteModel therefore takes omega in DP and takes no argument in EMT. A component
that hardcodes a frequency here rather than using the value handed to mnaCompInitialize is wrong
at any other system frequency.
Watch out: the mixed SSN base needs a pre-shifted matrix
One base does not follow this pattern. MixedVTypeVariableSSNComp does not augment internally:
it requires the derived component to hand it a state matrix that is already carrier shifted, because
its steady-state solve assumes so. Supplying an unshifted matrix there initializes to the wrong
operating point rather than failing, and it is the single easiest mistake to make when porting a
component from EMT to DP.
Frame metadata
getLocalAbcStateBlocks returns nothing by default and should be overridden only for states
that genuinely form physical abc triples. It is consumed by tooling that reasons about the state
vector in the phase frame, and declaring a block that is not one produces wrong groupings rather
than an error.
Initialization
calculateSteadyStateStateFromInput evaluates (jωI − A)⁻¹ B u, which requires the continuous
model to be set first. Components with real control states cannot use the default
initializeFromNodesAndTerminals on the mixed base; see
DP Ph1 averaged VSI implementation for that
case and for the requirement that the state matrix be handed over already carrier shifted.
The components
Fixed models: SSN_Full_Serial_RLC, SSN_Capacitor, SSN_Inductor, SSNTypeV2T, SSNTypeI2T.
Varying models: SSN_Variable_Serial_RLC, PiecewiseLinearInductor, and the inverter models under
power electronics. The Generic two- and
four-terminal classes take the matrices from the caller instead of forming them, so they are the
route to an SSN component without writing C++. Availability per domain is in
model availability.
3.5.6 - Signal Component Implementation
How control and signal blocks are written, stepped and scheduled.
Signal blocks derive from SimSignalComp and take no part in the nodal solve. They contribute tasks
through getTasks() rather than through the MNA hooks, and the scheduler orders them from the
attribute dependencies those tasks declare. A block that reads an attribute without declaring it may
still produce the right answer, by luck of ordering, and then change behaviour when an unrelated
component is added; see
adding tasks to a component.
Most blocks follow a two-task shape: a PreStep that copies the current values into the previous
ones, and a Step that computes the new state and output. The split exists so that a value consumed
by another block within the same step is unambiguous about which timestep it belongs to.
The mInputPrev / mInputCurr pattern
Blocks that integrate with the trapezoidal rule need both the present and the previous input, so
they carry mInputPrev, mInputCurr, mStatePrev, mStateCurr and the matching output pair. The
PreStep task performs the shift. Integrator::signalStep is the whole pattern:
VCO::signalStep deliberately does not, using state + dt * input, because it accumulates an angle
rather than integrating a control signal.
Every one of these blocks needs setSimulationParameters(timestep) before the run, since the step
size appears directly in the update. Blocks that expose setInitialValues must also have it called,
or they start from zero rather than from the operating point.
State-space blocks
PLL is written as an explicit state-space block rather than as arithmetic, setting
The first input is the nominal frequency and is held constant, which is how the feed-forward term
enters. Writing it this way means the block can be discretised by the same helpers as anything else
rather than by hand.
FIRFilter
FIRFilter keeps a circular buffer and a write index, and step sums mFilter[i] * mSignal[...]
over the filter length before advancing the index. It contributes a single Step task. Filter
coefficients are supplied by the caller; nothing validates their length against the buffer or checks
that they sum to a sensible gain.
Generators
SignalGenerator is the abstract base; SineWaveGenerator, DCGenerator, CosineFMGenerator and
FrequencyRampGenerator are the concrete ones, and all expose their value through a sigOut
attribute that a source component references.
Watch out: the default ramp depends on step history
FrequencyRampGenerator has two modes. The default accumulates phase incrementally, deriving its
timestep as time - mOldTime rather than from a configured step. The mUseAbsoluteCalc path
computes the phase in closed form from the ramp parameters instead. The incremental path makes the
waveform depend on the step history; the absolute path does not. Prefer the absolute path when
comparing runs at different step sizes.
Source
Under dpsim-models/src/Signal/. Availability is in
model availability; these blocks are domain
independent and appear there as a list rather than a matrix.
3.5.7 - DP Ph1 Averaged VSI Implementation
How the dynamic phasor averaged inverter is arranged in code and interfaced to the solver.
DP::Ph1::AvVoltSourceInverterStateSpace is final and derives from
DP::Ph1::MixedVTypeVariableSSNComp. The mixed base is what makes the model possible in this
domain: eight of the twelve states are real baseband control states and only the last four are the
real and imaginary parts of the two carrier-band envelopes, so the component cannot use the plain
complex SSN base.
State layout
The state order is fixed by a private StateIndex enum, which the linearization indexes directly.
Index
Name
Kind
0
Psi
PLL angle deviation from the nominal carrier phase
1
PhiPLL
PLL integrator
2, 3
PFiltered, QFiltered
power filter
4, 5
PhiD, PhiQ
outer power control integrators
6, 7
GammaD, GammaQ
inner current control integrators
8, 9
VcRe, VcIm
filter capacitor voltage envelope
10, 11
IfRe, IfIm
filter inductor current envelope
The base does not impose this ordering. It is told only how many real and how many complex states
there are, and sizes the packed real vector as realStateCount + 2 * complexStateCount. The
three-phase model orders its states the other way round, envelopes first and controls after, and is
equally valid. What the base does require is that the derived class hand it a state matrix that is
already carrier shifted: the steady-state solve assumes it, and a model that supplies an
unshifted matrix initializes to the wrong operating point rather than failing.
The default initializeFromNodesAndTerminals throws unless realStateCount is zero, so any model
with real control states, which includes this one, must override it.
Tracking Psi rather than the raw PLL angle keeps the tracked quantity bounded. The raw angle grows
without limit, which costs relinearization accuracy as a run gets longer.
Parameters
Watch out: fourteen positional parameters with no defaults
setParameters takes the filter and control parameters positionally, in the order
lf, cf, rf, rc, omegaN, kpPLL, kiPLL, omegaCutoff, pRef, qRef, kpPowerCtrl, kiPowerCtrl, kpCurrCtrl, kiCurrCtrl. There are fourteen of them and no defaults, so a transposed pair is easy to
introduce and produces a model that runs and is wrong rather than one that fails.
initializeFromNodesAndTerminals derives the initial state from the connected node voltage, so the
operating point comes from the powerflow rather than from user supplied states.
DP::Ph3::AvVoltSourceInverterStateSpace is final and derives from
DP::Ph1::MixedVTypeVariableSSNComp, the same mixed base as the single-phase model. Per-phase
complex quantities are carried as std::array<Complex, 3>.
State layout
Twenty states by default, ordered envelopes first and controls afterwards, or twenty-two with the
optional negative-sequence loop enabled.
Index
Name
Kind
0–5
VcARe … VcCIm
filter capacitor voltage envelope, per phase
6–11
IfARe … IfCIm
filter inductor current envelope, per phase
12
Psi
PLL angle deviation from the nominal carrier phase
13
PhiPLL
PLL integrator
14, 15
PFiltered, QFiltered
power filter
16, 17
PhiD, PhiQ
outer power control integrators
18, 19
GammaD, GammaQ
inner current control integrators
20, 21
GammaND, GammaNQ
negative-sequence current control integrators, only when enabled
This is the reverse of the single-phase ordering, which places controls first. The base does not
care: it is given only the counts of real and complex states and sizes the packed real vector as
realStateCount + 2 * complexStateCount. What it does require is a state matrix that is already
carrier shifted, since the steady-state solve assumes it.
The last two states are the difference from the single-phase model beyond the per-phase filter.
Three independent phase envelopes admit a negative-sequence component that a single positive-sequence
envelope cannot represent, so the controller carries its own negative-sequence integrator pair.
Enabling the negative-sequence loop
The constructor takes an enableNegSeqControl flag, false by default. The two references
$i_{nd,\mathrm{ref}}$ and $i_{nq,\mathrm{ref}}$ are the last two arguments of setParameters and
default to zero, which makes the loop a suppressor rather than an injector. The measured
$i_{rc,nd}$ and $i_{rc,nq}$ are exposed as the irc_n_d and irc_n_q attributes, and stay at zero
while the loop is disabled.
Leave it off to compare against EMT::Ph3
The flag exists because the two configurations answer different questions. Off, the model has the
same 20 states and the same eigenvalue count as its EMT::Ph3 counterpart, which is what a
cross-domain comparison needs. On, it gains 2 states and can regulate an unbalanced terminal.
The two integrators are appended after the control block rather than inserted next to the other
control states, so enabling the flag leaves every envelope and positive-sequence control index
unchanged. Code indexing into the state vector therefore does not need to know about the flag.
EMT::Ph3::AvVoltSourceInverterStateSpace is final and derives from
EMT::Ph3::TwoTerminalVTypeVariableSSNComp. Unlike the dynamic phasor ports of this model, every
state here is real, so it uses the plain variable state-space nodal base rather than the mixed one.
The base sets PhaseType::ABC in its constructor, which this component relies on.
State layout
Fourteen real states, controls first and filter states afterwards.
Index
Name
Kind
0
ThetaPLL
PLL angle
1
PhiPLL
PLL integrator
2, 3
PFiltered, QFiltered
power filter
4, 5
PhiD, PhiQ
outer power control integrators
6, 7
GammaD, GammaQ
inner current control integrators
8–10
VcA, VcB, VcC
filter capacitor voltage, per phase
11–13
IfA, IfB, IfC
filter inductor current, per phase
The first state is the raw PLL angle. The dynamic phasor ports track the deviation from the nominal
carrier phase instead, because there the angle is compared against a carrier and an unboundedly
growing value costs relinearization accuracy. In EMT there is no carrier to drift against, so the
raw angle is used directly.
Six real filter states here correspond to two complex envelopes in the single-phase dynamic phasor
model and six in the three-phase one. That correspondence is the practical statement of what the
envelope transform buys.
EMT::Ph3::SSN_GFM is final and derives from EMT::Ph3::TwoTerminalVTypeVariableSSNComp. All
seventeen states are real.
State layout
Index
Name
Kind
0, 1
PFiltered, QFiltered
power filter
2, 3
Omega, Theta
droop frequency and angle
4
VoltageMagnitude
voltage droop output
5, 6
VoltageIntegratorD, VoltageIntegratorQ
outer voltage control
7, 8
CurrentIntegratorD, CurrentIntegratorQ
inner current control
9, 10
DelayVoltageD, DelayVoltageQ
modulation delay
11–13
VcA, VcB, VcC
filter capacitor voltage, per phase
14–16
IfA, IfB, IfC
filter inductor current, per phase
Omega and Theta being states rather than inputs is what makes this grid forming: the converter
carries its own frequency and angle instead of tracking a measured one through a PLL.
Numerical linearization
The Jacobians are not written out by hand. calculateNumericalJacobians forms all four by central
differences of the nonlinear state and output functions, so a change to the control equations needs
no matching change to any matrix code.
The perturbation for column $j$ is absoluteStep + relativeStep * max(1, |x_j|), defaulting to
1e-8 and 1e-6 and adjustable at runtime. The max(1, ...) floor means the step is effectively
absolute for small states and relative for large ones, which keeps the difference well conditioned
across states whose magnitudes differ by orders.
Because the model is time varying, the state-space form and its stamp are recomputed every step
rather than cached. That is the cost of this approach and the reason it is used only where the
control is genuinely nonlinear.
The mathematics behind the solvers and the component models.
The methods DPsim implements, described independently of the code. Nodal analysis and dynamic
phasors underpin the main solver and have a page each. The model pages
give the physical equations for a component and how they are transformed for each supported
domain.
DPsim also includes a load flow solver, used on its own or to compute the initial state of a
network when that state is not part of the network data. Electromagnetic transient models exist
alongside the dynamic phasor ones, serving both as a simulation domain and as the reference the
dynamic phasor models are tested against.
4.1 - Nodal Analysis
How a network becomes a system of equations, and how components stamp into it.
A circuit with $b$ branches has $2b$ unknowns since there are $b$ voltages and $b$ currents.
Hence, $2b$ linear independent equations are required to solve the circuit.
If the circuit has $n$ nodes and $b$ branches, it has
Kirchoff’s current law (KCL) equations
Kirchoff’s voltage law (KVL) equations
Characteristic equations (Ohm’s Law)
There are only $n-1$ KCLs since the nth equation is a linear combination of the remaining $n-1$.
At the same time, it can be demonstrated that if we can imagine a very high number of closed paths in the network, only $b-n+1$ are able to provide independent KVLs.
Finally there are $b$ characteristic equations, describing the behavior of the branch, making a total of $2b$ linear independent equations.
The nodal analysis method reduces the number of equations that need to be solved simultaneously.
$n-1$ voltage variables are defined and solved, writing $n-1$ KCL based equations.
A circuit can be solved using Nodal Analysis as follows
Select a reference node (mathematical ground) and number the remaining $n-1$ nodes, that are the independent voltage variables
Represent every branch current $i$ as a function of node voltage variables $v$ with the general expression $i = g(v)$
Write $n-1$ KCL based equations in terms of node voltage variable.
The resulting equations can be written in matrix form and have to be solved for $v$.
The matrix $\boldsymbol{Y}$ and the right hand side $\boldsymbol{i}$ are never written out by
inspecting the whole circuit at once. Each element contributes its own fixed pattern of entries,
and the system is formed by adding those contributions together. A conductance $G$ between nodes
$k$ and $l$ adds $G$ to the diagonal entries $Y_{kk}$ and $Y_{ll}$ and subtracts it from the
off-diagonal entries $Y_{kl}$ and $Y_{lk}$; if one terminal is the reference node, only the
single diagonal entry appears. A current source injecting $I$ into node $k$ from node $l$ adds
$I$ to $i_k$ and subtracts it from $i_l$.
This additive assembly is what makes the method practical. An element needs to know only the
indices of the nodes it is attached to, never anything about the rest of the network, and the
same element contributes the same pattern regardless of what it is connected to. It also
explains the structure of the result: each row corresponds to one node and holds a non-zero
entry only for nodes reachable through a single element, so $\boldsymbol{Y}$ is symmetric for
networks of passive elements and sparse for any network of realistic size.
Dynamic Elements and the Companion Model
The formulation above assumes every branch current can be written as a function of node voltages
alone. Inductors and capacitors do not satisfy this, since their currents depend on derivatives.
They are brought into the same form by discretising the differential relation over one time step.
Applying the trapezoidal rule to a capacitor between the instants $t - \Delta t$ and $t$ gives
Both have the form of a conductance in parallel with a current source. The conductance depends
only on the element value and the time step, so it is a constant contribution to
$\boldsymbol{Y}$; the current source depends only on quantities from the previous step, which are
known when the current step begins, so it is a contribution to $\boldsymbol{i}$. This pair is
called the companion model of the element, and the current source term is called its history
term. Once every dynamic element has been replaced by its companion model, the network at each
instant is a purely resistive one and the nodal formulation applies unchanged.
Extending the Formulation for Voltage Sources
An ideal voltage source cannot be stamped as a conductance, because its current is not determined
by the voltage across it. The formulation is extended by admitting the source current as an
additional unknown and adding the equation that constrains its terminal voltage. The system
becomes
where $\boldsymbol{j}$ collects the unknown source currents, $\boldsymbol{u}$ the prescribed
source voltages, and $\boldsymbol{A}$ has a single $+1$ and $-1$ per source marking its terminals.
This extension is known as modified nodal analysis, and it is the form actually solved. The
augmented matrix is no longer positive definite and carries zeros on part of its diagonal, which
is why the solver has to be one that tolerates that rather than one specialised to the passive
case.
Solving Over Time
Within a simulation the same system is solved once per time step. The left hand side depends only
on the element values, the topology and the time step, none of which change from one step to the
next in the ordinary case, so the matrix is factorised once and each step reuses that
factorisation with a new right hand side. The cost of a step is then a forward and backward
substitution rather than a full solve, which is what makes the method viable for large networks
and for real time.
Two situations invalidate the factorisation. A switching event changes the topology and therefore
the matrix, so the affected configuration has to be factorised again; simulations that switch
frequently often pre-compute a factorisation for each configuration instead. A non-linear element
makes the entries themselves depend on the solution, which requires iterating within the step
until the node voltages and the element operating points agree.
4.2 - Dynamic Phasors
The envelope transform behind the DP domain, derived two ways, and how it relates to EMT.
In the power systems community, dynamic phasors were initially introduced for power electronics analysis Sanders1991 as a more general approach than state-space averaging.
They were used to construct efficient models for the dynamics of switching gate phenomena with a high level of detail as shown in Mattavelli1999.
A few years later, dynamic phasors were also employed for power system simulation as described in Demiray2008.
In Strunz2006 the authors combine the dynamic phasor approach with the Electromagnetic Transients Program (EMTP) simulator concept which includes Modified Nodal Analysis (MNA).
Further research topics include fault and stability analysis under unbalanced conditions as presented in Stankovic2000 and also rotating machine models have been developed in dynamic phasors Zhang 2007.
Bandpass Signals and Baseband Representation
Although here, dynamic phasors are presented as a power system modelling tool, it should be noted that the concept is also known in other domains, for example, microwave and communications engineering Maas2003, Suarez2009, Haykin2009, Proakis2001.
In these domains, the approach is often denoted as base band representation or complex envelope.
Another common term coming from power electrical engineering is shifted frequency analysis (SFA).
In the following, the general approach of dynamic phasors for power system simulation is explained starting from the idea of bandpass signals.
This is because the 50 Hz or 60 Hz fundamental and small deviations from it can be seen as such a bandpass signal.
Futhermore, higher frequencies, for example, generated by power electronics can be modelled in a similar way.
Two Derivations of the Same Transform
The literature reaches the envelope description along two different routes. One
starts from the analytic signal and shifts the spectrum; the other averages a
sliding Fourier series. They are usually presented independently, under
different names, and it is not always stated that they arrive at the same
differential operator. The two routes are set out below so that the equivalence
is visible.
Route A: shifting the spectrum
A bandpass signal is one whose spectrum is concentrated in a band around some
carrier frequency $\omega_s$, narrow compared with $\omega_s$ itself. The
network voltages and currents of a power system are of this kind: energy sits
around the 50 Hz or 60 Hz fundamental and spreads only a little either side of
it during a transient.
where the complex quantity $X(t)$ is the envelope. It is obtained from the
analytic signal $x_a(t) = x(t) + j,\mathcal{H}{x}(t)$, with
$\mathcal{H}$ the Hilbert transform, by
$$X(t) = x_a(t)\, e^{-j \omega_s t}.$$
Multiplication by $e^{-j\omega_s t}$ translates the spectrum down by
$\omega_s$, so the band that sat around the carrier now sits around zero. That
translation is what the name shifted frequency analysis refers to. No
information is lost, and nothing is approximated: the operation is invertible
and $x(t)$ can be recovered from $X(t)$ at any instant.
The consequence that matters for simulation is what happens to the derivative.
Differentiating the expression above gives
An inductor $v = L,\frac{di}{dt}$ therefore satisfies
$V = L,\frac{dI}{dt} + j \omega_s L, I$. Two limiting cases are worth
noting. When the envelope is constant, the first term vanishes and what remains
is $V = j\omega_s L, I$, the classical steady-state phasor relation. When
$\omega_s = 0$, the second term vanishes and the envelope is the instantaneous
signal itself, which is the electromagnetic transient description. Both the
phasor and the instantaneous representation are special cases of the same
expression.
Route B: averaging a Fourier series
The dynamic phasor derivation starts elsewhere. Over a window of length
$T = 2\pi/\omega_s$ ending at time $t$, the waveform is expanded in a Fourier
series
$$x(t + s) = \sum_{k} X_k(t)\, e^{j k \omega_s s}, \qquad s \in (-T, 0],$$
whose coefficients are themselves functions of time,
These sliding-window coefficients are the dynamic phasors. Each one is constant
whenever the waveform is periodic and varies only as the waveform’s shape
changes, which is why they are also called generalized averages. Integrating by
parts gives the property the method is built on,
that is, the same shift as Route A, applied to the $k$-th coefficient.
Where the routes meet
Retaining only $k = 1$ makes Route B’s relation identical to Route A’s: the
first coefficient obeys $\frac{d}{dt} + j\omega_s$ and the network equations
that follow are the same. The two derivations produce one model.
The difference is emphasis rather than substance. Route A treats a single
carrier and is exact for any signal, so the accuracy question is only whether
the signal is genuinely bandpass around the carrier that was chosen. Route B
offers a whole family of coefficients, so it can carry a DC component in
$X_0$, the fundamental in $X_1$ and switching harmonics in higher $X_k$
simultaneously, at the cost of one complex state per coefficient retained.
Truncating that series is where the approximation enters: energy at frequencies
that were not given a coefficient is discarded, and the sliding window acts as a
low-pass filter on what remains, so envelope components varying as fast as
$\omega_s$ itself are attenuated.
This explains the split in usage. Work concerned with converter switching,
harmonic interaction or unbalanced and fault conditions keeps several
coefficients and speaks of dynamic phasors. Work concerned with transient
simulation of the fundamental keeps one and speaks of shifted frequency
analysis, emphasising the enlarged time step rather than the series. The
communications and microwave literature calls the same object the complex
envelope or the lowpass equivalent. The three names describe one transform, and
the choice between them says more about a paper’s lineage than about its
mathematics.
Relationship to Electromagnetic Transient Models
An electromagnetic transient (EMT) model solves for the instantaneous
quantities directly. Because the waveform it integrates oscillates at the
carrier, the step size has to resolve that oscillation, and the accuracy of the
result is bounded by how finely the fundamental period is sampled. A shifted
frequency model integrates the envelope instead. In sinusoidal steady state the
envelope is constant, so the step size is dictated by the bandwidth of the
transient rather than by the carrier, and it can be substantially larger for
the same accuracy. This is the reason the approach is attractive for transient
stability studies and for real-time simulation of large networks.
The saving is not free, and it is worth being precise about where it comes
from. It is not that the envelope model is a coarser description of the same
system; for a single carrier the transform is exact. It is that the carrier
oscillation has been removed from the quantity being integrated and folded into
the coefficient $j\omega_s$, which the model handles analytically rather than
numerically. What the envelope model cannot represent is content the retained
coefficients do not cover, which for a single-coefficient model means anything
outside the band around $\omega_s$.
Because $x(t) = \operatorname{Re}{X(t) e^{j\omega_s t}}$ holds at every
instant, converting between the two descriptions is an algebraic operation at a
given point in time, not a filtering or a fitting problem. That is what makes
mixed-domain and co-simulation setups possible: part of a network can be solved
in envelope form and part in instantaneous form, with the interface performing
the multiplication in one direction and the extraction of the envelope in the
other. Two conditions have to be respected at such an interface. The envelope
is defined relative to a chosen $\omega_s$ and a chosen time origin, so both
sides must agree on the carrier frequency and share a phase reference; and the
extraction direction needs the analytic signal, which for a general
instantaneous waveform cannot be formed from a single sample. Any delay
introduced between the two sides appears as a phase error on the carrier, and
that error grows with $\omega_s$ rather than with the envelope’s own rate of
change.
The state-space form of the shift, and how the resulting complex system is
solved as a real one, are covered in
State-Space Nodal.
4.3 - Powerflow
Steady-state solution of the network, and the bus types and iteration behind it.
The power flow problem is about the calculation of voltage magnitudes and angles for one set of buses.
The solution is obtained from a given set of voltage magnitudes and power levels for a specific model of the network configuration.
The power flow solution exhibits the voltages and angles at all buses and real and reactive flows can be deduced from the same.
Power System Model
Power systems are modeled as a network of buses (nodes) and branches (lines).
To a network bus, components such a generator, load, and transmission substation can be connected.
Each bus in the network is fully described by the following four electrical quantities:
$\vert V_{k} \vert$: the voltage magnitude
$\theta_{k}$: the voltage phase angle
$P_{k}$: the active power
$Q_{k}$: the reactive power
There are three types of networks buses: VD bus, PV bus and PQ bus.
Depending on the type of the bus, two of the four electrical quantities are specified as shown in the table below.
Bus Type
Known
Unknown
$VD$
$\vert V_{k} \vert, \theta_{k}$
$P_{k}, Q_{k}$
$PV$
$P_{k}, \vert V_{k} \vert$
$Q_{k}, \theta_{k}$
$PQ$
$P_{k}, Q_{k}$
$\vert V_{k} \vert, \theta_{k}$
Single Phase Power Flow Problem
The power flow problem can be expressed by the goal to bring a mismatch function $\vec{f}$ to zero.
The value of the mismatch function depends on a solution vector $\vec{x}$:
$$\vec{f}(\vec{x}) = 0$$
As $\vec{f}(\vec{x})$ will be nonlinear, the equation system will be solved with Newton-Raphson:
where $\Delta \vec{x}$ is the correction of the solution vector and $\textbf{J}(\vec{x})$ is the Jacobian matrix.
The solution vector $\vec{x}$ represents the voltage $\vec{V}$ by polar or cartesian quantities.
The mismatch function $\vec{f}$ will either represent the power mismatch $\Delta \vec{S}$ in terms of
where the vectors split the complex quantities into real and imaginary parts.
Futhermore, the solution vector $\vec{x}$ will represent $\vec{V}$ either by polar coordinates
This results in four different formulations of the powerflow problem:
with power mismatch function and polar coordinates
with power mismatch function and rectangular coordinates
with current mismatch function and polar coordinates
with current mismatch function and rectangular coordinates
To solve the problem using NR, we need to formulate $\textbf{J} (\vec{x})$ and $\vec{f} (\vec{x})$ for each powerflow problem formulation.
Of these four, DPsim currently implements the power mismatch function with polar
coordinates, detailed below. It is the formulation used by both the dense
(PFSolverPowerPolar) and sparse (PFSolverPowerPolarSparse) solvers; the other three
are not implemented.
Powerflow Problem with Power Mismatch Function and Polar Coordinates
Formulation of Mismatch Function
The injected power at a node $k$ is given by:
$$S_{k} = V_{k} I _{k}^{*}$$
The current injection into any bus $k$ may be expressed as:
We may define $G_{kj}$ and $B_{kj}$ as the real and imaginary parts of the admittance matrix element $Y_{kj}$ respectively, so that $Y_{kj} = G_{kj} + jB_{kj}$.
Then we may rewrite the last equation:
If we now perform the algebraic multiplication of the two terms inside the parentheses, and collect real and imaginary parts, and recall that $S_{k} = P_{k} + jQ_{k}$, we can express (1) as two equations: one for the real part, $P_{k}$, and one for the imaginary part, $Q_{k}$, according to:
These equations are called the power flow equations, and they form the fundamental building block from which we solve the power flow problem.
We consider a power system network having $N$ buses. We assume one VD bus, $N_{PV}-1$ PV buses and $N-N_{PV}$ PQ buses.
We assume that the VD bus is numbered bus $1$, the PV buses are numbered $2,…,N_{PV}$, and the PQ buses are numbered $N_{PV}+1,…,N$.
We define the vector of unknown as the composite vector of unknown angles $\vec{\theta}$ and voltage magnitudes $\vert \vec{V} \vert$:
The right-hand sides of equations (2) and (3) depend on the elements of the unknown vector $\vec{x}$.
Expressing this dependency more explicitly, we rewrite these equations as:
That is a system of nonlinear equations.
This nonlinearity comes from the fact that $P_{k}$ and $Q_{k}$ have terms containing products of some of the unknowns and also terms containing trigonometric functions of some the unknowns.
Formulation of Jacobian
As discussed in the previous section, the power flow problem will be solved using the Newton-Raphson method. Here, the Jacobian matrix is obtained by taking all first-order partial derivates of the power mismatch functions with respect to the voltage angles $\theta_{k}$ and magnitudes $\vert V_{k} \vert$ as:
The formulas above use the voltage magnitude $\vert V_k \vert$ as the unknown. The DPsim
implementation instead uses the relative voltage increment $\Delta \vert V_k \vert / \vert V_k \vert$,
which scales every voltage-magnitude column ($J^{PV}$, $J^{QV}$) by $\vert V_k \vert$. For
example $J_{jj}^{PV}$ becomes $P_j(\vec{x}) + G_{jj} \vert V_j \vert^2$. This is paired with
the multiplicative voltage update $\vert V_k \vert \leftarrow \vert V_k \vert (1 + \Delta \vert V_k \vert / \vert V_k \vert)$,
so the solution is identical; only the scaling of the voltage columns differs.
The linear system of equations that is solved in every Newton iteration can be written in matrix form as follows:
To sum up, the NR algorithm, for application to the power flow problem is:
Set the iteration counter to $i=1$. Use the initial solution $V_{i} = 1 \angle 0^{\circ}$
Compute the mismatch vector $\vec{f}({\vec{x}})$ using the power flow equations
Perform the following stopping criterion tests:
If $\vert \Delta P_{i} \vert < \epsilon_{P}$ for all type PQ and PV buses and
If $\vert \Delta Q_{i} \vert < \epsilon_{Q}$ for all type PQ
Then go to step 6
Otherwise, go to step 4.
Evaluate the Jacobian matrix $\textbf{J}^{(i)}$ and compute $\Delta \vec{x}^{(i)}$.
Compute the update solution vector $\vec{x}^{(i+1)}$. Return to step 3.
Stop.
Convergence and Step Control
The iteration is governed by two parameters:
Tolerance (default $10^{-8}$): the run is converged once every entry of the
mismatch vector satisfies $\vert f_{i}(\vec{x}) \vert <$ tolerance (an infinity-norm
test over all $\Delta P$ and $\Delta Q$ components).
Maximum iterations (default $20$): an upper bound on the number of Newton steps
per power flow solve.
To improve robustness far from the solution, the full Newton step is scaled by a
single factor $\alpha \in (0, 1]$ rather than damped component-wise. The factor is the
largest value that keeps the per-step changes within fixed bounds:
with $\Delta\theta_{max} = 0.2\ \text{rad}$ and $\Delta V_{max} = 0.1\ \text{pu}$. Because
the whole step is scaled by one factor, the Newton search direction is preserved, so
$\alpha = 1$ near the solution and quadratic convergence is retained; $\alpha < 1$ only
bounds large early steps. Voltage magnitudes are updated multiplicatively
($V_k \leftarrow V_k (1 + \alpha, \Delta V_k / V_k)$), consistent with the relative
voltage increment used in the Jacobian.
The formulation above is independent of DPsim. For the solvers it actually ships, their
convergence controls and the reactive power limit handling, see
power flow solvers.
4.4 - State-Space Nodal
Solving a component simultaneously with the network instead of through a delayed injection.
The state-space nodal (SSN) method represents a component by its own continuous
state-space model and couples it to the network through the nodal admittance
matrix. A component is described by
where $\mathbf{x}$ is the internal state, and the input $\mathbf{u}$ and output
$\mathbf{y}$ are the terminal quantities exchanged with the network (a voltage and
the corresponding current). Trapezoidal discretisation of $(\mathbf{A}, \mathbf{B})$
yields a discrete model $(\mathbf{A}_d, \mathbf{B}_d)$ and a Norton equivalent: a
constant conductance $\mathbf{W}$ stamped into the system matrix plus a history
current source recomputed each step from the previous state and input. Because the
component is solved simultaneously with the network in the same nodal system, SSN
is numerically robust without the parasitic snubbers that delayed
current-injection schemes require.
This builds directly on Nodal Analysis and is
the companion of
State-Space Extraction, which
recovers a state-space model from an MNA simulation rather than starting from one.
Shift to the Dynamic-Phasor Envelope
The same component model is discretised differently in an envelope domain: the operator becomes
$\frac{d}{dt} + j\omega_s$, so what is discretised is $\mathbf{A} - j\omega_s\mathbf{I}$ rather than
$\mathbf{A}$, and splitting the envelope into real and imaginary parts turns that into a real system
of twice the size. This is derived in full under
SSN across domains, together with why the equivalent
admittance is complex in an envelope domain and real in an instantaneous one.
The real-augmented form matches how the rest of the dynamic phasor system is already assembled: a
complex admittance $g = g_r + j g_i$ is stamped as the real block
$\left[\begin{smallmatrix} g_r & -g_i \ g_i & g_r \end{smallmatrix}\right]$, with real and
imaginary node parts in separate halves of a real-valued system. The SSN component therefore needs
no complex assembly of its own, and the trapezoidal discretisation used by the instantaneous models
applies unchanged.
Components
The single-phase dynamic-phasor SSN models are:
Full_Serial_RLC, a series resistor-inductor-capacitor one-port with a
hand-derived state-space model, used as the reference component.
GenericTwoTerminalVTypeSSN and GenericTwoTerminalITypeSSN, which accept a
user-supplied $(\mathbf{A}, \mathbf{B}, \mathbf{C}, \mathbf{D})$ and build the
V-type (voltage input, current output) or I-type (current input, voltage
output) stamping accordingly.
All three reproduce the classical dynamic-phasor stamping of the same circuit,
and the reconstructed time-domain waveform matches the EMT and EMT-SSN results
within discretisation error.
Three-Phase Components
The same real-augmented model extends per phase to DP::Ph3. The
$3 \times 3$ $\mathbf{A}$, $\mathbf{B}$, $\mathbf{C}$, $\mathbf{D}$ matrices
are general, so off-diagonal entries can couple the phases together:
Full_Serial_RLC, the three-phase series RLC one-port.
GenericTwoTerminalVTypeSSN and GenericTwoTerminalITypeSSN, the
three-phase generic V-type and I-type components.
As in the single-phase case, all three reproduce the classical three-phase
dynamic-phasor stamping exactly, and the reconstructed time-domain waveform
matches the EMT and EMT-SSN results within discretisation error once
corrected for the RMS-to-peak scaling that EMT::Ph3 sources apply and
DP::Ph3 sources do not, since the DP envelope already carries the complex
amplitude directly. The notebooks below only exercise the symmetrical,
diagonal case; coupling between phases is not covered by existing tests.
Variable Components
The components above are fixed-parameter LTI systems: $(\mathbf{A}, \mathbf{B},
\mathbf{C}, \mathbf{D})$ are built once from the component’s parameters and never
change. Some SSN components instead depend on the operating point and are
relinearized and re-stamped every step: the same simultaneous nodal solve, but around
a Jacobian frozen at the previous step’s converged state rather than a constant
matrix. The averaged grid-following inverter is one such component, and its
three-phase analogue carries a complex envelope per phase while sharing a single
positive-sequence dq control frame; see
Power Electronics for their state vectors and
model equations.
Validation and Examples
Two notebooks accompany the single-phase models, both on a single-carrier
series RLC one-port. examples/Notebooks/Circuits/DP_generalizedSSN_RLC.ipynb
validates the DP-SSN models against the classical dynamic-phasor stamping and
the EMT and EMT-SSN waveforms. examples/Notebooks/Circuits/DP_SSN_RLC_accuracy.ipynb
studies the time-step and frequency-dependent accuracy against a small-step
EMT reference.
The three-phase analogues,
examples/Notebooks/Circuits/DP_Ph3_generalizedSSN_RLC.ipynb and
examples/Notebooks/Circuits/DP_Ph3_SSN_RLC_accuracy.ipynb, repeat both
studies on DP::Ph3 circuits, including a current-driven network with generic
V-type and I-type components and a three-phase fault transient.
Further Reading
C. Dufour, J. Mahseredjian, and J. Bélanger, A Combined State-Space Nodal Method for the Simulation of Power System Transients, IEEE Transactions on Power Delivery, vol. 26, no. 2, pp. 928–935, 2011. https://doi.org/10.1109/TPWRD.2010.2090364
C. Dufour and D. S. Nasrallah, State-space-nodal rotating machine models with improved numerical stability, IECON 2016 – 42nd Annual Conference of the IEEE Industrial Electronics Society, 2016. https://doi.org/10.1109/IECON.2016.7793690
A. A. Kida, A. C. S. Lima, F. A. Moreira, J. R. Martí, and J. Tarazona, Inaccuracies due to the frequency warping in simulation of electrical systems using combined state–space nodal analysis, Electric Power Systems Research, vol. 223, art. 109657, 2023. https://doi.org/10.1016/j.epsr.2023.109657
4.5 - State-Space Nodal Across Domains
Why the same state-space component is discretised differently in an instantaneous and an envelope domain.
A component’s state-space model does not change between simulation domains. The same
$\boldsymbol{A}$, $\boldsymbol{B}$, $\boldsymbol{C}$ and $\boldsymbol{D}$ describe the same physics
whichever domain solves them. What changes is the operator that is discretised, and that difference
propagates all the way to whether the resulting nodal stamp is real or complex.
the trapezoidal rule gives a real discrete pair, and the equivalent admittance
$\boldsymbol{W} = \boldsymbol{C}\boldsymbol{B}_d + \boldsymbol{D}$ is real.
The envelope case
In an envelope domain the state is a complex envelope $\tilde{\boldsymbol{x}}$ carrying an implicit
$e^{j\omega_s t}$. Differentiating that product contributes the carrier term derived under
dynamic phasors, so the operator seen by the envelope is
and the system that must be discretised is not $\boldsymbol{A}$ but
$\boldsymbol{A} - j\omega_s \boldsymbol{I}$.
The real-augmented form
Rather than integrate a complex system, split the envelope into real and imaginary parts. The
shifted operator becomes a real system of twice the size,
The off-diagonal $\pm\omega_s \boldsymbol{I}$ blocks are the carrier rotation, and the block
structure $\begin{bmatrix} P & -Q \ Q & P \end{bmatrix}$ is the real representation of the complex
number $P + jQ$. Discretising this real system with the same trapezoidal rule and recombining the
blocks recovers the complex discrete pair, from which the equivalent admittance and the history term
follow exactly as in the instantaneous case.
Two things follow. The equivalent admittance is complex in an envelope domain and real in an
instantaneous one, so the same component stamps differently. And setting $\omega_s = 0$ collapses
the augmented system back to the instantaneous one, which is the general statement about the
envelope transform applied here: the instantaneous formulation is the zero-carrier special case, not
a separate method.
Why this matters for accuracy
The carrier rotation is handled analytically, inside $\boldsymbol{A}$, rather than numerically by
resolving the oscillation with small steps. For a component whose envelope varies slowly against a
fast carrier, the step size is then set by the envelope’s own bandwidth rather than by the carrier
frequency. That is the entire accuracy argument for using an envelope domain here, and it fails for
exactly the reason it succeeds: content outside the retained band around $\omega_s$ has no
representation at all.
4.6 - State-Space Extraction
Recovering a state-space model of the whole network from the solver.
A nodal simulation computes a trajectory: given a set of sources and initial conditions it
produces the node voltages step by step. It does not, by itself, say anything about the system’s
modes, its damping, or how close it is to instability. Those questions are answered by the
state-space description, which the extraction recovers from the discretised network that the
simulation is already solving. The result is the discrete-time model of exactly what is being
simulated, including the effect of the discretisation itself, rather than of an idealised
continuous system that the simulation approximates.
Where the Discrete Model Comes From
The starting point is the companion form described under
nodal analysis. Each dynamic element has already been turned
into a conductance in parallel with a history current source, and its history term is a
difference equation in the element’s own quantity: the current for an inductor, the voltage for a
capacitor. Those quantities are the states. The network solve then supplies the terminal voltages
that drive them.
This gives a system in two parts rather than one. The states advance using their own local
recurrence together with the network solution at the new instant, and the network solution at
that instant is itself driven by the states carried over from the previous one.
The resulting matrix $\mathbf{A}_{d}$ describes the homogeneous discrete-time dynamics of the EMT MNA simulation model at the current operating point and system-matrix configuration.
The elimination is written as a solve rather than as an inverse for a reason. $\mathbf{Y}$ is
sparse and already factorised for the time stepping, so the term is obtained by one forward and
backward substitution per column of $\mathbf{C}_{d,\mathrm{MNA}}$, that is, one per extracted
state. Forming $\mathbf{Y}^{-1}$ explicitly would discard the sparsity and cost far more than the
extraction itself.
Interpreting the Result
The eigenvalues of $\mathbf{A}_{d}$ are discrete-time modes, so they are read against the unit
circle rather than against the imaginary axis. A mode is stable when $|\lambda_d| < 1$, and the
closer it sits to the unit circle the more lightly damped it is. A corresponding continuous-time
eigenvalue follows from
$$\lambda_c = \frac{\ln \lambda_d}{\Delta t},$$
whose real part gives the damping and whose imaginary part gives the oscillation frequency, up to
the ambiguity that any frequency above the Nyquist rate of the time step is indistinguishable
from one below it.
It is worth being clear about what this eigenvalue belongs to. It is a mode of the discretised
system, not of the underlying continuous one. The two differ by the distortion the integration
rule introduces, and for the trapezoidal rule that distortion grows with frequency, so the fastest
modes are the least faithful. Reducing the time step reduces the discrepancy; comparing extraction
results across two time steps is a practical way to see which modes are trustworthy.
Validity
The extracted model is linear and time invariant, and it describes the system only for the
configuration it was taken from. Any event that changes $\mathbf{Y}$, a switch opening or closing
above all, produces a different $\mathbf{A}_{d}$, so a switching study means one extraction per
configuration rather than one for the simulation. The same holds for non-linear elements, whose
contributions are those at the operating point reached when the extraction was performed; moving
the operating point requires extracting again.
References
J. A. Hollman and J. R. Marti, Step-by-step eigenvalue analysis with EMTP discrete-time solutions, IEEE Transactions on Power Systems, 2010. https://doi.org/10.1109/TPWRS.2009.2039810
Y. Han, H. Sun, B. Huang, S. Qin, M. Mu, and Y. Yu, “Discrete-Time State-Space Construction Method for SSO Analysis of Renewable Power Generation Integrated AC/DC Hybrid System,” IEEE Transactions on Power Systems, 2022. https://doi.org/10.1109/TPWRS.2021.3115248
4.7 - Modal Analysis
Reading the modes of an extracted state-space model, and what participation factors say about them.
Extracting a state-space model, as described under
state-space extraction, produces a discrete state
matrix. Its eigenvalues describe how the system behaves without simulating it: which oscillations
exist, how fast each decays, and which states are involved in each one.
From discrete to continuous eigenvalues
The extracted model is discrete, so its eigenvalues $z$ live in the complex plane where stability
means $|z| < 1$. That is awkward to read, because the quantities of interest are a frequency in
hertz and a damping ratio, both of which are natural in the continuous plane.
Because the model was discretised with the trapezoidal rule, the mapping back is its inverse, the
bilinear transform
This maps the interior of the unit disc onto the left half plane exactly, so a mode that is stable
in one description is stable in the other, with no threshold effects at the boundary. From
$\lambda = \sigma + j\omega$ the damped frequency is $\omega / 2\pi$ and the damping ratio is
$-\sigma / |\lambda|$.
The mapping is exact for the discretisation used, not an approximation of it. What it cannot undo is
the frequency warping the trapezoidal rule introduced in the first place: a continuous mode at a
frequency approaching the Nyquist rate is represented at a shifted frequency in the discrete model,
and mapping back returns the shifted value rather than the original. Modes well below Nyquist are
unaffected; modes near it should not be read literally.
What the eigenvectors say
The eigenvalues say which modes exist but not which parts of the system take part in them. The right
eigenvectors describe how each mode appears in the states, the left eigenvectors describe how
strongly each state excites each mode, and the product of the two, element by element,
$$p_{ki} = \phi_{ki} \, \psi_{ik},$$
is the participation factor of state $k$ in mode $i$.
Participation factors are the useful output. A poorly damped oscillation is a number; knowing that
two particular machine rotor states dominate it is actionable. They are also dimensionless and
normalised in a way that makes them comparable across states with different units, which raw
eigenvector entries are not.
The computation requires the eigenvector matrix to be invertible. It is not, when the state matrix
is defective, meaning it has a repeated eigenvalue without a full set of independent eigenvectors.
This is not a numerical failure but a property of the system, and it is the one case where
participation factors are not defined at all.
Choice of frame
The states of the extracted model are in whatever frame each component works in, which for a network
containing machines means several rotating frames turning at different speeds plus the network’s own.
Eigenvalues of that system are still correct, but the modes mix frames, and a mode’s frequency is
then relative to whichever frame its dominant states live in.
Transforming everything into one common frame before the analysis removes that ambiguity, at the
price of choosing the frame and its initial angle. The two choices are therefore: analyse in the
native frames and read each mode relative to its own states, or transform to a single frame and read
every frequency against the same reference. The second is what makes modes from different machines
directly comparable.
Limits
The analysis is linear and local. It describes the system as it is at the operating point where the
model was extracted, and says nothing about behaviour after a large disturbance moves it elsewhere.
A system can be comfortably damped at its nominal point and not at another, so a single modal
analysis is evidence about one condition rather than about the system.
4.8 - Alternative Solution Methods
Differential-algebraic, ordinary differential and torn-network formulations, and when each is preferable to nodal analysis.
Nodal analysis is not the only way to advance a network in time.
Three other formulations exist, each answering a different objection to it.
Differential-algebraic formulation
Nodal analysis discretises each dynamic element separately, replacing it with a companion model, and
then solves an algebraic system. The step size is chosen in advance and applies to everything.
A differential-algebraic formulation instead writes the whole system as it stands,
mixing the differential equations of the dynamic elements with the algebraic constraints of the
network, and hands it to an integrator that chooses its own order and step size to meet a requested
error tolerance. Each component contributes its residual, the part of $\boldsymbol{F}$ it is
responsible for, rather than a companion model.
What this buys is error control. The user asks for an accuracy rather than a step size, and the
integrator takes small steps through fast transients and long ones through quiet intervals. What it
costs is determinism: the number of steps and the work per step are not known in advance, which
rules the method out for real-time execution and makes run times hard to predict. It is a method for
producing reference results, not for meeting a deadline.
The formulation also admits systems that nodal analysis handles awkwardly, because a constraint does
not have to be rewritten as an admittance to participate.
Ordinary differential formulation for a component
A weaker version of the same idea applies to one component rather than the whole network. The
network is still solved by nodal analysis at a fixed step, but a single component whose internal
dynamics are stiff or awkward is integrated by a dedicated solver between network steps.
The component exposes its state derivative, the solver advances it over the network step, and the
result re-enters the network as an ordinary companion contribution. This keeps the deterministic
outer loop while allowing one component to be integrated more carefully than the rest. The
limitation is the same one as any staggered coupling: the component sees the network’s terminal
conditions from the beginning of the step, so the exchange is only first-order accurate however
carefully the inner integration is done.
Tearing a network
The third objection is about size rather than accuracy. Factorising the system matrix costs
super-linearly in the number of nodes, so one large network is more expensive than the sum of its
parts.
Diakoptics exploits this. A small set of branches is removed, chosen so that what remains falls into
independent subnetworks. Each subnetwork is factorised on its own, which is cheap, and the removed
branches are restored by solving a small dense system in the currents through them,
where $\boldsymbol{K}$ records which nodes each removed branch connected and
$\boldsymbol{Z}_{tear}$ holds their impedances. The bracketed matrix has the dimension of the number
of torn branches, so the method pays off exactly when a network can be split by cutting few
branches.
The result is not an approximation. Unlike the delay-based decoupling described under
branches, which introduces a travel time and therefore an error,
tearing reproduces the solution of the intact network to within round-off, because the removed
branches are restored within the same step. The trade is that the subnetworks cannot be advanced
independently: the small system couples them, so they must be solved together each step.
Choosing where to tear is the part with no general answer. Too few cuts leave the subnetworks large,
too many make the dense system dominate, and a cut through a strongly coupled part of the network
produces subnetworks whose solutions are dominated by the correction.
4.9 - Models
Mathematical description of the models implemented in DPsim.
Each page in this section derives one component family: what it represents, the equations that
describe it, and what those equations assume. None of them names a class or a file. How a model is
arranged in code is in the Developer Guide, and which domains
implement which model is in
model availability, which is generated from
the source and is therefore the authoritative answer to that question.
The domains
A model is written per simulation domain and per phase count, so the same component may exist as a
single-phase dynamic phasor model, a three-phase electromagnetic transient model, or both.
EMT carries instantaneous waveforms, DP carries complex envelopes of those waveforms around a
carrier frequency, and SP carries steady-state phasors. Ph1 is single phase, usually a positive
sequence representation, and Ph3 is three phase. The three domains are not separate methods but
cases of one envelope description, which is the subject of
dynamic phasors.
The practical consequence is that the domain determines what a model can represent, not only how
fast it runs. An envelope domain cannot represent content outside the band it retains, whatever step
size is used.
How the models group
Passive elements and branches.RLC elements covers the elements
every other model is built from. Branches covers the lines connecting
two nodes, including the travelling-wave line. Transformer covers the
two-winding transformer and the ideal transformer within it.
Sources, switches and loads.Sources explains why a current source
costs nothing while a voltage source extends the system matrix.
Switches and loads cover the two-resistance
switch and the impedance and current representations of demand. These three share one theme: a
choice that looks physical is usually a numerical trade-off.
Machines.Synchronous generator covers the full-order and
transient-stability machines, and
reduced order the voltage-behind-reactance
family from third to sixth order. The
regulators that drive them, exciters, turbines and
governors, sit alongside.
Converters.Power electronics covers the averaged inverter
models, and converter control the phase-locked loop, the
oscillator and the cascaded control that distinguish grid-following from grid-forming behaviour.
Signal blocks.Signal processing covers the integrators,
filters and generators the other models are assembled from. These carry no current and connect to no
node.
4.9.1 - RLC-Elements
Resistance, inductance and capacitance, and the companion models they discretise to.
EMT Equations and Modified Nodal Analysis
Inductance
An inductance is described by
$$v_j(t) - v_k(t) = v_L(t) = L \frac{\mathrm{d} i_L(t)}{\mathrm{d}t}$$
Integration results in an equation to compute the current at time $t$ from a previous state at $t - \Delta t$.
There are various methods to discretize this equation in order to solve it numerically.
The trapezoidal rule, an implicit second-order method, is commonly applied for circuit simulation:
To simulate the transient behavior of circuits, this linear equation has to be solved repeatedly.
As long as the system topology and the time step is fixed, the system matrix is constant.
Extension with Dynamic Phasors
The dynamic phasor concept can be integrated with nodal analysis.
The overall procedure does not change but the system equations are rewritten using complex numbers and all variables need to be expressed in terms of dynamic phasors.
Therefore, the resistive companion representations of inductances and capacitances have to be adapted as well.
Inductance
In dynamic phasors the integration of the inductance equation yields
$$\begin{align}
\langle v_L \rangle(t) &= \Big \langle L \frac{\mathrm{d} i_L}{\mathrm{d}t} \Big \rangle(t) \nonumber \\
&= L \frac{\mathrm{d}}{dt} \langle i_L \rangle(t) + j \omega L \ \langle i_L \rangle(t)
\end{align}$$
Both line models below are composite components: they do not stamp the system matrix directly
but are built from resistor, inductor and capacitor subcomponents, each of which contributes its
own stamp. See subcomponents for how that
composition works, and RLC elements for the stamps of the
individual elements.
The transformer is documented separately under transformer.
RX-Line
The RX line represents a line by its series resistance and series inductance only, ignoring the
shunt admittance. It is the appropriate choice for short lines, where the charging current is
negligible, and it is what the CIM reader produces for an ACLineSegment when no shunt data is
present.
The model is composed of a series resistor and a series inductor between the two terminals:
$$\underline{Z} = R + j \omega L$$
An additional resistor from the inductor terminal to ground is present to make initialisation
well posed. It is not part of the physical model.
RxLine exists in DP::Ph1, EMT::Ph3, SP::Ph1 and SP::Ph3.
PI-Line
The PI line adds the shunt admittance of the line, split evenly between the two terminals, which
matters once the line is long enough for the charging current to affect the result. The name
comes from the shape of the equivalent circuit: a series branch with one shunt branch at each
end.
The series branch carries the resistance and inductance as above. Each terminal additionally
carries half of the total shunt capacitance and half of the total shunt conductance:
The shunt capacitance and conductance are specified as totals for the line, and the halving between
the two ends is part of the model rather than something the user does.
Decoupling Line
The decoupling line is a distributed parameter line based on the Bergeron travelling wave
method. Unlike the two models above it is not primarily a fidelity improvement: its purpose is
to remove the direct coupling between the two terminals so that the network on either side can
be solved as an independent system, which is what makes splitting a network across solvers or
across simulators possible.
The method rests on the behaviour of a lossless line. For a line with distributed inductance and
capacitance, the quantity $v + Z_c, i$ observed at one end reappears unchanged at the other end
one travel time later, and likewise in the opposite direction. Nothing propagates faster than
that travel time, so the two ends cannot influence each other within it. The surge impedance and
the travel time follow from the line’s total inductance and capacitance,
Each terminal is then represented by a resistance to ground in parallel with a current source.
The resistance is $Z_c + R/4$, and the current source carries the history term, whose value
depends on the voltage and current recorded at the other terminal one travel time ago. Because
that value is already known when the step begins, it enters the system as a constant injection
rather than as a coupling into the admittance matrix, and the matrix separates into two blocks
that can be factorised and solved independently.
The series resistance is not distributed along the line. It is lumped, with $R/4$ placed at each
end and the remainder in the middle of the equivalent, which is why the terminating resistance
and the history coefficients carry $R/4$ terms rather than the full $R$.
The travel time is not required to be a whole number of time steps. The recorded quantities are
held in a buffer of $\lceil \tau / \Delta t \rceil$ samples and the value one travel time ago is
recovered by linear interpolation between the two nearest entries. The one hard requirement is
that the travel time exceed the time step; a line whose $\tau$ is shorter than $\Delta t$ cannot
decouple anything, and setting one up is rejected rather than silently approximated.
In the dynamic phasor domain the history terms carry an additional rotation $e^{-j \omega_s
\tau}$. This is a direct consequence of working with envelopes: a delay of $\tau$ applied to an
instantaneous waveform becomes, for the envelope, the same delay together with a phase rotation
of the carrier over that interval, as described under
dynamic phasors. Note that this rotation is
currently evaluated at a fixed 50 Hz rather than at the system frequency in use.
The decoupling is exact for the lossless travelling wave line it is derived from. The error
introduced in practice comes from the lumped treatment of the series resistance and from the
interpolation of the delayed quantities, and it grows as the time step approaches the travel
time.
Choosing between them
Use the RX line when the shunt admittance can be neglected and you want the smaller system
matrix, since the PI line introduces additional nodes for its shunt branches. Use the PI line
when the line is long enough that its charging current matters, or when you are comparing
against a reference tool that models the shunt branch.
Both are lumped parameter models and therefore do not reproduce travelling wave behaviour. Use
the decoupling line when you need that behaviour, or when the reason for reaching for a line
model is to split the network in the first place. For the domains each model is available in,
see model availability.
4.9.3 - Transformer
Two-winding transformer, the ideal part that extends the matrix, and the snubbers.
2-Winding Transformer
The transformer model is composed of an RL-segment and an ideal transformer.
The single line diagram is depicted in the figure below.
If node reduction is not applied, two virtual nodes are created to stamp this model into the system matrix.
Furthermore, the ideal transformer has an additional equation, which requires an extension of the system matrix.
The complete matrix stamp for the ideal transformer is
$$\begin{array}{c|c c c}
~ & j & k & l \cr
\hline
j & & & -1 \cr
k & & & T \cr
l & 1 & -T & 0
\end{array}
\begin{pmatrix}
v_j \cr
v_k \cr
i_{l} \cr
\end{pmatrix}
=
\begin{pmatrix}
\cr
\cr
0\cr
\end{pmatrix}$$
The variable $j$ denotes the high voltage node while $k$ is the low voltage node.
$l$ indicates the inserted row and column to accommodate the relation between the two voltages at the ends of the transformer.
The transformer ratio is defined as $T = V_{j} / V_{k}$.
A phase shift can be introduced if $T$ is considered as a complex number.
Why the ideal part needs an extra equation
The ideal transformer imposes two constraints at once: the voltages are in a fixed ratio and the
powers on the two sides are equal, which makes the currents inversely proportional to the same ratio,
$$\frac{v_j}{v_k} = T, \qquad i_k = -T \, i_j .$$
Neither is a current balance at a node, so neither can be written as an admittance. This is the same
situation as an ideal voltage source described under sources: the
system is extended with the branch current as an unknown, the constraint occupies the added row, and
the added diagonal entry is zero. The asymmetry of the stamp, $-1$ against $T$ in the added column
and $1$ against $-T$ in the added row, is exactly the statement that voltage scales by $T$ while
current scales by $1/T$ with opposite sign.
Making $T$ complex adds a phase shift, which is how a delta-wye connection is represented without
modelling the windings. The magnitude and the angle then carry the tap ratio and the vector group
respectively.
Series impedance and the direction of the ratio
The winding resistance and leakage inductance are lumped into one series branch on one side of the
ideal part rather than split between the two sides. Referring an impedance across an ideal
transformer scales it by $T^2$, so the choice of side is a choice of reference, not an
approximation, and the parameters have to be given consistently with it.
The ratio is defined greater than one, from high voltage to low. Supplying it the other way round
describes the same physical device but with the two ends exchanged, so a transformer given an
inverted ratio has to have its terminal assignment inverted with it to remain the same transformer.
Numerical damping
Connecting an inductive branch between two nodes that have no other path to ground leaves those
nodes weakly defined, and the resulting matrix can be poorly conditioned or singular. Small shunt
elements at each terminal remove that, at the cost of a negligible current that would not exist in
the physical device.
Those elements are sized from the transformer’s rated power, which makes the rating a required
parameter rather than documentation. Without a positive rating there is no scale to size them
against, and the natural result is an infinite resistance and a zero capacitance whose admittance is
not a number. A single such entry propagates through the factorisation and destroys the whole
solution, not merely the transformer, so the rating cannot be treated as optional.
4.9.4 - Ideal Transformer Model
Splitting a circuit at any point with a controlled source pair, and what the delay costs.
The Ideal Transformer Model (ITM) is a signal component that splits a circuit into two subcircuits, using a common node as a Point of Common Coupling (PCC), in such a way that a copy of this node is found in the two subcircuits, as shown in Fig. 1, where the copies of the node are denoted as $n$ and $m$. Moreover, the circuits are coupled using a controlled voltage source and a controlled current source, which exchange their interface currents and voltages, respectively, namely the interface signals. This exchange takes place using a ring buffer, on top of which a second ring buffer has been implemented to emulate a co-simualtion using a macro-step, which means that the exchange of interface signals can be made at an interval larger than the simulation’s step size. This second ring buffer is used to implement Zero- and First-Order hold extrapolation methods, while the first ring buffer allows to linearly interpolate the value of the signal at the current time step, in case the delay between both subcircuits is not an integer multiple of the step size.
Fig. 1: Ideal Transformer Model Circuit diagram.
To add an ITM, users must split the cirtuit and create the copies of the PCC node. An example of this process can be found in the Notebook ITM.ipynb.
To avoid connections of the controlled voltage source with a capacitor, or the controlled current source with an inductor, the resistors $R_{\mathrm{series}}$ and $R_{\mathrm{parallel}}$ are included.
Why the resistors are necessary
The two failure cases they prevent are the same one seen twice. A voltage source directly across a
capacitor over-determines that node: both impose a voltage, and the capacitor’s companion model and
the source’s constraint row describe the same quantity. A current source in series with an inductor
under-determines the branch in the dual way, since both impose a current. In each case the system
matrix becomes singular rather than merely ill-conditioned, so the resistors are a condition for the
method to work at all and not a refinement of it.
Their values are a compromise of the kind described under switches.
Small enough to be electrically negligible, large enough not to dominate the condition number.
What the delay costs
The exchanged signals are always at least one step old, because each side computes from what the
other produced previously. That delay is the reason the two subcircuits can be solved separately at
all, and it is also the entire error of the method: the coupled system is not the original circuit
but the original circuit with a transport delay inserted at the point of common coupling.
The consequence is that accuracy is governed by how much the interface signals change within one
exchange interval, not by how accurately either side is solved internally. Refining the step inside
a subcircuit while holding the macro-step fixed improves nothing at the interface.
A macro-step larger than the simulation step makes this explicit, which is the point of the second
ring buffer: it is the co-simulation case, where the two sides may be different tools exchanging at
a rate neither controls. Zero-order hold holds the last received value for the whole interval;
first-order hold extrapolates linearly from the last two. The first is safe and lags; the second
tracks a smoothly varying signal better and overshoots at a discontinuity, which is exactly what a
fault produces.
Contrast with the alternatives
Three ways of splitting a network appear in this documentation and they differ in what they cost.
Tearing, described under
alternative solution methods, is exact: the
removed branches are restored within the same step, so the answer matches the intact network. It
does not allow the parts to be advanced independently.
The travelling-wave line under branches is exact for the lossless
line it derives from, and its delay is physical rather than introduced. It requires that a real line
with a travel time longer than the step exists at the splitting point.
The ideal transformer model requires no such line and can split anywhere, and pays for that with a
delay that has no physical counterpart. It is the general method and the least accurate of the
three.
4.9.5 - Switches
Two-resistance switching and the variable-resistance switch used for faults.
A switch in a nodal formulation is not an ideal open or short. Both would be singular: an ideal
short shorts two node equations together, and an ideal open leaves a node with no path to ground.
Switches are therefore represented by a finite resistance that takes one of two values.
The two-resistance model
The switch contributes a single admittance between its two terminals,
stamped as a conductance between the two terminal nodes, with the usual reduction when one terminal
is grounded. Typical values are far apart, of the order of milliohms closed and megohms open, so the
switch is a near short or a near open without ever being singular.
The consequence of this choice is that the ratio $R_{open} / R_{closed}$ lands directly in the
condition number of the system matrix. Making the contrast arbitrarily large to approach an ideal
switch degrades the accuracy of every node voltage in the network, not only those near the switch.
The values are a numerical compromise, not a physical measurement.
Because the admittance appears in the system matrix rather than in the right hand side, changing
state requires the matrix to be refactorised. This is why a network that switches often costs more
than one that does not, even though the model itself is trivial.
Why a step change in resistance is a problem
Opening a switch that carries inductive current asks the network to interrupt that current within
one time step. The inductor opposes it, and with the trapezoidal companion model the result is a
numerical oscillation across the switch: the current alternates sign at the step frequency and
decays slowly, contaminating the solution for many steps after the event.
This is a property of the discretisation, not of the physical circuit. The physical arc that would
form across real contacts dissipates that energy; a two-valued resistance has no equivalent
mechanism.
The variable-resistance switch
The variable-resistance switch removes the oscillation by refusing to make the change in a single
step. On opening, the resistance is multiplied by a fixed factor each step,
$$R[k+1] = \alpha \, R[k], \qquad \alpha > 1,$$
until it reaches the target open value, after which it is held there. The current therefore decays
geometrically over several steps rather than being interrupted at once, which is close to what an
arc does and which the trapezoidal companion model can follow without ringing.
The growth factor is tied to the step size so that the transition covers a comparable interval of
time rather than a comparable number of steps. Closing is not ramped: the resistance is taken
straight to its closed value, because energising a path through a small resistance does not produce
the same interruption problem.
The cost is that the system matrix changes on every step of the transition rather than once, so each
of those steps requires a refactorisation. The switch is worth its cost where the interruption is
severe, typically a fault applied at a machine terminal or a transformer winding, and unnecessary
for ordinary load switching.
Series switching
Where a switch is combined with the series resistance it energises, the two are represented as one
element rather than as a switch plus a resistor. This keeps the branch to a single admittance and
avoids introducing an internal node that carries no physical meaning and adds an equation to the
system.
4.9.6 - Loads
Constant impedance and constant current representations of a load, and what each assumes.
A load is specified as an active and a reactive power at a nominal voltage, but a nodal solver needs
either an admittance or a current. The two ways of making that conversion behave differently as the
terminal voltage moves away from nominal, and the difference matters more than the model’s
simplicity suggests.
Constant impedance
The powers are converted once, at the nominal voltage, into a resistance and a reactance,
$$R = \frac{V_{nom}^2}{P}, \qquad X = \frac{V_{nom}^2}{Q},$$
and the reactance becomes an inductance or a capacitance according to its sign,
The load is then an ordinary passive branch to ground, and it is stamped exactly as the elements it
is built from.
Both conversions divide by a power, so a load with zero active power has no defined resistance and
one with zero reactive power has no defined reactance. Such a branch is simply absent rather than
infinite, which is the correct behaviour but means a load specified with one of the two set to zero
is not the load a reader might expect.
The assumption is that consumption follows the square of the voltage. At nominal voltage the load
draws exactly $P$ and $Q$; at 0.9 per unit it draws 81 percent of them. For a genuinely impedance
like load this is right, and for anything regulated it understates the demand during a depression.
Constant current
The alternative injects a current derived from the specified power,
held fixed as the terminal voltage varies. Consumption then falls linearly with voltage rather than
quadratically, which is closer to the behaviour of many aggregated loads.
Note what this is not. Because the current is computed from the nominal voltage and not from the
present terminal voltage, this is a constant current model and not a constant power one. A true
constant power load would require the current to be recomputed from the solved voltage at every
step, making the component nonlinear and the nodal solve iterative. The linear model is used
because it keeps the system matrix constant.
Which to use
The three canonical load characteristics are constant impedance, constant current and constant
power, differing in whether demand follows the square of voltage, the voltage, or neither. Only the
first two are available as linear models. For a voltage excursion of a few percent the choice
changes little; for a deep depression during a fault it changes the answer materially, and the
constant impedance model is the optimistic one because it sheds load exactly when the network is
weakest.
Shunts
A shunt is specified directly as a conductance and a susceptance rather than as a power, so no
conversion is involved. It is the natural representation for a capacitor bank or a reactor, where
the rating is an admittance and the consumed power is a consequence of the voltage rather than the
specification.
4.9.7 - Sources
Ideal and non-ideal sources, and what each costs the solver.
A source imposes a quantity on the network. Which quantity it imposes, and whether it does so
exactly, determines how it enters the system of equations and what it costs.
Current sources are free, voltage sources are not
A current source imposes a known current into a node. Its contribution is entirely on the right hand
side of the nodal equations, and the system matrix does not know it exists.
A voltage source imposes a relation between two node voltages, which is not a nodal equation at all.
Nodal analysis has one equation per node expressing current balance, and there is no current
variable for an ideal voltage source to appear in. The system is extended with the source current as
an unknown and with the constraint that fixes the voltage difference, as described under
nodal analysis. The matrix grows by one row and
column per source, and the added diagonal entry is zero, so the extended matrix is no longer
positive definite and cannot be factorised by methods that assume it is.
This asymmetry is the reason so many models are formulated as current injections even when what they
physically represent is a voltage behind an impedance.
The Norton equivalent
A voltage source with a series resistance can avoid the extension entirely. Source transformation
replaces a voltage $V$ behind a resistance $R$ with a current $V/R$ in parallel with the same
resistance,
$$I_{eq} = \frac{V}{R}, \qquad G = \frac{1}{R},$$
which contributes a conductance to the matrix and a current to the right hand side. No extra
unknown, no zero on the diagonal, and the matrix stays the shape it would have had without the
source.
The two representations are equivalent at the terminals, exactly, for any $R$ that is not zero. The
choice is therefore numerical rather than physical, and the cost is that the source is no longer
ideal: its terminal voltage falls with the current drawn. Where a genuinely stiff source is wanted,
$R$ has to be made small, and a small $R$ means a large conductance, which is the same conditioning
trade-off that appears in switches.
Sources that change over time
The simplest time-varying source takes its value from a generator, as described under
signal processing blocks.
A ramp source is more specific: it holds one value, then moves to a second over a defined interval,
and holds that. The subtlety is what happens when the ramp changes not only the magnitude and phase
but also the frequency. Interpolating a frequency linearly and applying it as if it had always been
in force produces a phase discontinuity at both ends of the ramp, because phase is the integral of
frequency and not its product with time. Blending the frequency contribution in and out smoothly
over the ramp interval avoids that, at the price that the frequency during the transition is not the
linear interpolation it appears to be.
A profile source takes its value from a recorded sequence instead of from a formula, stepping
through samples as the simulation advances. It is the right choice when the excitation comes from a
measurement, and it carries the obvious constraint that the sample rate and the simulation step must
be reconciled: a profile is silent about what happens between its samples, and the simulation will
ask.
Controlled sources
A controlled source takes its reference from another quantity in the simulation rather than from a
parameter or a clock. This is what allows a component to be built out of sources: a converter
imposes a voltage its control law computed, and an interface between two solvers imposes a value the
other side produced.
The distinction from a time-varying source is that the reference is not known in advance. Since the
reference is read as an input rather than solved simultaneously, it is the value from the previous
step, which introduces a delay of one step into whatever loop the source closes. For a control loop
that is usually acceptable and always worth knowing about; for a coupling between two solvers it is
the central property of the method, and it is the subject of
branches where the same delay is used deliberately.
4.9.8 - Network Injection and Compensation
Representing the rest of the grid, and the compensators that regulate voltage against it.
Every simulated network stops somewhere. What lies beyond the boundary has to be represented by
something, and once it is, the question of holding voltage at a bus becomes a question about what
that representation will support.
The external network
The rest of the grid is represented as an ideal voltage source behind no impedance: a bus whose
voltage is imposed and whose current is whatever the network draws. This is the slack of the
powerflow carried into the time domain, and it supplies unlimited power at a fixed voltage and
frequency.
That idealisation is the right one when the external system is genuinely much stiffer than what is
being studied, and it is misleading when it is not. A stiff boundary suppresses exactly the
behaviour that a weak grid study is about: it holds the voltage the compensator is supposed to be
regulating and absorbs the power swings the converters are supposed to be sharing. Placing an
impedance between the source and the network is what makes the boundary finite, and the short
circuit ratio it produces is a modelling decision rather than a detail.
Because the imposed voltage comes from a signal generator rather than a constant, the boundary can
also be driven: a frequency ramp to study the response to rate of change of frequency, or a
modulated frequency to probe a control loop. The boundary then becomes the disturbance source rather
than the reference.
Static reactive compensation
A static compensator regulates bus voltage by varying a shunt susceptance. It exchanges reactive
power only, so it can raise or lower voltage but supplies no energy.
The regulator measures the bus voltage through a first-order lag, forms the per-unit error against a
reference, and drives the susceptance through a further first-order lag with gain $K_r$ and time
constant $T_r$,
both lags integrated with the trapezoidal rule so that the controller and the network advance
consistently.
Two properties follow from the physics rather than from the controller. The susceptance is bounded
at both ends by the installed capacitive and inductive ratings, and the regulator saturates against
those bounds rather than failing; a compensator sitting on its limit is providing everything it has
and the voltage error persists. And because the device is a susceptance rather than a source, the
reactive power it delivers falls with the square of the voltage. It is weakest exactly when the
voltage is lowest, which is when it is most needed.
The measurement lag matters more than it appears. It sits inside the regulator loop, so it is not
merely a smoothing of the reported value; making it small to track faster couples the compensator to
noise, and making it large delays the response into a range where it can interact with nearby
machine controls.
Discrete compensation
Where the compensation is switched rather than continuous, the control is a different kind. The
regulator compares the voltage error against a deadband, and only if the error exceeds it does it
move by one discrete step, in the direction that reduces the error, subject to end stops.
The deadband is not a refinement but the central element. Without it any measurement noise drives
continual switching, and the switching is mechanical and finite in life. With it, the steady-state
voltage is not the reference but anywhere within a band around it, which is the accepted cost.
Power flow control
A device that connects two systems through a converter pair rather than through a magnetic circuit
does not transfer voltage; it transfers power. Both sides are then specified as power exchanges
rather than by a turns ratio: an active power to be moved from one side to the other, and a reactive
power at each terminal set independently.
The distinction from a conventional transformer is that the sides are decoupled. The reactive power
on one side is not a consequence of the other, the transfer does not depend on the angle across the
device, and the two systems need not share a frequency. What is not free is the active power, which
is common to both terminals up to losses; specifying it independently on each side would ask the
device to create or destroy energy.
4.9.9 - Synchronous Generator
Machine models, from the full dq0 description to the reduced order family.
Two different synchronous machine models are currently available:
the full order dq0 reference frame model (EMT, DP) [Kundur, Power system stability and control, 1994]
and the much simpler transient stability model (DP) [Eremia, Handbook of Electrical Power System Dynamics, 2003]
$\theta_r$ is the rotor position, $\omega_r$ is the angular electrical speed, $P$ is the number of poles, $J$ is the moment of inertia, $T_m$ and $T_e$ are the mechanical and electrical torque, respectively.
Motor convention is used for all models.
dq0 Reference Frame 9th Order Model
For stator referred variables, the base quantities for per unit are chosen as follows:
$v_{s base}$ peak value of rated line-to-neutral voltage in V
$i_{s base}$ peak value of rated line current in A
$f_{base}$ rated frequency in Hz
The synchronous generator equations in terms of per unit values in the rotor reference frame become:
For the simulation, fluxes are chosen as state variables.
To avoid the calculation of currents from fluxes using the inverse of the inductance matrix, the equation set needs to be solved for the fluxes analytically.
To simplify the calculations, dq axis magnetizing flux linkages are defined [Krause, Analysis of electric machinery and drive systems, 2002]:
The fundamental dynamic phasors are similar to the dq0 quantities for symmetrical conditions since both yield DC quantities in a rotating reference frame.
The network abc dynamic phasor quantities can be converted to dq0 dynamic phasors by applying the symmetrical components transformation and a rotation.
The angle $\delta$ is the orientation of the dq0 reference frame relative to the abc frame.
In the dynamic phasor case, the equation for $\frac{d}{dt} \langle \lambda_{0s} \rangle_1$ has a frequency shift.
To complete the state model, the magnetizing flux linkages are expressed as:
Third to sixth order machine equations and the voltage-behind-reactance form they are solved in.
The full dq0 machine keeps every rotor winding as a state. Reduced order models keep the rotor
flux linkages that matter on the timescale of interest and represent the rest algebraically, which
removes the fastest states and lets the machine be stepped at the same rate as the network.
This page derives the equations. Nothing here depends on how they are arranged in software.
Per unit system and operational parameters
All quantities are in the reciprocal per unit system referred to the direct axis mutual
inductance [Kundur1994]. With rated apparent power $S_n$ and rated line to line
voltage $V_n$ as the stator base, the base peak phase voltage, base current and base impedance are
The machine is described by operational parameters rather than by winding data: the synchronous
inductances $L_d$ and $L_q$, the transient inductances $L_d’$ and $L_q’$, the subtransient
inductances $L_d’’$ and $L_q’’$, the corresponding open circuit time constants $T_{d0}’$,
$T_{q0}’$, $T_{d0}’’$, $T_{q0}’’$, and the inertia constant $H$. A sixth parameter $T_{aa}$, the
armature to field coupling time constant, distinguishes the two sixth order variants.
Which states each order retains
The state variables are the voltages behind the transient and subtransient reactances,
$E_d’$, $E_q’$, $E_d’’$ and $E_q’’$, together with the two mechanical states. They are defined
from the terminal quantities by
and analogously for the subtransient pair with $L_d’’$ and $L_q’’$.
Order
Electrical states
Physical content
3
$E_q'$
Field winding only; the q axis rotor is neglected.
4
$E_d’$, $E_q'$
Field winding and one q axis damper.
5
$E_q’$, $E_d’’$, $E_q’'$
Adds both subtransient windings, no q axis transient state.
6a
$E_d’$, $E_q’$, $E_d’’$, $E_q’'$
Full transient and subtransient set, with $T_{aa} \neq 0$.
6b
$E_d’$, $E_q’$, $E_d’’$, $E_q’'$
Same states with $T_{aa} = 0$.
Every order carries the same two mechanical states, so the third order model has five states in
total and the sixth order models have eight.
Voltage behind reactance form
Written directly, the stator equations couple the machine currents to the network currents, and the
machine inductances appear in the axis frame while the network is solved in the phase frame. The
voltage behind reactance form removes that coupling: the machine is expressed as an internal
voltage in series with a reactance that is constant in the axis frame, so the only quantity that
changes between steps is the internal voltage.
The internal voltage is not a free variable. Applying the trapezoidal rule to the rotor flux
equations over one step $\Delta t$ gives it as a recursion in quantities already known at the start
of the step. For the transient states,
where $E_f$ is the field voltage supplied by the excitation system. The coefficients follow from
the trapezoidal integration and depend only on the parameters and the step size:
The reactance differences are $Z_q’ = L_d - L_d’ - Y_d$ and $Z_d’ = L_q - L_q’ - Y_q$. For the
orders without subtransient states $Y_d = Y_q = 0$ and $T_f = 0$. For the sixth order variant with
armature coupling,
The subtransient states obey a recursion of the same shape with $T_{d0}’’$ and $T_{q0}’’$ in place
of the transient time constants, and with the transient history entering as a forcing term.
Two properties of this form matter. The coefficients are computed once for a fixed step size, since
they contain no state. And because $B_d’$ and $B_q’$ are the trapezoidal amplification factors
$(2T - \Delta t)/(2T + \Delta t)$, they lie strictly inside the unit interval for any positive time
constant, so the flux recursion is unconditionally stable regardless of step size.
with the electrical torque taken from the axis frame quantities as
$T_e = V_d I_d + V_q I_q$. The load angle follows from the initial operating point as the phase of
$V + j L_q I$, which is the standard construction of the q axis position from terminal conditions.
Solution schemes
The recursion above evaluates the internal voltage from quantities at the previous step, so the
machine and the network are solved once per step in sequence. Two refinements exist for cases where
that single pass is not accurate enough.
The predictor corrector method takes the single pass result as a prediction, re-evaluates the flux
recursion using the corrected terminal quantities, and repeats until the change between successive
passes falls below a tolerance. It converges to the solution of the implicit trapezoidal step
rather than to its explicit approximation, at the cost of repeated network solutions.
The two stage predictor method splits the step differently: it advances the machine state on a
predicted terminal voltage, then applies a single correction derived from the network solution,
without iterating to convergence. It costs one extra network solve per step and removes most of the
one step delay error.
Both are schemes for solving the same equations. They do not change the model order or the retained
states.
Validity
The reduced order models assume the stator transients are fast enough to be neglected, so the
stator is treated as algebraic. This is the assumption that makes the model valid for
electromechanical studies and invalid where stator dynamics matter, such as during the first cycles
of a close-in fault or for subsynchronous interaction. Neglecting the q axis rotor entirely, as the
third order model does, additionally removes damping that is physically present, so a third order
machine oscillates more than the same machine represented at fourth order.
References
[Kundur1994] P. Kundur, Power System Stability and Control. New York: McGraw-Hill, 1994. Source of the reciprocal per unit system and of the operational parameter definitions used throughout this page.
4.9.10 - Synchronous Generator Regulators
Excitation and speed control models attached to the synchronous machine.
In DPSim, synchronous generator control systems are solved separately from the electric network. The outputs of the electric network (active and reactive power, node voltages, branch currents and rotor speed of synchronous generators) at time $k- \Delta t$ are used as the input of the controllers to calculate their states at time $k$. Because of the relatively slow response of the controllers, the error in the network solution due to the time delay $\Delta t$ introduced by this approach is negligible.
References
[1] “IEEE Recommended Practice for Excitation System Models for Power System Stability Studies,” in IEEE Std 421.5-2016 (Revision of IEEE Std 421.5-2005) , vol., no., pp.1-207, 26 Aug. 2016, doi: 10.1109/IEEESTD.2016.7553421.
[2] F. Milano, “Power system modelling and scripting,” in Power System Modelling and Scripting. London: Springer-Verlag, 2010, ISBN: 978-3-642-13669-6. doi: 10.1007/978-3-642-13669-6.
[3] F. Milano, A. Manjavacas, “Frequency Variations in Power Systems: Modeling, State Estimation, and Control”. ISBN: 978-1-119-55184-3.
[4] F. Milano, “Power System Analysis Toolbox: Documentation for PSAT”, ISBN: 979-8573500560.
[6] A. Roehder, B. Fuchs, J. Massman, M. Quester, A. Schnettler, “Transmission system stability assessment within an integrated grid development process”.
4.9.10.1 - Exciters and Power System Stabiliser
Voltage regulation of the synchronous machine field.
DC1 type model is the standard IEEE type DC1 exciter, whereas the other model is a simplified version of the IEEE DC1 type model. The inputs of the exciters are the magnitude of the terminal voltage of the generator connected to the exciter $v_h$ and the voltage reference $v_{ref}$, which is defined as a variable since other devices such as over-excitation limiters or power system stabilizers (PSS) modify such reference with additional signals. At the moment, no over-excitation limiters have been implemented in DPSim so that the reference voltage is given by:
$$
v_{ref}(t) = v_{ref,0} + v_{pss}(t)
$$
where $v_{ref,0}$ is initialized after the power flow computations and $v_{pss}(t)$ is the output of the (optional) PSS connected to the exciter. The output of the exciter systems is the induced emf by the field current at $t=k + \Delta t$: $v_{ef}(k + \Delta t)$ (sometimes the alternative notation $e_{fd}(k + \Delta t)$ is used).
IEEE Type DC1 exciter model
Fig. 1: Control diagram of the IEEE Type DC1 exciterAdapted from: Milano, Frequency Variations in Power Systems
This model is used to represent field controlled dc commutator exciters with continuously acting voltage regulators (especially the direct-acting rheostatic, rotating
amplifier, and magnetic amplifier types). The control diagram of this exciter is depicted in Fig. 1 and it is described by the following set of differential equations:
$$
T_{b} \frac{d}{dt} v_{b}(t) = v_{ref} - v_{R}(t) - v_{f}(t) - v_{b}(t),
$$
$$
T_{a} \frac{d}{dt} v_{a}(t) = K_{a} v_{in}(t) - v_{a}(t),
$$
$$
T_{f} \frac{d}{dt} v_{f}(t) - K_{f} \frac{d}{dt} v_{ef}(t) = -v_{f}(t),
$$
$$
T_{ef} \frac{d}{dt} v_{ef}(t) = v_{a}(t) - (K_{ef} + sat(t)) v_{ef}(t),
$$
where $v_h$ is the module of the machine’s terminal voltage, and $v_{in}$ is the amplifier input signal, which for the IEEE Type DC1 is given by:
$$
v_{in}(t) = T_{c} \frac{d}{dt} v_b(t) + v_b(t).
$$
The ceiling function approximates the saturation of the excitation winding:
$$
sat(t) = A_{ef} e^{(B_{ef} | v_{ef}(t) | )}
$$
The set of differential equations are discretized using forward euler in order to solve it numerically, which leads to the following set of algebraic equations:
$$
v_R(k + \Delta t) = v_R(k) + \frac{\Delta t}{T_R} ( v_h(k) - v_R(k) ),
$$
$$
v_b(k + \Delta t) = v_b(k)(1 - \frac{\Delta t}{T_b}) + \frac{\Delta t}{T_b} ( v_{ref}(k) - v_R(k) - v_f(k)),
$$
$$
v_{in}(k + \Delta t) = \Delta t \cdot \frac{T_c}{T_b} (v_{ref}(k) - v_R(k) - v_{f}(k) - v_b(k)) + v_b(k+1),
$$
$$
v_a(k + \Delta t) = v_a(k) + \frac{\Delta t}{T_a} ( v_{in}(k) K_a - v_a(k) ),
$$
$$
v_f(k + \Delta t) = (1 - \frac{\Delta t}{T_f}) v_f(k) + \frac{\Delta t K_f}{T_f T_{ef}} ( v_{a}(k) - (K_{ef} + sat(k)) v_{ef}(k) ),
$$
$$
v_{ef}(k + \Delta t) = v_{ef}(k) + \frac{\Delta t}{T_{ef}} ( v_{a}(k) - (sat(k) + K_{ef}) v_{ef}(k)),
$$
$$
sat(k) = A_{ef} e^{(B_{ef} | v_{ef}(k) | )}
$$
Since the values of all variables for $t=k$ are known, $v_{ef}(k+1)$ can be easily calculated using the discretised equations, which is carried out in the preStep function of the generator connected to each exciter.
The initial values of all variables, which are used in the first simulation step, are calculated assuming that the simulation starts in the steady. This is equivalent to assume that all derivative are equal to zero, which leads to:
$$
v_R(k=0) = v_h(k=0),
$$
$$
v_f(k=0) = 0
$$
$$
v_a(k=0) = K_{ef} v_{ef}(k=0) + A_{ef} e^{B_{ef} |v_{ef} (k=0)|} v_{ef}(k=0),
$$
$$
v_{in}(k=0) = \frac{v_a(k=0)}{K_a},
$$
$$
v_b(k=0) = v_{in}(k=0),
$$
$$
v_{ref}(t=0) = v_{in}(t=0) + v_b(t=0),
$$
where $v_h(k=0)$, $v_{ef}(k=0)$ are calculated after the power flow analysis and after the initialization of synchronous machines (see section initialization of SG).
Simplified IEEE Type DC1 exciter model (DC1Simp)
Fig. 2: Control diagram of the IEEE Type DC1 exciterAdapted from: Milano, Power System Modelling and Scripting
Because the time constants $T_b$ and $T_c$ of the IEEE Type DC1 exciter model are frequently small enough to be neglected, in DPSim a simplified model of this exciter which neglect these time constants is also implemented. The control diagram of this exciter is depicted in Fig. 2 and it is described by the following set of differential equations:
$$
T_R \frac{d}{dt} v_R(t) = v_h(t) - v_R(t)
$$
$$
T_a \frac{d}{dt} v_a(t) = - v_a(t) + K_a v_{in}(t)
$$
$$
T_f \frac{d}{dt} v_f(t) - K_f \frac{d}{dt} v_{ef}(t) = -v_f(t),
$$
$$
T_e \frac{d}{dt} v_{ef}(t) = v_a(t) - v_{ef}(t) (sat(t) + K_{ef})
$$
where $v_h$ is the module of the machine’s terminal voltage, and $v_{in}$ is the amplifier input signal, which is given by:
$$
v_{in}(t) = v_{ref} (t) - v_R(t) - v_f(t)
$$
The set of differential equations are discretized using forward euler in order to solve it numerically, which leads to the following set of algebraic equations:
$$
v_R(k + \Delta t) = v_R(k) + \frac{\Delta t}{T_R} ( v_h(k) - v_R(k) ),
$$
$$
v_{in}(k) = v_{ref}(k) - v_R(k) - v_f(k),
$$
$$
v_a(k + \Delta t) = v_a(k) + \frac{\Delta t}{T_a} ( v_{in}(k) K_a - v_a(k) ),
$$
$$
v_f(k + \Delta t) = (1 - \frac{\Delta t}{T_f}) v_f(k) + \frac{\Delta t K_f}{T_f T_{ef}} ( v_{a}(k) - (K_{ef} + sat(k)) v_{ef}(k) ),
$$
$$
v_{ef}(k + \Delta t) = v_{ef}(k) + \frac{\Delta t}{T_{ef}} ( v_{a}(k) - (sat(k) + K_{ef}) v_{ef}(k)),
$$
$$
sat(k) = A_{ef} e^{(B_{ef} | v_{ef}(k) | )}
$$
Since the values of all variables for $t=k$ are known, $v_{ef}(k+1)$ can be easily calculated using the discretised equations, which is carried out in the preStep function of the generator connected to each exciter.
The initial values of all variables, which are used in the first simulation step, are calculated assuming that the simulation starts in the steady. This is equivalent to assume that all derivative are equal to zero, which leads to:
$$
v_R(k=0) = v_h(k=0),
$$
$$
v_f(k=0) = 0,
$$
$$
v_a(k=0) = K_{ef} v_{ef}(k=0) + A_{ef} e^{B_{ef} |v_{ef} (k=0)|} v_{ef}(k=0),
$$
$$
v_{in}(k=0) = \frac{v_a(k=0)}{K_a},
$$
$$
v_{ref}(t=0) = v_R(t=0) + v_{in}(t=0),
$$
where $v_h(k=0)$, $v_{ef}(k=0)$ are calculated using the power flow analysis and after the initialization of synchronous machines (see section initialization of SG).
Static Exciter
Fig. 3: Control diagram of the Static ExciterAdapted from [6]
The control diagram of this is depicted in Fig. 3. It can be observed as a simplified version of the DC1 type exciter which is composed only by the regulator, the amplifier and an optional transducer. To discretize the lead-lag compensator using forward euler it is better to split this block into two parallel blocks as depicted in Fig. 4.
Fig. 4: Control diagram of the Static Exciter
where:
$$
C_{a} = \frac{T_{a}}{T_{b}}, \quad C_{b} = \frac{T_{b}-T_{a}}{T_{b}}.
$$
and it is described by the following set of differential equations:
$$
T_{R} \frac{d}{dt} v_{r}(t) = v_{h}(t) - v_{r}(t)
$$
$$
T_{b} \frac{d}{dt} x_{b}(t) = v_{in}(t) - x_{b}(t)
$$
$$
T_{e} \frac{d}{dt} e_{fd}(t) = K_{a} v_{e}(t) - e_{fd}(t),
$$
Then, the set of differential equations are discretized using forward euler in order to solve it numerically, which leads to the following set of algebraic equations:
To consider the saturation of $e_{fd}$ there are two different implementations, which is automatically selected depending of value of the parameter $K_{bc}$:
where $e^{*}_{fd}$ represents the output of the exciter.
Anti-windup ($K_{bc}>0$): for controllers with an integral component, i.e. also for PID controllers, the so-called “windup effect” can occur when using the standard saturation function. A strategy for limiting the anti-windup effect is shown in Fig. 5.
Fig. 5: Control diagram of the Static Exciter with anti windup strategy
which means that the input of the differential equation describing $e_{fd}$, $v_{e}$, takes now the following form:
The initial values of all variables, which are used in the first simulation step, are calculated assuming that the simulation starts in the steady. This is equivalent to assume that all derivative are equal to zero, which leads to:
PSS is a controller of synchronous generators used to enhance damping of electromechanical oscillations. The PSS1A implemented in DPSim accepts three optional input signals: rotor speed $\omega$, active power $P$, and terminal voltage magnitude $V_h$. The combined input signal is:
$$
s(t) = K_w \omega(t) + K_p P(t) + K_v V_h(t)
$$
Setting $K_p = K_v = 0$ recovers the speed-only special case. The PSS output $v_{pss}$ at time $t=k$ is a signal used as the input of the AVR to calculate the field voltage at $t=k+\Delta t$, $v_{fd}(k+\Delta t)$. At present, only one PSS is implemented in DPSim which is a simplified version of the IEEE PSS1A type model.
IEEE PSS1A type PSS
Fig. 6: Control diagram of the PSS Type 1 (speed input only;the implementation also accepts active power $K_p P$ and terminal voltage $K_v V_h$).Adapted from: Milano, Power System Modelling and Scripting
The control diagram of this PSS is depicted in Fig. 6. It includes a washout filter and two lead-lag blocks and is described by the following set of differential equations:
$$
T_w \frac{d}{dt} v_1(t) = -(s(t) + v_1(t)),
$$
$$
T_2 \frac{d}{dt} v_2(t) = (1 - \frac{T_1}{T_2})(s(t) + v_1(t)) - v_2(t),
$$
$$
T_4 \frac{d}{dt} v_3(t) = (1 - \frac{T_3}{T_4})\left(v_2(t) + \frac{T_1}{T_2}(s(t) + v_1(t))\right) - v_3(t),
$$
$$
v_{pss}(t) = v_3(t) + \frac{T_3}{T_4}\left(v_2(t) + \frac{T_1}{T_2}(s(t) + v_1(t))\right),
$$
where $s(t) = K_w \omega(t) + K_p P(t) + K_v V_h(t)$ is the combined input signal and $v_{pss}(t)$ is the output signal used to modify the reference voltage of the AVR.
The set of differential equations are discretized using forward euler in order to solve it numerically, which leads to the following set of algebraic equations:
$$
v_1(k + \Delta t) = v_1(k) - \frac{\Delta t}{T_w} (s(k) + v_1(k)),
$$
$$
v_2(k + \Delta t) = v_2(k) + \frac{\Delta t}{T_2} \left((1-\frac{T_1}{T_2})(s(k) + v_1(k)) - v_2(k)\right),
$$
$$
v_3(k + \Delta t) = v_3(k) + \frac{\Delta t}{T_4} \left((1-\frac{T_3}{T_4})\left(v_2(k) + \frac{T_1}{T_2}(s(k) + v_1(k))\right) - v_3(k)\right),
$$
$$
v_{pss}(k) = v_3(k) + \frac{T_3}{T_4} \left(v_2(k) + \frac{T_1}{T_2} (s(k) + v_1(k))\right)
$$
Since the values of all variables for $t=k$ are known, $v_{pss}(k)$ can be easily calculated using the discretised equations, which is carried out in the preStep function of the generator connected to each exciter. Then, $v_{pss}(k)$ is used as input of the AVR to calculate the field voltage at time $k+1$. The values $v_1(k+1)$, $v_2(k+1)$, $v_3(k+1)$ are stored and used to calculate the PSS output of the next time step.
The initial values of all variables, which are used in the first simulation step, are calculated assuming that the simulation starts in steady state. This is equivalent to assuming that all derivatives are equal to zero, which leads to:
$$
v_1(k=0) = -s(k=0),
$$
$$
v_2(k=0) = (1 - \frac{T_1}{T_2})(s(k=0) + v_1(k=0)),
$$
$$
v_3(k=0) = (1 - \frac{T_3}{T_4})\left(v_2(k=0) + \frac{T_1}{T_2}(s(k=0) + v_1(k=0))\right),
$$
$$
v_{pss}(k=0) = v_3(k=0) + \frac{T_3}{T_4}\left(v_2(k=0) + \frac{T_1}{T_2}(s(k=0) + v_1(k=0))\right),
$$
where $s(k=0) = K_w \omega(k=0) + K_p P(k=0) + K_v V_h(k=0)$ is evaluated after the power flow analysis and initialization of synchronous machines (see section initialization of SG). In steady state $\omega(k=0) = 1.0$ (pu), and if $K_p = K_v = 0$ then $v_2 = v_3 = v_{pss} = 0$.
Turbine Governor Models
In DPsim there are two types of Turbine Governor implementations. The Turbine Governor Type 1 implements both the turbine and the governor in one component. In contrast, Steam Turbine and Steam Turbine Governor are implemented as two separate classes and their objects are created independently. Steam/Hydro Turbine and Steam/Hydro Turbine Governor are two blocks that must be connected in series.
The input of the turbine governor models is the mechanical omega at time $t=k-\Delta t$ and the output is the mechanical power at time $t=k$. This variable is then used by the SG to predict the mechanical omega at time $t=k+\Delta t$.
Turbine Governor Type 1
Fig. 7: Control diagram of the turbine governor type 1Source: Milano, Power System Modelling and Scripting
This model includes a governor, a servo and a reheat block. The control diagram of this governor is depicted in Fig. 7 and it is described by the following set of differential equations:
$$
p_{in}(t) = p_{ref} + \frac{1}{R} (\omega_{ref} - \omega(t)),
$$
$$
T_s \frac{d}{dt} x_{g1}(t) = p_{in}(t) - x_{g1}(t),
$$
$$
T_c \frac{d}{dt} x_{g2}(t) = \left(1 - \frac{T_3}{T_c}\right) x_{g1}(t) - x_{g2}(t),
$$
$$
T_5 \frac{d}{dt} x_{g3}(t) = \left(1 - \frac{T_4}{T_5}\right) \left(x_{g2}(t) + \frac{T_3}{T_c} x_{g1}(t)\right) - x_{g3}(t),
$$
$$
\tau_m(t) = x_{g3}(t) + \frac{T_4}{T_5} \left(x_{g2}(t) + \frac{T_3}{T_c} x_{g1}(t)\right),
$$
where $\omega(t)$ is the input signal and $\tau_m(t)$ is the output signal of the governor.
The differential equations are discretized using the forward Euler method, which leads to the following set of algebraic equations:
$$
p_{in}(k-\Delta t) = p_{ref} + \frac{1}{R} (\omega_{ref} - \omega(k-\Delta t)),
$$
$$
x_{g1}(k) = x_{g1}(k-\Delta t) + \frac{\Delta t}{T_s} \left(p_{in}(k-\Delta t) - x_{g1}(k-\Delta t)\right),
$$
$$
x_{g2}(k) = x_{g2}(k-\Delta t) + \frac{\Delta t}{T_c} \left(\left(1 - \frac{T_3}{T_c}\right) x_{g1}(k-\Delta t) - x_{g2}(k-\Delta t)\right),
$$
$$
x_{g3}(k) = x_{g3}(k-\Delta t) + \frac{\Delta t}{T_5} \left(\left(1 - \frac{T_4}{T_5}\right) \left(x_{g2}(k-\Delta t) + \frac{T_3}{T_c} x_{g1}(k-\Delta t)\right) - x_{g3}(k-\Delta t)\right),
$$
$$
\tau_m(k) = x_{g3}(k) + \frac{T_4}{T_5} \left(x_{g2}(k) + \frac{T_3}{T_c} x_{g1}(k)\right).
$$
Since all variables at $t=k-\Delta t$ are known, $\tau_m(k)$ is computed in the preStep of the generator and used to approximate the mechanical equations at time $k+\Delta t$.
4.9.10.2 - Turbines and Governors
Mechanical power control of the synchronous machine.
Steam Governor
Fig. 8: Control diagram of the steam turbine governorAdapted from [6]
The control diagram of this model is depicted in Fig. 8. This model receives as input the frequency deviation $\Delta\omega = \omega_{ref} - \omega$ from the nominal frequency (normally $50,\text{Hz}$ or $60,\text{Hz}$) and produces the valve opening signal $p_{gv}$ for the turbine. $p_{ref}$ is the mechanical power produced at nominal frequency. The governor implements a lead-lag controller $\frac{K(1+sT_2)}{(1+sT_1)}$ where $K=1/R$ and $R$ is the droop coefficient, followed by a PT1 integrator with embedded rate limiters and an anti-windup loop. To avoid unnecessary dead-beat behaviour, complex transfer functions with more than one pole and zero are decomposed via partial fraction expansion into parallel PT1 elements, as shown in Fig. 9.
Fig. 9: Control diagram of the steam turbine governor after partial-fraction decomposition
Analogous to the static exciter model, the integrator uses an anti-windup strategy as shown in Fig. 10.
Fig. 10: Control diagram of the steam turbine governor with anti-windup strategy
If $T_1 = 0$ the $p_1(k)$ equation is skipped and $p(k)$ is instead:
$$
p(k-\Delta t) = \frac{1}{R} \left(\Delta \omega(k-\Delta t) + \frac{T_{2}}{\Delta t} \left(\Delta \omega(k-\Delta t) - \Delta \omega(k-2\Delta t)\right)\right).
$$
Assuming the simulation starts in steady state (all derivatives zero, $\Delta\omega(0)=0$), the initial values are:
$$
p_{1}(t=0) = 0, \quad p(t=0) = 0, \quad p_{ref} = p_{gv}^{*}(t=0) = p_{gv}(t=0).
$$
Steam Turbine
Fig. 11: Control diagram of the steam turbineAdapted from [6]
The steam turbine receives the valve opening signal $p_{gv}$ from the Steam Governor and outputs mechanical power $p_m$ to the synchronous generator. It is divided into high-pressure (HP), intermediate-pressure (IP), and low-pressure (LP) stages, each modelled as a first-order lag with time constants $T_{CH}$, $T_{RH}$, $T_{CO}$ respectively. Setting a time constant to zero disables that lag element. The total mechanical power is a weighted sum of each stage: $F_{HP} + F_{IP} + F_{LP} = 1$ must hold. The forward-Euler discretised equations are:
Fig. 12: Control diagram of a hydro turbine governorAdapted from [6]
The Hydro Turbine Governor receives the frequency deviation $\Delta\omega = \omega_{ref} - \omega$ as input and produces the valve/gate opening signal $p_{gv}$ for the turbine. $p_{ref}$ is the mechanical power produced at nominal frequency. The controller transfer function is $K\frac{1+sT_2}{(1+sT_1)(1+sT_3)}$, where $K=\frac{1}{R}$ and $R$ is the droop coefficient. The transfer function is decomposed into two parallel PT1 blocks as shown in Fig. 13.
Fig. 13: Control diagram of a hydro turbine governor after partial-fraction decomposition
Assuming the simulation starts in steady state (all derivatives zero, $\Delta\omega(t=0)=0$), the initial values are:
$$
x_{1}(t=0) = 0, \quad x_{2}(t=0) = 0, \quad p_{ref} = p_{gv}(t=0).
$$
Hydro Turbine
Fig. 14: Control diagram of a hydro turbineAdapted from [6]
The Hydro Turbine receives the gate opening signal $p_{gv}$ from the Hydro Turbine Governor and outputs mechanical power $p_m$ to the synchronous generator. The transfer function is specified by the water starting time $T_W$ and can be represented as the sum of two parallel blocks as shown in Fig. 15.
Fig. 15: Control diagram of a hydro turbine after decompositionAdapted from [6]
Assuming the simulation starts in steady state (all derivatives zero), the initial values are:
$$
x_{1}(t=0) = p_{gv}(t=0), \quad p_{m}(t=0) = p_{gv}(t=0).
$$
4.9.11 - Power Electronics
Averaged voltage source inverter models and their control.
Every inverter model here is averaged: the switching is not represented, and the converter is
treated as a controllable voltage behind its filter. Averaging removes the switching frequency from
the problem, which is what allows a step size set by the control bandwidth rather than by the
carrier. It also means these models say nothing about switching losses, harmonic injection or any
behaviour that depends on the modulation itself.
The control that sits on top of each is derived separately under
converter control, because the same cascade appears in more
than one of these models.
Choosing among them
The models differ along two axes: which domain they are written in, and whether the converter
follows the grid or forms it.
EMT Ph3 averaged VSI is the reference formulation. All
fourteen states are real, the three filter phases are represented individually, and there is no
carrier, so nothing is assumed about the bandwidth of what it carries.
DP Ph1 averaged VSI is the same converter as a single
positive-sequence envelope. Its six real filter states become two complex envelopes, which is the
saving the envelope description buys, at the cost of being unable to represent an unbalance.
DP Ph3 averaged VSI restores per-phase representation in the
envelope domain, with one complex envelope per phase and a controller that keeps a single
positive-sequence frame. Because three independent phase envelopes admit a negative-sequence
component, it carries negative-sequence current control that the single-phase model has no need for.
EMT Ph3 grid-forming VSI is the one that differs in
kind rather than in representation. It carries its own frequency and angle as states instead of
tracking the grid’s, so it can energise a network with no other source. Its control is nonlinear
enough that the model is linearized numerically at each operating point rather than written in
closed form.
What they share
All four are solved simultaneously with the network rather than through a delayed injection, using
the state-space nodal method described under
SSN components. All four are therefore re-formed as the
operating point moves, and all four make the system matrix change at every step, which is the cost
of the approach.
4.9.11.1 - EMT Ph3 Averaged Voltage Source Inverter
Three-Phase Averaged Voltage Source Inverter with State-Space Nodal Interface
This model represents a grid-following averaged voltage source inverter in the EMT domain.
Because its state-space form is recomputed as the operating point moves, it is solved simultaneously with the network rather than through a delayed injection.
The model includes a PLL, filtered active/reactive power measurement, outer power control, inner current control, and an LC filter with coupling resistance to the grid node.
4.9.11.2 - DP Ph1 Averaged Voltage Source Inverter
Single-Phase Averaged Voltage Source Inverter with State-Space Nodal Interface (Dynamic Phasor)
This model ports the same grid-following averaged inverter into the dynamic-phasor (DP) domain, as a single positive-sequence complex envelope rather than three abc waveforms.
The PLL, power filter, outer power control, and inner current control are baseband and stay real; only the LC filter’s two states are genuine carrier-band envelopes and carry the $-j\omega_n$ shift described in State-Space Nodal.
The terminal input is the PCC voltage envelope
$$u = U ,$$
and the state vector is the mixed real/complex-envelope form
where $\psi := \theta_{\mathrm{PLL}} - \omega_n t$ is the PLL angle’s deviation from the nominal carrier phase, tracked instead of the raw, unboundedly growing $\theta_{\mathrm{PLL}}$ for relinearization accuracy, and $V_c$, $I_f$ are complex envelopes replacing EMT’s six abc filter states.
The model output is the interface current injected into the MNA system,
$$y = \frac{U - V_c}{R_c}.$$
Model equations
The controller uses the opposite current direction, i.e. positive current denotes inverter injection into the grid,
$$I_{rc} = \frac{V_c - U}{R_c}.$$
Because the DP envelope already demodulates the carrier, the dq quantities are obtained by rotating the envelope by $\psi$ alone, not by the full absolute angle $\theta_{\mathrm{PLL}}$,
At each simulation step, the nonlinear model is locally linearized into the affine state-space form, packing the 8 real states and the real/imaginary parts of the 2 complex states into one real 12-vector,
4.9.11.3 - DP Ph3 Averaged Voltage Source Inverter
Three-Phase Averaged Voltage Source Inverter with State-Space Nodal Interface (Dynamic Phasor)
This model extends the single-phase grid-following averaged inverter to the three-phase dynamic-phasor (DP) domain.
Each phase of the LC filter is represented by an independent complex envelope, $V_{c,a/b/c}$ and $I_{f,a/b/c}$, in contrast to the single positive-sequence envelope of the single-phase model, whereas the controller retains a single positive-sequence $dq$ frame shared by the PLL, the power filter, and the outer and inner control loops.
As in the single-phase case, the control states are baseband quantities and remain real-valued; only the six per-phase filter envelopes are carrier-band quantities, and each carries the $-j\omega_n$ frequency shift introduced in State-Space Nodal.
The terminal input is the PCC voltage envelope of the three phases,
and the state vector concatenates the 6 complex per-phase envelopes ahead of the 8 real control states, keeping the carrier-band and baseband blocks separate,
where $\psi := \theta_{\mathrm{PLL}} - \omega_n t$ again denotes the deviation of the PLL angle from the nominal carrier phase, retained as a state to preserve relinearization accuracy. Each per-phase envelope contributes its real and imaginary parts to the packed real vector, yielding 20 real states in total, or 22 with the optional negative-sequence loop described below.
The model output is the per-phase interface current injected into the MNA system,
The main extension relative to DP::Ph1 is the per-phase projection onto, and redistribution from, the single positive-sequence $dq$ control frame.
The three capacitor-voltage envelopes are projected onto a single positive-sequence phasor,
and the PCC input $\underline{U}$ is projected identically, so that the coupling-current envelope seen by the controller is $\underline{I}_{rc} = (\underline{V}_c - \underline{U})/R_c$, with positive current again denoting injection from the inverter into the grid.
The $dq$ quantities are obtained by rotating the projected envelopes by $\psi$,
with $v_{c,d} = \operatorname{Re}{V_{c,dq}}$, $v_{c,q} = \operatorname{Im}{V_{c,dq}}$, and analogously for $i_{rc,d}$ and $i_{rc,q}$.
Taken together, the $1\times 3$ projection, the scalar $dq$ rotation, and the $3\times 1$ redistribution defined below constitute a rank-one $3\times 3$ Park mapping on the envelope triple, which reduces to the single-envelope relation of DP::Ph1 under balanced operation.
The positive-sequence active and reactive power measurements used by the controller are
with the projection scaling chosen so that $p$ and $q$ match the total three-phase active and reactive powers under balanced operation; under unbalanced operation they are the positive-sequence components seen by the single-frame controller.
The control chain from the PLL through the inner current loop is identical in form to that of DP::Ph1 and operates on the single positive-sequence $dq$ pair. The PLL and power-filter dynamics read
The single $dq$ voltage reference $V_{\mathrm{ref},dq} = v_{d,\mathrm{ref}} + j v_{q,\mathrm{ref}}$ is redistributed to the per-phase bridge-voltage envelopes through the inverse projection,
the phases being coupled only through the shared control chain, that is, through $V_{\mathrm{ref},p}$.
At each simulation step the nonlinear model is linearized about the current operating point into the affine state-space form, with the real and imaginary parts of the 6 complex per-phase envelopes and the 8 real control states packed into a single real 20-vector,
which is subsequently discretized and stamped into the DP MNA system.
In this default configuration the controller operates in a single positive-sequence $dq$ frame, so only the positive-sequence component of an unbalanced terminal is regulated. The negative-sequence response is present in the per-phase filter envelopes but is not itself a control state, and the $2\omega_n$ ripple it would otherwise induce in the $dq$ frame is therefore not represented.
Optional negative-sequence current control
A second, negative-sequence current-control loop can be added alongside the positive-sequence one, giving the dual-sequence structure of Yazdani and Iravani, chapter 8. The two configurations answer different questions: without the loop the model has the same 20 states and the same eigenvalue count as its EMT::Ph3 counterpart, which is what a cross-domain comparison requires, while with it the model gains 2 states and can regulate an unbalanced terminal.
The negative-sequence quantities are obtained by projecting the same three envelopes onto the conjugate sequence set,
A negative-sequence component rotates backwards relative to the PLL frame, so in envelope terms its $dq$ image follows from conjugating the projected phasor and rotating by $+\psi$ rather than $-\psi$,
This is what keeps the extension cheap. The negative-sequence loop costs only the two real integrator states $\gamma_{nd}$ and $\gamma_{nq}$, with no second carrier and no $2\omega_n$ term anywhere in the model.
The loop itself is the same PI structure as the positive-sequence inner loop,
reusing the inner-loop gains $K_{p,I}$ and $K_{i,I}$. Its output is redistributed to the per-phase bridge voltages through the sequence-orthogonal set, and adds to the positive-sequence command of the previous section,
The two references $i_{nd,\mathrm{ref}}$ and $i_{nq,\mathrm{ref}}$ default to zero, which makes the loop a negative-sequence suppressor. A non-zero pair commands a deliberate negative-sequence injection instead, as required by some unbalanced fault ride-through grid codes.
The state vector grows to 22 by appending the two integrators after the control block, so that the envelope and positive-sequence control indices are unaffected. Under a single-line-to-ground fault, enabling the loop suppresses the negative-sequence component of the injected current by about 40 percent while moving the positive-sequence component by less than 0.1 percent.
References
M. Mirz, S. Vogel, G. Reinke, and A. Monti, “DPsim: A dynamic phasor real-time simulator for power systems,” SoftwareX, vol. 10, art. 100253, 2019. https://doi.org/10.1016/j.softx.2019.100253
A. Yazdani and R. Iravani, Voltage-Sourced Converters in Power Systems: Modeling, Control, and Applications. Hoboken, NJ: Wiley-IEEE Press, 2010. https://ieeexplore.ieee.org/book/5237659
X. Gao, D. Zhou, A. Anvari-Moghaddam, and F. Blaabjerg, “Stability Analysis of Grid-Following and Grid-Forming Converters Based on State-Space Model,” in Proc. 2022 International Power Electronics Conference (IPEC-Himeji 2022, ECCE Asia), pp. 422–428. https://ieeexplore.ieee.org/document/9806927
Three-Phase Averaged Grid-Forming Inverter with State-Space Nodal Interface
This model represents a grid-forming averaged voltage source inverter in the EMT domain.
The control structure follows the state-space grid-forming converter of Gao2022 (VSG algorithm loop, voltage loop, current loop with active damping), whose grid-following counterpart in the same paper is the basis for the averaged inverter above; the inner voltage/current control and LC filter modeling follow Yazdani2010.
Like the grid-following inverter above it is a variable state-space nodal component stamped directly into the MNA system, but instead of a PLL that locks to the grid it carries its own virtual synchronous machine (VSG): the internal angle and voltage magnitude are states driven by active- and reactive-power balance, so the inverter imposes a voltage and can run islanded.
The model includes the VSG swing dynamics, a reactive-power/voltage excitation loop, filtered active/reactive power measurement, a cascaded voltage and current controller, a first-order converter/digital-delay approximation, and an LC filter with coupling resistance to the grid node.
where $\theta$ is the VSG angle (there is no PLL), $E$ is the excitation-controlled voltage magnitude, $\xi_{v}$, $\xi_{i}$ are the voltage- and current-loop integrators, and $v_{\mathrm{del}}$ are the two delay states.
The model output is the interface current injected into the MNA system,
The virtual synchronous machine sets the internal angle from the active-power balance and the internal magnitude from the reactive-power/voltage loop; the cascaded voltage and current controllers then track that internal reference through the LC filter. The dashed grid-connected extensions (virtual impedance, feed-forward scaling, Q-V droop) are described below.
Model equations
The physical grid current, positive for injection into the grid, is
and the capacitor current is $\mathbf{i}{\mathrm{cap},dq} = \mathbf{i}{f,dq} - \mathbf{i}_{g,dq}$.
Because the Park transform is amplitude invariant, three-phase instantaneous power carries the factor $3/2$,
an integral law on the reactive error with a voltage-droop term.
The excitation defines the dq voltage reference; in the islanded model it is aligned with the d-axis,
$$v_{d,\mathrm{ref}} = E, \qquad v_{q,\mathrm{ref}} = 0 .$$
The voltage controller integrates the voltage error and forms the current reference with the capacitor-current feed-forward and dq decoupling,
The current controller integrates the current error and forms the converter voltage reference, with inductor decoupling and optional active damping on the capacitor current,
and its output, transformed back to abc as $\mathbf{v}{\mathrm{inv}} = \mathbf{T}^{-1}(\theta),[v{\mathrm{del},d}\ v_{\mathrm{del},q}]^\top$, drives the LC filter,
The equations above describe the islanded inverter. Three opt-in extensions adapt it to a stiff grid; each defaults to the value that recovers the islanded model exactly, so the eigenstructure is unchanged unless a setter is called.
Virtual output impedance. A virtual impedance $Z_v = R_v + jX_v$ is subtracted from the excitation to form the voltage reference, using the filter current $\mathbf{i}_{f,dq}$,
$Z_v = 0$ recovers $v_{d,\mathrm{ref}} = E,\ v_{q,\mathrm{ref}} = 0$.
A finite $R_v$ adds a current-proportional term opposing motion, damping the power-synchronization loop on a stiff grid at the electrical timescale, an alternative to raising the mechanical damping $D$.
The drop is taken off the filter-current state $\mathbf{i}_f$ rather than the algebraically reconstructed grid current $\mathbf{i}_g = (\mathbf{v}_c-\mathbf{u})/R_c$; the latter would multiply the reference by a factor $\propto 1/R_c$, amplifying state and linearization error.
Grid-current feed-forward scale. A scalar $\kappa$ scales the grid-current feed-forward in the current reference,
a first-order lag with a stable fixed point $E^* = E_{\mathrm{set}} + D_q(Q_{\mathrm{ref}} - Q)$ and pole at $-\omega_q$.
On a stiff grid the network fixes $U_{\mathrm{pcc}}$, so the reactive error $Q_{\mathrm{ref}} - Q$ cannot be driven to zero and the integral law $\dot E = K_q(Q_{\mathrm{ref}} - Q) + K_u(U_n - U_{\mathrm{pcc}})$ has no reachable equilibrium (reactive windup); the proportional droop always has one.
The setpoint $E_{\mathrm{set}}$ is captured at initialization as the operating magnitude, so $\dot{E} = 0$ when $Q = Q_{\mathrm{ref}}$ at $t = 0$.
Linearization and stamping
The model is nonlinear (Park transforms with the moving angle $\theta$, the $1/\omega$ swing term, the power products). It is not linearized by hand; at each simulation step the state and output Jacobians are computed by central finite differences of the nonlinear functions $\mathbf{f}(\mathbf{x},\mathbf{u}) = \dot{\mathbf{x}}$ and $\mathbf{g}(\mathbf{x},\mathbf{u}) = \mathbf{y}$,
each column $j$ evaluated as $[\mathbf{f}(\mathbf{x}+\delta_j\mathbf{e}_j,\mathbf{u}) - \mathbf{f}(\mathbf{x}-\delta_j\mathbf{e}_j,\mathbf{u})]/(2\delta_j)$ with a mixed relative/absolute step $\delta_j$.
Because the grid-connected extensions above all enter through $\mathbf{f}$, they are captured in $\mathbf{A}$, $\mathbf{B}$, $\mathbf{C}$ and $\mathbf{D}$ automatically.
The affine offsets fix the model to the current operating point,
The dq/abc transformations and the nonlinear controls make this local model time varying, so it holds only in a neighbourhood of the operating point it was formed at.
How the linearization is carried out and stamped, together with the source and the runnable examples, is covered under
EMT Ph3 grid-forming VSI implementation.
References
[Gao2022] X. Gao, D. Zhou, A. Anvari-Moghaddam, and F. Blaabjerg, “Stability Analysis of Grid-Following and Grid-Forming Converters Based on State-Space Model,” in 2022 International Power Electronics Conference (IPEC-Himeji 2022 - ECCE Asia), 2022, pp. 422-428. Source of both the grid-following and grid-forming state-space control structures. Its eigenvalue analysis finds grid-following control better suited to a stiff grid and grid-forming control to a weak grid; the grid-connected extensions above (virtual impedance, Q-V droop) are what let the grid-forming model stay stable when connected to a stiff grid.
[Yazdani2010] A. Yazdani and R. Iravani, Voltage-Sourced Converters in Power Systems: Modeling, Control, and Applications. Hoboken, NJ: Wiley-IEEE Press, 2010. Basis for the inner voltage/current control and LC-filter modeling of both inverters.
4.9.12 - Converter Control
Phase tracking, angle generation and the cascaded control that drives a converter.
A converter model needs an angle to transform between the phase frame and its control frame, and a
control law that decides what to synthesise. The two questions are separable, and the answer to the
first is what distinguishes a grid-following converter from a grid-forming one.
Tracking an angle: the phase-locked loop
A phase-locked loop drives the estimated angle until the measured voltage sits on the chosen axis of
the control frame. The error signal is the off-axis component, which is zero exactly when the frame
is aligned, and it is fed to a proportional-integral controller whose output is a frequency
correction.
With the integrator state $\phi$ and the frequency error input $e$, the loop is
$$\dot{\phi} = k_i e, \qquad
\omega = \omega_{nom} + k_p e + \phi, \qquad
\dot{\theta} = \omega .$$
The nominal frequency enters as a feed-forward term rather than being learned, so the loop only has
to supply the deviation from it. That keeps the integrator near zero in normal operation and is why
a loop initialised at nominal frequency locks quickly.
The proportional gain sets how fast the loop follows a phase step and the integral gain how fast it
removes a standing frequency error. Making them large tracks disturbances the converter should
arguably ignore: a phase-locked loop that follows a fault as fast as it can is not obviously
desirable, since the converter then propagates the disturbance into its own control frame.
The important structural point is that a converter with a phase-locked loop takes its angle from the
network. It cannot operate without a voltage to lock to, which is what “grid following” means.
Generating an angle: the oscillator
The alternative is to carry the angle as a state and advance it at a commanded frequency,
$$\dot{\theta} = \omega_{ref},$$
with no measurement involved. The converter then imposes a phase rather than following one, which is
what “grid forming” means, and it continues to operate into a network with no other voltage source.
The difference between the two is one equation, but it determines whether the converter can start a
de-energised network or support frequency, and whether it has any defined behaviour when the grid
voltage collapses.
Cascaded control
Above the angle sits a cascade, ordered from slowest to fastest.
The outer loop compares measured active and reactive power against their references. The measurement
is low-pass filtered first, because the instantaneous power computed from the terminal quantities
carries components at twice the fundamental under any unbalance, and feeding those into a controller
produces a modulation the converter should not emit. The filter cut-off therefore bounds how fast
this loop can be, independently of its gains.
The inner loop regulates the filter current to the reference the outer loop produced. It must be
substantially faster than the outer loop for the cascade to behave as intended: the outer loop is
designed assuming its commanded current is achieved essentially immediately, and that assumption
fails if the two bandwidths approach each other. The usual consequence is not instability but an
interaction that appears as a poorly damped oscillation at neither loop’s design frequency.
Both loops are proportional-integral in the control frame, where a balanced fundamental quantity is
constant, so an integrator can drive the steady-state error to zero. This is the reason for working
in a rotating frame at all: the same controller applied to a sinusoid in the phase frame would leave
a standing error, because an integrator cannot track a moving target.
Grid-forming voltage control
A grid-forming converter replaces the outer power loop with a voltage magnitude and frequency law.
Droop characteristics relate active power to frequency and reactive power to voltage, which lets
several converters share load without communicating: each responds to the same measured deviation,
and the split follows from the droop gains.
Below that, a voltage loop regulates the filter capacitor voltage and hands a current reference to
the same inner current loop as before. The inner loop is therefore common to both control
philosophies; only what sits above it changes.
4.9.13 - State-Space Nodal Components
How an individual component is written as a state-space model and turned into a nodal stamp.
The state-space nodal method solves a component
simultaneously with the network instead of coupling it through a delayed injection. This page covers
the other half: how a single component is written so that the method applies to it, and what the
resulting stamp is.
What a component supplies
Each component provides a continuous-time model in a state, an input and an output of its own
choosing,
The choice of what the vectors mean is the entire modelling step. For a component that behaves as an
admittance the input is the terminal voltage and the output is the terminal current; for one that
behaves as an impedance the roles are exchanged. Everything after this is mechanical.
Discretisation
The state equation is integrated with the trapezoidal rule, which gives the discrete pair
This is exactly a companion model. $\boldsymbol{W}$ is an equivalent admittance that goes into the
system matrix and $\boldsymbol{y}_{hist}$ is an equivalent source that goes into the right hand
side. The difference from element-by-element companion models is only that $\boldsymbol{W}$ is
derived from the component’s own state-space description rather than written by hand, so a component
with internal states and cross-coupling between phases needs no special treatment.
The simplest case reproduces the classical result
Take a three-phase inductor. The natural choice is the current as state, the voltage as input and
the current as output, giving
With $\boldsymbol{A} = \boldsymbol{0}$ the discretisation collapses to
$\boldsymbol{A}_d = \boldsymbol{I}$ and $\boldsymbol{B}_d = \tfrac{\Delta t}{2}\boldsymbol{L}^{-1}$,
so the equivalent admittance is $\tfrac{\Delta t}{2}\boldsymbol{L}^{-1}$ and the history term is the
previous current plus the previous voltage contribution. For a single phase that is
$\Delta t / 2L$, the familiar trapezoidal companion model of an inductor.
This is worth doing once because it shows the machinery adds no approximation of its own. A
component whose model is a plain inductor gets exactly the stamp it would have had.
Where it earns its cost
The method is worth using when the component cannot be decomposed into independent elements. A
series RLC branch written as three separate companion models introduces two internal nodes; written
as one state-space model it introduces none, and the resulting stamp is a full matrix that captures
the coupling directly. The same applies to any component whose phases are coupled through a
non-diagonal inductance or through a control law.
The cost is that $\boldsymbol{W}$ is dense over the component’s terminals, where element models
produce sparse contributions, and that a matrix inverse of the size of the state vector is required
whenever the model changes.
The derivation above is the instantaneous case. The same component model is discretised differently
in an envelope domain; see
SSN across domains.
Fixed and varying models
If $\boldsymbol{A}$, $\boldsymbol{B}$, $\boldsymbol{C}$ and $\boldsymbol{D}$ are constant, the
discrete matrices are computed once and the system matrix never changes on account of the component.
If the model depends on the operating point, it must be re-formed and re-discretised as the
operating point moves, and the system matrix refactorised with it. A saturating inductor whose
inductance is a piecewise linear function of flux is the simple case; a converter whose control law
is nonlinear is the general one.
Initialization
The steady state at a given frequency follows from the continuous model directly,
which is the state-space equivalent of evaluating a phasor impedance. A component initialized this
way starts in steady state rather than settling into it, provided the model is linear at the
operating point.
4.9.14 - Signal Processing Blocks
Integrators, filters and the generators that drive time-varying sources.
Alongside the network components, a simulation contains blocks that carry no current and connect to
no node. They compute a value from another value, and they exist because controllers and sources are
built out of them.
Integration
An integrator advances a state from its input using the trapezoidal rule,
which is the same rule the network solver applies to reactive elements, so a control loop and the
circuit it acts on are integrated consistently. Using a cruder rule for the controller would put an
error into the loop that no amount of tuning removes.
Not every block needs that accuracy. An angle accumulator advancing at a commanded frequency is
often stepped with the rectangular rule instead,
$$\theta[k] = \theta[k-1] + \Delta t \, \omega[k],$$
which is a step behind but adds no dependence on the previous input. The distinction is worth
knowing when comparing an angle against one produced elsewhere, because the two rules differ by half
a step of phase.
Finite impulse response filtering
A finite impulse response filter forms its output as a weighted sum of the most recent inputs,
$$y[k] = \sum_{i=0}^{N-1} h_i \, u[k-i],$$
holding those inputs in a circular buffer of length $N$. Because the output depends only on past
inputs and never on past outputs, the filter cannot become unstable whatever the coefficients are,
and its phase response can be made exactly linear. The price is that a given sharpness needs a long
filter, which costs both memory and delay.
The delay is the part that matters in a control loop: a filter of length $N$ contributes roughly
$N/2$ steps of it. Inside a feedback path that delay is a phase lag, and it erodes stability margin
just as surely as raising a gain would.
Signal generators
A source that varies over time takes its value from a generator. Four behaviours cover most uses: a
constant, a sinusoid at a fixed frequency and amplitude, a sinusoid whose frequency ramps between
two values, and one whose frequency is modulated continuously.
The frequency ramp is the one with a subtlety. A ramp is described by a start frequency, an end
frequency and a rate of change, and it is tempting to generate it by evaluating $\sin(\omega(t),t)$
with a time-varying $\omega$. That is wrong: the argument of the sine is the accumulated phase, not
the product of the present frequency and the elapsed time, and the two differ whenever the frequency
is not constant. The phase must be accumulated,
$$\theta[k] = \theta[k-1] + 2\pi f[k] \, \Delta t ,$$
so that the instantaneous frequency is the derivative of the phase by construction. Generating a
ramp the naive way produces a signal whose actual frequency sweeps at twice the intended rate.
Accumulating phase makes the result depend on the step size and on the history of the run. Where an
exactly reproducible waveform is wanted, independent of when the simulation started or what steps it
took, the phase can instead be computed in closed form from the ramp parameters, which for a linear
ramp is a quadratic in time.
The Doxygen documentation is automatically generated from the C++ code using Doxygen.
It is helpful to understand the general structure of the C++ DPsim core components.
5.1 - Model Availability
Which simulation domain implements which model.
Which model exists in which domain. A tick means the domain has an implementation, a dash means it
does not.
The table below is generated from the headers under dpsim-models/include/dpsim-models by
scripts/docs/generate_model_availability.py. Do not edit it by hand; run the script with --write
instead. A model class the script does not recognise makes it fail rather than silently drop the
model, so the table cannot fall behind the code. For the equations behind a model, see
models.
Passive elements and sources
Model
SP::Ph1
SP::Ph3
DP::Ph1
DP::Ph3
EMT::Ph1
EMT::Ph3
Resistor
✓
✓
✓
✓
✓
✓
Inductor
✓
✓
✓
✓
✓
✓
Capacitor
✓
✓
✓
✓
✓
✓
VoltageSource
✓
✓
✓
✓
✓
✓
CurrentSource
–
–
✓
✓
✓
✓
VoltageSourceNorton
–
–
✓
–
✓
✓
VoltageSourceRamp
–
–
✓
–
✓
–
ProfileVoltageSource
–
–
✓
–
–
–
ControlledVoltageSource
✓
–
✓
–
–
✓
ControlledCurrentSource
✓
–
✓
–
–
✓
NetworkInjection
✓
–
✓
✓
–
✓
Branches
Model
SP::Ph1
SP::Ph3
DP::Ph1
DP::Ph3
EMT::Ph1
EMT::Ph3
PiLine
✓
–
✓
✓
✓
✓
RxLine
–
✓
✓
–
–
✓
RXLine
✓
–
–
–
–
–
SeriesResistor
–
–
–
✓
–
✓
ResIndSeries
–
–
✓
–
–
–
Transformer
✓
–
✓
–
–
✓
SolidStateTransformer
✓
–
–
–
–
–
Switches and loads
Model
SP::Ph1
SP::Ph3
DP::Ph1
DP::Ph3
EMT::Ph1
EMT::Ph3
Switch
✓
–
✓
✓
✓
✓
SeriesSwitch
–
–
–
✓
–
✓
varResSwitch
✓
–
✓
–
–
–
RXLoad
–
–
✓
–
–
✓
RXLoadSwitch
–
–
✓
–
–
–
PQLoadCS
–
–
✓
–
–
–
Load
✓
–
–
–
–
–
Shunt
✓
–
✓
–
–
✓
SVC
–
–
✓
–
–
–
Synchronous generators
Model
SP::Ph1
SP::Ph3
DP::Ph1
DP::Ph3
EMT::Ph1
EMT::Ph3
SynchronGenerator
✓
–
–
–
–
–
SynchronGeneratorDQ
–
–
–
✓
–
✓
SynchronGeneratorDQODE
–
–
–
✓
–
✓
SynchronGeneratorDQTrapez
–
–
–
✓
–
✓
SynchronGeneratorVBR
–
–
–
–
–
✓
SynchronGenerator3OrderVBR
✓
–
✓
–
–
✓
SynchronGenerator4OrderVBR
✓
–
✓
–
–
✓
SynchronGenerator5OrderVBR
✓
–
✓
–
–
✓
SynchronGenerator6aOrderVBR
✓
–
✓
–
–
✓
SynchronGenerator6bOrderVBR
✓
–
✓
–
–
✓
SynchronGenerator4OrderPCM
–
–
✓
–
–
✓
SynchronGenerator6OrderPCM
–
–
✓
–
–
–
SynchronGenerator4OrderTPM
–
–
✓
–
–
–
SynchronGeneratorIdeal
–
–
✓
–
–
✓
SynchronGeneratorIter
–
–
✓
–
–
–
SynchronGeneratorTrStab
✓
–
✓
–
–
✓
Power electronics
Model
SP::Ph1
SP::Ph3
DP::Ph1
DP::Ph3
EMT::Ph1
EMT::Ph3
AvVoltageSourceInverterDQ
✓
–
✓
–
–
✓
AvVoltSourceInverterStateSpace
–
–
✓
✓
–
✓
Inverter
–
–
✓
–
–
–
VoltageSourceInverter
✓
–
–
–
–
–
VSIVoltageControlVCO
–
–
–
–
–
✓
SSN_GFM
–
–
–
–
–
✓
State-space nodal components
Model
SP::Ph1
SP::Ph3
DP::Ph1
DP::Ph3
EMT::Ph1
EMT::Ph3
SSN_Full_Serial_RLC
–
–
✓
✓
✓
✓
SSN_Variable_Serial_RLC
–
–
✓
–
–
–
SSN_Capacitor
–
–
–
–
–
✓
SSN_Inductor
–
–
–
–
–
✓
SSNTypeV2T
✓
–
–
–
✓
–
SSNTypeI2T
✓
–
–
–
✓
–
PiecewiseLinearInductor
–
–
–
–
–
✓
GenericTwoTerminalVTypeSSN
–
–
✓
✓
–
✓
GenericTwoTerminalITypeSSN
–
–
✓
✓
–
✓
GenericFourTerminalVTypeSSN
–
–
–
–
–
✓
Excitation and stabilizers
ExciterDC1
ExciterDC1Simp
ExciterST1Simp
ExciterStatic
PSS1A
Turbines and governors
SteamTurbine
SteamTurbineGovernor
HydroTurbine
HydroTurbineGovernor
TurbineGovernor
TurbineGovernorType1
Converter control
PowerControllerVSI
VoltageControllerVSI
PLL
VCO
Signal sources and filters
SignalGenerator
SineWaveGenerator
CosineFMGenerator
DCGenerator
FrequencyRampGenerator
FIRFilter
Integrator
Decoupling components
These are network components: they connect to nodes and own their own sources. They are declared in the Signal namespace for historical reasons, which is why their domain appears in the class name rather than in the namespace.
Model
SP::Ph1
SP::Ph3
DP::Ph1
DP::Ph3
EMT::Ph1
EMT::Ph3
DecouplingLine
–
–
✓
–
–
–
DecouplingLineEMT
–
–
–
–
✓
–
DecouplingLineEMT_Ph3
–
–
–
–
–
✓
DecouplingIdealTransformer_SP_Ph1
✓
–
–
–
–
–
DecouplingIdealTransformer_DP_Ph1
–
–
✓
–
–
–
DecouplingIdealTransformer_EMT_Ph1
–
–
–
–
✓
–
DecouplingIdealTransformer_EMT_Ph3
–
–
–
–
–
✓
5.2 - Signal Models
Controllers, regulators, generators and decoupling elements in the Signal namespace.
Signal models live in CPS::Signal and are domain independent: the same controller drives a
dynamic phasor or an electromagnetic transient machine model, because it operates on scalar
signals rather than on network quantities. The exception is the decoupling group, which exists
per domain since it inserts real components into the network.
Excitation systems
Regulate generator terminal voltage by acting on field voltage. Equations and block diagrams are on the
regulators page.
Model
Description
ExciterDC1
Standard IEEE type DC1 exciter
ExciterDC1Simp
Simplified version of the IEEE type DC1 exciter
ExciterST1Simp
Simplified static exciter
ExciterStatic
Static exciter, with an anti-windup strategy for the integral component
Power system stabiliser
Model
Description
PSS1A
Simplified IEEE PSS1A. Enhances damping of electromechanical oscillations, accepting rotor speed, active power and terminal voltage magnitude as optional inputs. Its output feeds the exciter
Turbines and governors
Governors set mechanical power from speed deviation; turbine models convert that into the torque
applied to the machine.
Model
Description
SteamTurbine
Steam turbine, used in series with its governor
SteamTurbineGovernor
Governor for the steam turbine, instantiated separately from it
HydroTurbine
Hydro turbine, used in series with its governor
HydroTurbineGovernor
Governor for the hydro turbine, instantiated separately from it
TurbineGovernorType1
Turbine and governor combined in one component
TurbineGovernor
Turbine and governor combined in one component
Converter control
Control loops for the averaged inverter models. See
power electronics for how these
attach to the converter.
Model
Description
PowerControllerVSI
Power control loop used by the averaged grid-following inverter models
VoltageControllerVSI
Voltage control loop used by the grid-forming inverter models
PLL
Phase-locked loop
VCO
Voltage-controlled oscillator
Signal generators
Drive sources and setpoints from a prescribed waveform rather than a constant.
Model
Description
SignalGenerator
Base class for the generators below
SineWaveGenerator
Sine wave
CosineFMGenerator
Frequency-modulated cosine
FrequencyRampGenerator
Frequency ramp
DCGenerator
Constant value
Filters and maths
Model
Description
FIRFilter
Finite impulse response filter
Integrator
Integrator block used inside the control models
Decoupling
Split a network into parts that can be solved separately, either across solvers or across
simulators in a co-simulation. See
co-simulation.
State-space extraction is available for EMT Ph3 and DP Ph1 simulations
using the direct MNA solver. For models containing switches, the extracted matrix represents the currently
active switch configuration. The matrix is recomputed when the switch status
changes.
EMT Ph3
Supported components with extraction states are:
EMT::Ph3::Inductor,
EMT::Ph3::Capacitor,
EMT::Ph3::TwoTerminalVTypeSSNComp,
EMT::Ph3::TwoTerminalVTypeVariableSSNComp.
Supported algebraic components without extraction states are:
EMT::Ph3::Resistor,
EMT::Ph3::Switch,
EMT::Ph3::VoltageSource.
The following composite components are supported through their immediate
MNA subcomponents:
EMT::Ph3::NetworkInjection,
EMT::Ph3::PiLine,
EMT::Ph3::RXLoad,
EMT::Ph3::RxLine,
EMT::Ph3::Shunt,
EMT::Ph3::Transformer.
DP Ph1
Supported components with extraction states are:
DP::Ph1::Inductor,
DP::Ph1::Capacitor,
DP::Ph1::TwoTerminalVTypeSSNComp,
DP::Ph1::MixedVTypeVariableSSNComp.
Supported algebraic components without extraction states are:
DP::Ph1::Resistor,
DP::Ph1::Switch,
DP::Ph1::VoltageSource.
The following composite components are supported through their immediate
MNA subcomponents:
DP::Ph1::NetworkInjection,
DP::Ph1::PiLine,
DP::Ph1::RXLoad,
DP::Ph1::RxLine,
DP::Ph1::Shunt,
DP::Ph1::Transformer.
Supported composite components are expanded by one level during contributor
discovery. Their immediate MNA subcomponents provide the state-space
contributions, while the composite parent remains part of the simulation and
retains its normal MNA stamping. Nested composites are currently unsupported.
Other component types are rejected explicitly when state-space extraction is
enabled.
6 - Contributing
How to get a change into DPsim.
Contributions of all kinds are welcome, including code, documentation, examples, models, bug
reports and reviews. Open a pull request or get
in touch through GitHub Discussions.
These pages cover the process. For how the code is organised and the conventions it follows, see
the developer guide.
Quick start
Fork the repository and clone your fork.
Run pre-commit install to activate the automated checks (formatting, notebook output stripping).
Create a branch with a descriptive prefix (feature/, fix/, docs/).
Commit with a sign-off, using the conventional commit style:
git commit -s -m "fix: correct node voltage initialization in DiakopticsSolver"
Keep your branch up to date by rebasing; do not merge the target branch into your branch:
git fetch upstream
git rebase upstream/master
git push --force-with-lease
Open a pull request from your fork against sogno-platform/dpsim:master.
Pull Requests
There are no strict formal requirements besides the following:
Developer Certificate of Origin (DCO)
We require a Developer Certificate of Origin. See more here.
Code Formatting with pre-commit
We enforce code formatting automatically using pre-commit. Please run pre-commit install the first time you clone the repository to run pre-commit before each commit automatically. If you forgot to do this, you will need to use the command pre-commit run --all-files one time to format your changes.
Development in Forks Only
We accept contributions made in forks only. The main repository is not intended for contributor-specific branches.
SPDX Headers
Use SPDX headers to indicate copyright and licensing information, especially when introducing new files to the codebase. For example:
/* Author: John Smith <John.Smith@example.com>
* SPDX-FileCopyrightText: 2025 Example.com
* SPDX-License-Identifier: MPL-2.0
*/
Keep the SPDX tags on adjacent lines with no blank line between them, as the tooling
reads them as a block.
Linear History (no merge commits)
DPsim maintains a linear git history.
Never merge the target branch into your feature branch; rebase instead:
git rebase upstream/master
git push --force-with-lease
No Saved Notebook Outputs
Jupyter notebooks must be committed without saved cell outputs (images, plots, printed text).
Saved outputs bloat diffs and can break the notebook test collector, which re-executes notebooks and re-extracts their outputs.
Strip outputs before committing:
A pre-commit hook does this automatically once you have run pre-commit install; CI also rejects a pull request that changes a notebook still carrying saved outputs.
Creating New Releases (info for maintainers)
DPsim currently uses Semantic Versioning. The periodic creation of
new versions can help to mark significant changes and to analyze new portions of code using tools like SonarCloud.
A new version of DPsim has to be indicated as follows:
Update version in pyproject.toml
Update VERSION in the top level CMakeLists.txt
Update sonar.projectVersion in sonar-project.properties
Update CHANGELOG.md and include all the unreleased changes in the list
Create a new tag with an increased version number, which can be done during the release in GitHub
Python Packages
Due to the creation of a new tag, a new PyPi package will be deployed automatically.
Only Linux packages are currently available, other platforms will be supported in the future.
Container Images
To release an updated Docker image, the container workflow needs to be triggered manually.
If a Pull Request changes a container image, this is not updated automatically in the container image register.
Planning
Short-term planning for new features is done on the GitHub Project board.
Parts of this documentation were drafted with the help of large language models. The maintainers edit
and curate that output; they do not vouch for every line as if it were written from first-hand
knowledge, and the documentation carries no warranty.
That is a statement about how it is produced, not an excuse. The working rules exist precisely
because generated prose is confidently wrong in ways that read well:
A page is written after running its code, never from reading the API. Every tutorial has a script
in the repository and quotes it; if the two disagree, the page is wrong.
A claim about the code is traced to where the value is used before it is written down. Two hazard
notes here described correct code as broken because a line was read in isolation; both were
corrected once the value was followed through.
Anything measurable is measured by a script that reads the source, not asserted from having read a
lot of it. That is what generate_model_availability.py, check_docs_pairing.py and
check_docs_hazards.py are for.
No reference is cited that the repository does not already contain. Invented citations look
exactly like real ones.
A notice in the site footer says the same to readers. If you find something wrong,
open an issue. That is more useful than
assuming a page is authoritative because it is detailed.
How the documentation is organised
Six sections, split by what the reader is trying to do rather than by topic. Putting a page in the
right one is most of the work; the rest is deciding which half of the subject it covers.
Section
For someone who wants to
Names C++ classes
User Guide
install DPsim and run a simulation
no
Tutorials
learn by working through one idea at a time
only Python API calls
Developer Guide
change DPsim, or understand how it works inside
yes
Concepts
know the mathematics behind a model or method
never
Reference
look up what exists and where
generated
Contributing
contribute to the project
n/a
Every subject is written twice
Concepts carries the mathematics: what the model represents, the equations, and what they
assume. It names no class, no file and no example, so a reader could follow it while implementing
the model in something else entirely.
Developer Guide carries the arrangement in code: the class hierarchy, how the component
interfaces with the solver, its attributes and state layout, the traps in configuring it, and links
to the source and examples.
The two link to each other. scripts/docs/check_docs_pairing.py reports any Concepts page without a
counterpart and exits non-zero, with an exemption list for method pages that have no single
implementation behind them.
What is generated rather than written
Do not hand-edit these.
The model availability matrix comes from the component headers via
scripts/docs/generate_model_availability.py. A model class it does not recognise makes it fail rather
than silently omit the model.
The Python and C++ API references are generated by Sphinx and Doxygen from the source, so they are
improved by writing better doc comments, not by editing the site.
Tutorial figures come from scripts/docs/generate_tutorial_figures.py. The architecture diagrams do
not: they are editable draw.io SVGs and must be edited in draw.io.
Conventions
Every page carries a description, which appears under its title in section listings, and a
weight, which sets its order in the sidebar. A page without a weight falls to alphabetical, which
is how sections drift into arbitrary order.
Terminology follows standard electrical engineering usage. Avoid words that collide with class
names: “signal” reads as SimSignalComp rather than as a recorded quantity, so prefer “attribute”.
Highlighting what a reader must not miss
Documentation prose is read in order, so anything that will silently cost someone an afternoon has
to break out of the prose. Use the Docsy alert shortcode, with one of four titles, and keep the
vocabulary small so the colours stay meaningful.
{{% alert title="Requires a build with VILLASnode" color="info" %}}
Something the reader must have before this page works at all.
{{% /alert %}}
Colour
Use it for
Title starts with
info
a prerequisite: a build flag, an optional dependency, a package that is not installed by default
Requires
warning
a trap: correct code that silently does the wrong thing, a parameter that means two things, an ordering that matters
Watch out:
danger
a known defect in DPsim itself, not something the reader can avoid by being careful
Defect: or Suspected defect:
primary
a genuine aside that is neither a prerequisite nor a hazard
Note
Use exactly these four prefixes. Earlier pages mixed Trap:, Watch out: and Gap: for the same
colour, which made the severity unreadable: if two words mean one thing, neither means anything.
Three rules keep this useful. Mark a hazard where the reader meets it, not only in a reference page
they may never open, so the same trap can appear on a tutorial and on an implementation page. Reserve danger for defects: if a reader can avoid the problem by knowing about it, it is a
warning, and a page full of red stops being read. And a missing capability is a Note, not a
Watch out:, because there is no hazard to avoid, only something that cannot be done.
Do not use blockquotes for this. They carry no colour and no title, so they read as ordinary text.
6.1 - LLM Pull Request Review
How the automated pull request review works and what it checks.
Overview
DPsim ships an optional, non-blocking pull-request reviewer that runs a series of
specialised passes over the diff of a pull request using a large language model
and posts a single review comment. It is intended as an assistive first pass: it
never requests changes and cannot block a merge, so a human review remains
authoritative.
The reviewer lives under .github/llm-review/ (the prompts in prompts.py and a
pure-standard-library runner in review.py) and is driven by two workflows:
llm-review-collect.yml, which runs on the pull request itself, and
llm-review.yml, which performs the review. The split is what makes reviewing
pull requests from forks safe (see Fork pull requests). It
communicates with any OpenAI-compatible chat endpoint, configured through the
environment variables described below.
How it works
For each pull request the runner reads the base..head diff and sends it, in
turn, to a set of focused review stages, each with its own prompt. The stages
cover model equations and their derivation, MNA stamping and domain modeling,
numerical correctness, task scheduling and attribute usage, real-time safety,
C++ class design and reuse, naming and in-code documentation, logging discipline,
the Python bindings, input parsing, the build system and dependencies, testing
and component coverage, and licensing and pull-request hygiene. Each stage returns
a strict JSON list of findings. A final synthesis pass deduplicates and
prioritises them, and the runner posts them as one review, anchoring inline
comments only to lines present in the diff.
The prompts encode DPsim’s documented conventions (see
Guidelines) and the recurring points raised in
past reviews, so the feedback stays specific to this project rather than generic.
Configuration
The workflow requires one repository secret:
RWTH_LLM_TOKEN: the bearer API key for the chat endpoint.
The following repository Actions variables are optional and override the
defaults baked into the workflow:
LLM_BASE_URL: the OpenAI-compatible base URL.
LLM_MODEL: the model identifier.
LLM_CHAT_PATH: the chat path appended to the base URL (default
/chat/completions).
LLM_REVIEW_RUNNER: the runner label (default ubuntu-latest; see
Runner selection).
The workflow’s baked-in defaults target an OpenAI-compatible deployment; override
LLM_BASE_URL and LLM_MODEL to point at a different endpoint or model.
Obtaining an API key
Obtain a bearer API key from the chosen OpenAI-compatible provider and store it as
the RWTH_LLM_TOKEN repository secret. The key is only exposed to workflow runs on
pull requests from the repository itself, never from forks.
Before storing the secret, a single request confirms that the key reaches the
model:
The runner has a dry-run mode that executes the full pipeline and prints the
assembled review instead of posting it. It needs no GitHub token and no Actions
runner, only network access to the endpoint:
cd .github/llm-review
exportLLM_BASE_URL='<openai-compatible-base-url>'exportLLM_MODEL='<model-identifier>'exportLLM_CHAT_PATH='/chat/completions'exportLLM_API_KEY="$RWTH_LLM_TOKEN"exportBASE_SHA=$(git rev-parse origin/main)HEAD_SHA=$(git rev-parse HEAD)PR_NUMBER=0python3 review.py --dry-run
Runner selection
The workflow defaults to a GitHub-hosted ubuntu-latest runner. If the chosen
endpoint is only reachable from within a particular network, set the
LLM_REVIEW_RUNNER variable to a self-hosted runner label registered inside that
network; no change to the workflow is required.
Fork pull requests
Reviewing pull requests from forks requires care, because a fork’s code is
untrusted and must never gain access to the secret. The reviewer uses the
workflow_run pattern for this, rather than pull_request_target, and splits the
work into two workflows:
llm-review-collect.yml runs on the pull_request event, including from
forks. GitHub withholds secrets from fork pull_request runs, so this job has
no key. It checks out nothing and runs no code from the pull request; it only
records the PR number and commit SHAs, taken from trusted GitHub context, into
an artifact.
llm-review.yml runs on workflow_run, after the collect job completes, in
the base repository context where the secret is available. It checks out the
base repository’s own code, never the pull request’s, and reads the diff as
data through the GitHub API. It never builds or executes anything from the pull
request.
Two properties keep the key safe. First, the key is only ever sent as an
Authorization header to the configured LLM endpoint, and is never placed in the
model prompt, so a prompt-injection payload in the diff cannot reveal it. Second,
the privileged job runs only trusted base-repository code, so untrusted pull
request code never executes with the secret in scope. The PR metadata read from
the collect artifact is validated (numeric PR number, hexadecimal SHAs) before
use. For this to operate, both the workflows and the secret must reside on the
repository the pull requests target.
7 - How to Cite
Cite DPsim if you use it in published work.
If you use DPsim in your research, please cite the software paper below. If your work depends on
a specific capability, cite the corresponding paper from
further publications as well.
Software paper
M. Mirz, S. Vogel, G. Reinke and A. Monti, “DPsim: A dynamic phasor real-time simulator for
power systems”, SoftwareX, vol. 10, 100253, 2019.
https://doi.org/10.1016/j.softx.2019.100253
@article{mirz2019dpsim,title={DPsim: A dynamic phasor real-time simulator for power systems},author={Mirz, Markus and Vogel, Steffen and Reinke, Georg and Monti, Antonello},journal={SoftwareX},volume={10},pages={100253},year={2019},issn={2352-7110},doi={10.1016/j.softx.2019.100253},url={https://www.sciencedirect.com/science/article/pii/S2352711018302760}}
Citing a specific version
To make a result reproducible, cite the version you ran alongside the paper:
The repository also carries a CITATION.cff file, so GitHub offers a ready-made citation
through the “Cite this repository” link on the project page.
Further publications
Cite these when your work builds on the specific method they describe.
Shifted frequency analysis and reduced-order machine models:
J. Dinkelbach, M. Moraga and A. Monti, “Reduced-Order Synchronous Generator Modelling for
Real-Time Simulation using Shifted Frequency Analysis”, OSMSES, 2023.
https://ieeexplore.ieee.org/document/10089718
G. Nakti, J. Dinkelbach, M. Mirz and A. Monti, “Comparative Assessment of Shifted Frequency
Modeling in Transient Stability Analysis using the Open Source Simulator DPsim”, OSMSES, 2022.
https://ieeexplore.ieee.org/document/9769135
J. Dinkelbach, G. Nakti, M. Mirz and A. Monti, “Simulation of Low Inertia Power Systems Based
on Shifted Frequency Analysis”, Energies, vol. 14, no. 7, 1860, 2021.
https://www.mdpi.com/1996-1073/14/7/1860
Power electronics modelling and parallelisation:
M. Mirz, J. Dinkelbach and A. Monti, “DPsim: Advancements in Power Electronics Modelling Using
Shifted Frequency Analysis and in Real-Time Simulation Capability by Parallelization”,
Energies, vol. 13, no. 15, 3879, 2020. https://www.mdpi.com/1996-1073/13/15/3879
Solver performance:
J. Dinkelbach, L. Schumacher, L. Razik, A. Benigni and A. Monti, “Factorisation Path Based
Refactorisation for High-Performance LU Decomposition in Real-Time Power System Simulation”,
Energies, vol. 14, no. 23, 7989, 2021. https://www.mdpi.com/1996-1073/14/23/7989
Grid data and CIM:
J. Dinkelbach, L. Razik, M. Mirz, A. Benigni and A. Monti, “Template-based generation of
programming language specific code for smart grid modelling compliant with CIM and CGMES”,
The Journal of Engineering, 2022.
https://onlinelibrary.wiley.com/doi/abs/10.1049/tje2.12208
Real-time and co-simulation:
S. Vogel, M. Mirz, L. Razik and A. Monti, “An Open Solution for Next-generation Real-time Power
System Simulation”, IEEE EI2, 2017. https://ieeexplore.ieee.org/document/8245739
M. Mirz, A. Estebsari, F. Arrigo, E. Bompard and A. Monti, “Dynamic phasors to enable
distributed real-time simulation”, ICCEP, 2017.
https://ieeexplore.ieee.org/document/8004805
Parts of this documentation were drafted with the help of large language models and then
reviewed and edited by the maintainers. It may still contain errors or omissions, and it is
provided without warranty of any kind. Please
report anything that looks
wrong; corrections are welcome.