This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Documentation

Documentation for DPsim, a solver library for dynamic power system simulation.

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.

Contributors

See CONTRIBUTORS.md for a list of contributors.

Publications

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:

Binder

Python package

DPsim is published on PyPI and installs like any other Python package:

python3 -m venv venv
source venv/bin/activate
pip install dpsim

If you prefer conda, the equivalent is:

conda create -n dpsim python=3.13
conda activate dpsim
pip install dpsim

Docker

You need Docker installed first. The prepared image on Docker Hub bundles the module together with a JupyterLab session:

docker run -p 8888:8888 sogno/dpsim

Then open http://localhost:8888/lab?token=3adaa57df44cea75e60c0169e1b2a98ae8f7de130481b5bc.

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.

Where each kind fits

If you want toGo to
Learn how a simulation is put togetherTutorials
See a complete study with plotsNotebooks, below
Use DPsim from an applicationC++ examples, below
Look up what a model doesConcepts

Notebooks

Run them in the browser with no local installation:

Binder

Locally, they live under examples/Notebooks and need the Python package on the path, as described in the build section.

CategoryContents
Quickstart GuideA single notebook covering a first simulation end to end
CircuitsSmall networks exercising one modelling aspect at a time
ComponentsOne component at a time, often comparing domains against each other
GridsPublished test systems such as the WSCC 9 bus and CIGRE networks
FeaturesCross-cutting capabilities rather than a specific network
PerformanceTiming and scaling comparisons
StateSpaceState-space extraction from a running simulation

Understanding_DP.ipynb is worth reading early if dynamic phasors are new to you, since it builds the intuition the concepts section then formalises.

Continuing from a tutorial

Each tutorial ends at the point where the notebooks take over, so a category is usually the natural next step from the rung that introduced the idea.

After this tutorialThese notebooks go further
Adding dynamicsCircuits, for larger passive networks
Two-bus networkGrids, for published test systems solved the same way
Comparing domainsComponents, which compare one model across domains
Adding a machineCircuits, for the SMIB and multi-machine fault studies
Exchanging dataThe co-simulation examples below

C++ examples

Under dpsim/examples/cxx, built as part of a normal build and produced as executables in the build directory.

DirectoryContents
CircuitsNetworks assembled directly in C++
ComponentsSingle component scenarios
CIMReading network data from CIM and CGMES files
StateSpaceState-space extraction
RealTimeScenarios run against the wall clock
DAE, signals, timerSmaller 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:

cmake --build . --target DP_VS_RL1
./dpsim/examples/cxx/DP_VS_RL1

Co-simulation

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

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.

Registering an attribute

logger = dpsimpy.Logger("my_simulation")
logger.log_attribute("n1.v", "v", n1)
logger.log_attribute("load.i_intf", "i_intf", load)

sim.add_logger(logger)

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.

Reading it back

import villas.dataprocessing.readtools as rt

results = rt.read_timeseries_dpsim("logs/my_simulation.csv")
voltage = results["n1.v"]

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.

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.

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:

echo "evacuating cores"
tuna isolate -c 9,11,13,15

echo "disabling RT throttling"
echo -1 > /proc/sys/kernel/sched_rt_runtime_us

echo "stopping systemd services"
systemctl stop polkit
systemctl stop containerd
systemctl stop crond
systemctl stop chronyd

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 simulation
taskset -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.

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.

  1. Your first simulation. A source and a resistor. The shape of a script, and how to read a result back.
  2. Adding dynamics. An inductor, the transient it produces, and how to choose a time step.
  3. A network, and where it starts from. A line between two buses, and initializing the dynamic run from a powerflow.
  4. Applying a fault. Switching during a run, and why clearing a fault needs more care than applying one.
  5. The same circuit in two domains. Waveforms against envelopes, and what the envelope buys.
  6. Adding a machine. A synchronous generator, initializing it correctly, and what the model order changes.
  7. Exchanging data with another tool. Handing a value out of a running simulation, and where the boundary lies.

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.

A voltage source feeding a resistor.

The whole script

import dpsimpy
import villas.dataprocessing.readtools as rt
from villas.dataprocessing.timeseries import TimeSeries as ts

name = "first_simulation"

# 1. Nodes
gnd = dpsimpy.dp.SimNode.gnd
n1 = dpsimpy.dp.SimNode("n1")

# 2. Components
src = dpsimpy.dp.ph1.VoltageSource("src")
src.V_ref = complex(100, 0)

load = dpsimpy.dp.ph1.Resistor("load")
load.R = 10.0

# 3. Connections
src.connect([gnd, n1])
load.connect([n1, gnd])

# 4. Topology
system = dpsimpy.SystemTopology(50, [gnd, n1], [src, load])

# 5. Logging, then run
logger = dpsimpy.Logger(name)
logger.log_attribute("n1.v", "v", n1)
logger.log_attribute("load.i_intf", "i_intf", load)

sim = dpsimpy.Simulation(name)
sim.set_domain(dpsimpy.Domain.DP)
sim.set_system(system)
sim.set_time_step(1e-3)
sim.set_final_time(0.1)
sim.add_logger(logger)
sim.run()

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.

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 name
logger.log_attribute("n1.v", "v", n1)                  # 2. register each attribute you want
logger.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.

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.

Reading the results

results = rt.read_timeseries_dpsim("logs/" + name + ".csv")

print(sorted(results.keys()))
# ['load.i_intf', 'n1.v']

v = results["n1.v"]
print(v.time[1], abs(v.values[1]))
# 0.001 100.0

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.

Plotting

import villas.dataprocessing.plottools as pt

pt.plot_timeseries(1, results["n1.v"].abs())
pt.plot_timeseries(2, results["load.i_intf"].abs())

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:

emt = ts.frequency_shift_list(results, 50)
print(sorted(emt.keys()))
# ['load.i_intf_shift', 'n1.v_shift']

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.

A source, a resistor and an inductor in series.

The circuit

A source, a resistor and an inductor in series. Only the inductor is new; everything else is the same shape as before.

import dpsimpy
import villas.dataprocessing.readtools as rt

name = "rl_circuit"

gnd = dpsimpy.dp.SimNode.gnd
n1 = dpsimpy.dp.SimNode("n1")
n2 = dpsimpy.dp.SimNode("n2")

src = dpsimpy.dp.ph1.VoltageSource("src")
src.V_ref = complex(100, 0)

r = dpsimpy.dp.ph1.Resistor("r")
r.R = 10.0

l = dpsimpy.dp.ph1.Inductor("l")
l.L = 0.05

src.connect([gnd, n1])
r.connect([n1, n2])
l.connect([n2, gnd])

system = dpsimpy.SystemTopology(50, [gnd, n1, n2], [src, r, l])

logger = dpsimpy.Logger(name)
logger.log_attribute("i_l", "i_intf", l)

sim = dpsimpy.Simulation(name)
sim.set_domain(dpsimpy.Domain.DP)
sim.set_system(system)
sim.set_time_step(1e-4)
sim.set_final_time(0.05)
sim.add_logger(logger)
sim.run()

current = rt.read_timeseries_dpsim("logs/" + name + ".csv")["i_l"]

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,

$$|I| = \frac{|V|}{\sqrt{R^2 + (\omega L)^2}} = \frac{100}{\sqrt{10^2 + (2\pi \cdot 50 \cdot 0.05)^2}} = \frac{100}{18.62} = 5.37 \ \mathrm{A},$$

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:

Time0.1 ms step5 ms step
5 ms5.699 A2.953 A
10 ms6.105 A6.256 A
20 ms5.271 A5.276 A
50 ms5.371 A5.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.

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.

A slack bus, a line, and a load at the far bus.

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.

import dpsimpy
import villas.dataprocessing.readtools as rt

Vnom = 20e3

n1pf = dpsimpy.sp.SimNode("n1", dpsimpy.PhaseType.Single)
n2pf = dpsimpy.sp.SimNode("n2", dpsimpy.PhaseType.Single)

slack = dpsimpy.sp.ph1.NetworkInjection("slack")
slack.set_parameters(voltage_set_point=Vnom)
slack.set_base_voltage(Vnom)
slack.modify_power_flow_bus_type(dpsimpy.PowerflowBusType.VD)

line = dpsimpy.sp.ph1.PiLine("line")
line.set_parameters(R=0.5, L=0.5 / 314, C=50e-6)
line.set_base_voltage(Vnom)

load = dpsimpy.sp.ph1.Load("load")
load.set_parameters(active_power=100e3, reactive_power=50e3, nominal_voltage=Vnom)
load.modify_power_flow_bus_type(dpsimpy.PowerflowBusType.PQ)

slack.connect([n1pf])
line.connect([n1pf, n2pf])
load.connect([n2pf])

system_pf = dpsimpy.SystemTopology(50, [n1pf, n2pf], [slack, line, load])

logger_pf = dpsimpy.Logger("pf")
logger_pf.log_attribute("v1", "v", n1pf)
logger_pf.log_attribute("v2", "v", n2pf)

sim_pf = dpsimpy.Simulation("pf")
sim_pf.set_system(system_pf)
sim_pf.set_domain(dpsimpy.Domain.SP)
sim_pf.set_solver(dpsimpy.Solver.NRP)
sim_pf.set_solver_component_behaviour(dpsimpy.SolverBehaviour.Initialization)
sim_pf.do_init_from_nodes_and_terminals(False)
sim_pf.set_time_step(0.1)
sim_pf.set_final_time(0.1)
sim_pf.add_logger(logger_pf)
sim_pf.run()

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.

n1 = dpsimpy.dp.SimNode("n1", dpsimpy.PhaseType.Single)
n2 = dpsimpy.dp.SimNode("n2", dpsimpy.PhaseType.Single)

slack_d = dpsimpy.dp.ph1.NetworkInjection("slack")
slack_d.set_parameters(V_ref=complex(Vnom, 0))

line_d = dpsimpy.dp.ph1.PiLine("line")
line_d.set_parameters(
    series_resistance=0.5,
    series_inductance=0.5 / 314,
    parallel_capacitance=50e-6,
)

load_d = dpsimpy.dp.ph1.RXLoad("load")
load_d.set_parameters(active_power=100e3, reactive_power=50e3, volt=Vnom)

slack_d.connect([n1])
line_d.connect([n1, n2])
load_d.connect([n2])

system_dp = dpsimpy.SystemTopology(50, [n1, n2], [slack_d, line_d, load_d])
system_dp.init_with_powerflow(systemPF=system_pf, domain=dpsimpy.Domain.DP)

logger = dpsimpy.Logger("dyn")
logger.log_attribute("v2", "v", n2)

sim = dpsimpy.Simulation("dyn")
sim.set_system(system_dp)
sim.set_domain(dpsimpy.Domain.DP)
sim.set_time_step(1e-3)
sim.set_final_time(0.05)
sim.add_logger(logger)
sim.run()

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.

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.

The same network with a switched fault branch at the load bus.

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))   # apply
sim.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:

TimeBus voltage
0.1999 s19 090 V
0.2000 s22 684 V
0.2001 s29 036 V
0.2002 s33 103 V
0.2003 s34 011 V
0.2005 s26 726 V
0.2007 s14 642 V
0.2009 s9 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

With the same fault at the same instant:

TimePlain switchVariable-resistance switch
0.1999 s19 090 V19 090 V
0.2001 s29 036 V19 595 V
0.2003 s34 011 V20 432 V
0.2006 s20 511 V20 943 V
0.2009 s9 081 V20 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:

def build(domain, ns, ph1, name, dt):
    gnd = ns.SimNode.gnd
    n1 = ns.SimNode("n1")
    n2 = ns.SimNode("n2")

    src = ph1.VoltageSource("src")
    src.set_parameters(
        V_ref=complex(100, 0),
        f_src=(50.0 if domain == dpsimpy.Domain.EMT else 0.0),
    )

    r = ph1.Resistor("r"); r.set_parameters(R=10.0)
    l = ph1.Inductor("l"); l.set_parameters(L=0.05)

    src.connect([gnd, n1]); r.connect([n1, n2]); l.connect([n2, gnd])

    system = dpsimpy.SystemTopology(50, [gnd, n1, n2], [src, r, l])
    logger = dpsimpy.Logger(name)
    logger.log_attribute("i_l", "i_intf", l)

    sim = dpsimpy.Simulation(name)
    sim.set_domain(domain); sim.set_system(system)
    sim.set_time_step(dt); sim.set_final_time(0.06)
    sim.add_logger(logger)
    sim.run()
    return rt.read_timeseries_dpsim("logs/%s.csv" % name)

emt = build(dpsimpy.Domain.EMT, dpsimpy.emt, dpsimpy.emt.ph1, "cmp_emt", 5e-5)["i_l"]
dp = build(dpsimpy.Domain.DP, dpsimpy.dp, dpsimpy.dp.ph1, "cmp_dp", 1e-3)
dp_shift = ts.frequency_shift_list(dp, 50)["i_l_shift"]

The one parameter that means different things

f_src is not the same quantity in the two domains, and this is the single easiest way to get a wrong answer here.

The comparison

EMT waveform and DP envelope for the same RL circuit

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.

QuantityEMTDP shifted back
Time step50 µs1 ms
Samples over 60 ms120161
Peak current, steady state5.3703 A5.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.

The transform itself is derived under dynamic phasors.

Reading it back

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:

gen = dpsimpy.dp.ph1.SynchronGenerator4OrderVBR("gen")
gen.set_operational_parameters_per_unit(
    nom_power=555e6, nom_voltage=24e3, nom_frequency=60.0, H=3.7,
    Ld=1.81, Lq=1.76, L0=0.15,
    Ld_t=0.3, Lq_t=0.65, Td0_t=8.0, Tq0_t=1.0,
)

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,

$$Z_{base} = \frac{V_{nom}^2}{S_{nom}} = \frac{(24\,\mathrm{kV})^2}{555\,\mathrm{MVA}} = 1.04 \ \Omega .$$

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.

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 angle
gen.set_initial_values(
    init_complex_electrical_power=complex(300e6, 0),
    init_mechanical_power=300e6,
    init_complex_terminal_voltage=vterm,
)

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:

Rotor speed through a fault for third, fourth and sixth order machine models

Model orderPeak speedStates
3rd1.00362 pufield winding only
4th1.00308 pufield and one q-axis damper
6a1.00228 putransient 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.

Configuring the far side

interface_config = {
    "type": "file",
    "format": "csv",
    "uri": "logs/exchanged.csv",
    "out": {"flush": True},
}

interface = dpsimpyvillas.InterfaceVillas(
    name="dpsim-file", config=json.dumps(interface_config)
)

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.

Choosing what crosses the boundary

interface.export_attribute(boundary.attr("i_intf").derive_coeff(0, 0), 0)
sim.add_interface(interface)

Two things are worth reading slowly.

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.

What comes out

The file is written in VILLASnode’s sample format, not as a DPsim result CSV:

# secs,nsecs,offset,sequence,signal0
1785540392,258011384,nan,0,5.00000000000000000+0.00000000000000000i

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.

image

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.

image

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.

image

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:

docker build -t sogno/dpsim:dev -f packaging/Docker/Dockerfile.dev .

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:

cd /dpsim
mkdir build && cd build
cmake ..
cmake --build . --target dpsimpy

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
export PYTHONPATH=$(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:

git clone --branch v3.2.1 --recurse-submodules --depth 1 https://github.com/LLNL/sundials.git
mkdir -p sundials/build && cd sundials/build
cmake .. -DCMAKE_BUILD_TYPE=Release
make -j$(nproc) install

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.

For the C++ libraries only:

mkdir build
cd build
cmake -DWITH_PYBIND=OFF ..
cmake --build . --target dpsim --target dpsim-models --parallel

For the Python bindings, install pybind11 first:

pip install pybind11[global]
mkdir build
cd build
cmake -DWITH_PYBIND=ON ..
cmake --build . --target dpsimpy --parallel

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:

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
brew install gcc git cmake graphviz python3 gsl eigen spdlog

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:

echo "experimental-features=nix-command flakes" > ~/.config/nix/nix.conf

Building DPsim, including all its dependencies:

nix build github:sogno-platform/dpsim

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.

Python

Install Sphinx or use the Docker image, then:

mkdir -p build && cd build
cmake ..
make docs

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.

C++

Install Doxygen or use the Docker image, then:

mkdir -p build && cd build
cmake ..
make docs_cxx

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
  const Attribute<Real>::Ptr mSeriesRes;
  const Attribute<Real>::Ptr mSeriesInd;
  const Attribute<Real>::Ptr mParallelCap;
  const Attribute<Real>::Ptr mParallelCond;

// Component constructor: Initializes the attributes in the initialization list
Base::Ph1::PiLine(CPS::AttributeList::Ptr attributeList) :
  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(String name, Logger::Level logLevel) :
  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&:

AttributeBase::Ptr attr = AttributeStatic<Real>::make(0.001);
Real read1 = attr->get(); //read1 = 0.001
Real read2 = **attr; //read2 = 0.001
Real& read3 = **attr; //read3 = 0.001

The value of an attribute can be changed by either writing to the mutable reference obtained from get, or by calling the set-method:

AttributeBase::Ptr attr = AttributeStatic<Real>::make(0.001);
Real read1 = **attr; //read1 = 0.001
**attr = 0.002;
Real read2 = **attr; //read2 = 0.002
attr->set(0.003);
Real read3 = **attr; //read3 = 0.003

Working with Dynamic Attributes

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:

AttributeBase::Ptr attr1 = AttributeStatic<Real>::make(0.001);
AttributeBase::Ptr attr2 = AttributeDynamic<Real>::make();

attr2->setReference(attr1);

Real read1 = **attr2; //read1 = 0.001
**attr1 = 0.002;
Real read2 = **attr2; //read2 = 0.002

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::Ptr A = AttributeDynamic<Real>::make();
AttributeBase::Ptr B = AttributeDynamic<Real>::make();
AttributeBase::Ptr C = 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::Ptr A = AttributeDynamic<Real>::make();
AttributeBase::Ptr B = AttributeDynamic<Real>::make();
AttributeBase::Ptr C = 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:

AttributeBase::Ptr attr1 = AttributeStatic<Complex>::make(Complex(3, 4));
AttributeBase::Ptr attr2 = attr1->deriveMag();

Real read1 = **attr2; // read1 = 5
**attr1 = Complex(1, 0);
Real read2 = **attr2; // read2 = 1

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:

auto r1 = DP::Ph1::Resistor::make("r_1");
r1->setParameters(5);

auto logger = DataLogger::make("simName");
// Access the attribute through the member variable
logger->logAttribute("i12", r1->mIntfCurrent);

auto intf = 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:

# dpsim-mqtt.py
intf = dpsimpyvillas.InterfaceVillas(name='dpsim-mqtt', config=mqtt_config)
intf.import_attribute(evs.attr('V_ref'), 0, True)
intf.export_attribute(r12.attr('i_intf').derive_coeff(0, 0), 0)

Using Attributes to Schedule Tasks

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:

void DP::Ph1::Inductor::mnaAddPostStepDependencies(
    AttributeBase::List &prevStepDependencies, AttributeBase::List &attributeDependencies,
    AttributeBase::List &modifiedAttributes, Attribute<Matrix>::Ptr &leftVector
  ) {
    attributeDependencies.push_back(leftVector);
  modifiedAttributes.push_back(mIntfVoltage);
  modifiedAttributes.push_back(mIntfCurrent);
}

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:

  1. Does it need to be logged?
  2. Does it need to be imported or exported?
  3. Does it need to be accessed from Python or by name?
  4. Is it used as a scheduler dependency?
  5. Is it an externally relevant model input, output, state, or setpoint?
  6. 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:

const Attribute<Real>::Ptr mPower;

MyComponent::MyComponent(const String& name)
    : IdentifiedObject(name),
      mPower(mAttributes->create<Real>("P", 0.0)) {}

Use dynamic, referenced, or derived attributes only when the value must depend on another attribute.

For example, use a derived attribute when exporting one coefficient of a matrix or vector attribute:

intf->exportAttribute(component->mIntfCurrent->deriveCoeff<Complex>(0, 0), 0, true);

Avoid long chains of dynamic, referenced, or derived attributes unless they are really needed.

Access Attributes in C++

In model code, prefer typed attribute members:

mPower->set(power);
const Real power = **mPower;

Use string-based lookup mainly in generic code, logging, interfaces, tests, or Python-style access:

logger->logAttribute("P", component->mPower);
intf->exportAttribute(component->attribute("P"), 0, true);

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.

class MyComponent : public IdentifiedObject {
private:
  Real mConductance = 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.

class MyComponent : public IdentifiedObject {
public:
  const Attribute<Real>::Ptr mPower;

  explicit MyComponent(const String& name)
      : IdentifiedObject(name),
        mPower(mAttributes->create<Real>("P", 0.0)) {}

  void updatePower(Real power) {
    mPower->set(power);
  }
};

Runtime setpoint: use an attribute

This value is a model input or setpoint. It may be changed from outside the component, for example through Python, an interface, or a test setup.

class MySource : public IdentifiedObject {
public:
  const Attribute<Real>::Ptr mVoltageRef;

  explicit MySource(const String& name)
      : IdentifiedObject(name),
        mVoltageRef(mAttributes->create<Real>("V_ref", 0.0)) {}

  void setParameters(Real voltageRef) {
    mVoltageRef->set(voltageRef);
  }
};

Derived scalar value: use a derived attribute

This value is not stored separately. It is a scalar view of an existing vector or matrix attribute, which avoids duplicating data manually.

intf->exportAttribute(component->mIntfCurrent->deriveCoeff<Complex>(0, 0), 0, true);

3.2.3 - Interfaces

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

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.

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.

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::string mqttConfig = 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 documentation
mqtt_config = '''{
        "type": "mqtt",
        "format": "json",
        "host": "mqtt",
        "in": {
            "subscribe": "/mqtt-dpsim"
        },
        "out": {
            "publish": "/dpsim-mqtt"
        }
}'''

# Creating a new InterfaceVillas object
intf = dpsimpyvillas.InterfaceVillas(name='dpsim-mqtt', config=mqtt_config)

# Configuring the InterfaceVillas to import and export attributes
intf.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
RealTimeSimulation sim(simName);
sim.setSystem(sys);
sim.setTimeStep(timeStep);
sim.setFinalTime(10.0);

// Create and configure interface
auto intf = //...

// 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.

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:

virtual void execute(Real time, Int timeStepCount) = 0;

To participate in scheduling, a task declares its data dependencies through three attribute lists that are populated in the task’s constructor:

ListMeaning
mAttributeDependenciesAttributes this task reads in execute()
mModifiedAttributesAttributes this task writes in execute()
mPrevStepDependenciesAttributes 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.

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:

TaskTypical responsibility
MnaPreStepComponent-specific preparation before the matrix solve, often updating internal state and stamping the right-hand-side contribution
MnaPostStepComponent-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.

image

Scheduler variants

ClassParallelism strategy
SequentialSchedulerSingle-threaded; follows topological order
ThreadLevelSchedulerDistributes each level across N worker threads
ThreadListSchedulerDistributes tasks greedily across N threads
OpenMPLevelSchedulerUses #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:

for (auto& task : mSchedule)
    task->execute(time, timeStepCount);

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():

class MyComponent : public SimSignalComp {
public:
    const Attribute<Real>::Ptr mInput;       // written by upstream component
    const Attribute<Real>::Ptr mOutput;      // read by downstream component
    const Attribute<Real>::Ptr mOutputPrev;  // state carried across timesteps

    class PreStep : public Task {
    public:
        explicit PreStep(MyComponent& comp)
            : Task(**comp.mName + ".PreStep"), mComp(comp) {
            mPrevStepDependencies.push_back(mComp.mOutput);
            mModifiedAttributes.push_back(mComp.mOutputPrev);
        }
        void execute(Real time, Int timeStepCount) override {
            **mComp.mOutputPrev = **mComp.mOutput;
        }
    private:
        MyComponent& mComp;
    };

    class Step : public Task {
    public:
        explicit Step(MyComponent& comp)
            : Task(**comp.mName + ".Step"), mComp(comp) {
            mAttributeDependencies.push_back(mComp.mInput);
            mModifiedAttributes.push_back(mComp.mOutput);
        }
        void execute(Real time, Int timeStepCount) override {
            mComp.signalStep(time, timeStepCount);
        }
    private:
        MyComponent& mComp;
    };

    Task::List getTasks() 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.

void DP::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
}

void DP::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.

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:

ListContents
mMNAComponentsStatic MNA power components
mMNAIntfVariableCompsVariable-stamp MNA components (e.g. under MNAVariableCompInterface)
mMNAIntfSwitchesComponents with a switch interface
mSimSignalCompsSignal 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.

Step 5 — Initialize components (initializeComponents)

This step has three sub-passes over the component lists.

5a — Power components: initializeFromNodesAndTerminals

For every SimPowerComp<VarType> in mMNAComponents and mMNAIntfVariableComps:

  1. checkForUnconnectedTerminals() validates connectivity.
  2. 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.

5c — MNA components: mnaInitialize

Each MNA component (including switches) receives mnaInitialize(omega, timeStep, leftVector). In MNASimPowerComp this method:

  1. Clears and re-registers MNAPreStep / MNAPostStep tasks according to the hasPreStep / hasPostStep flags.
  2. Initializes mRightVector to zero with the correct size.
  3. 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:

ValueWhen activeTypical use
Behaviour::InitializationDuring PF steady-state init passComponents may disable transient update equations
Behaviour::PFSimulationDuring PFSolver runActivates power-flow-specific stamping
Behaviour::MNASimulationAfter initialize() completesNormal 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.

ResponsibilityConstructor / setParameterscreateSubComponentsinitializeFromNodesAndTerminalsmnaCompInitialize
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.

Composite Component Initialization Sequence

The following diagram shows how the solver and a composite component interact during initialization. For further details see Subcomponent Handling.

sequenceDiagram participant MNA as MnaSolver participant CC as CompositePowerComp participant SC as SubComponent Note over MNA,SC: Step 2 - pre-pass (topology only) MNA->>CC: createSubComponents() CC->>SC: make_shared + connect() + addMNASubComponent() Note over MNA,SC: Step 3 - matrix sizing MNA->>CC: collectVirtualNodes() MNA->>MNA: assignMatrixNodeIndices() Note over MNA,SC: Step 5a - parameterization MNA->>CC: initializeFromNodesAndTerminals(freq) CC->>CC: createSubComponents() idempotent guard CC->>CC: initializeParentFromNodesAndTerminals(freq) CC->>SC: initialize(frequencies) CC->>SC: initializeFromNodesAndTerminals(freq) Note over MNA,SC: Step 5c - MNA setup MNA->>CC: mnaInitialize(omega, dt, v) CC->>SC: mnaInitialize(omega, dt, v) CC->>CC: mnaParentInitialize(omega, dt, v)

PFSolver Initialization

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.

flowchart TD pf[PFSolver::initialize] --> p1[Classify components\ninto generator/load/line/... lists] p1 --> p2[setBaseApparentPower\nCompute per-unit base] p2 --> p3[assignMatrixNodeIndices] p3 --> p4[initializeComponents\ninitializeFromNodesAndTerminals\ncalculatePerUnitParameters] p4 --> p5[determinePFBusType\nPQ / PV / VD] p5 --> p6[determineNodeBaseVoltages] p6 --> p7[composeAdmittanceMatrix\nBuild Y-bus] p7 --> done([Ready to solve power flow])

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::initialize(Matrix frequencies) naming clash

SimPowerComp<T>::initialize(Matrix frequencies) is called by the solver to propagate frequency information down the component tree.

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) after initializeFromNodesAndTerminals 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:

uname -v | grep -q PREEMPT_RT && echo "already real-time" || echo "not a PREEMPT_RT kernel"

If you need one, most distributions still offer a binary package. On Rocky Linux:

sudo dnf --enablerepo=rt install kernel-rt kernel-rt-devel

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:

sudo grubby --update-kernel=ALL --args="isolcpus=9,11,13,15 nohz_full=9,11,13,15"

Something similar, but less invasive and non-permanent can be achieved using tuna:

sudo tuna isolate -c 9,11,13,15

To avoid real-time throttling to cause overruns disable this feature:

sudo bash -c "echo -1 > /proc/sys/kernel/sched_rt_runtime_us"

Note that this is not persistent when rebooting.

Simulation Model Tuning

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.

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.

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:

virtual void mnaCompInitialize(Real omega, Real timeStep, Attribute<Matrix>::Ptr leftVector);
virtual void mnaCompApplySystemMatrixStamp(SparseMatrixRow& systemMatrix);
virtual void mnaCompApplyRightSideVectorStamp(Matrix& rightVector);
virtual void mnaCompUpdateVoltage(const Matrix& leftVector);
virtual void mnaCompUpdateCurrent(const Matrix& leftVector);
virtual void mnaCompPreStep(Real time, Int timeStepCount);
virtual void mnaCompPostStep(Real time, Int timeStepCount, Attribute<Matrix>::Ptr &leftVector);
virtual void mnaCompAddPreStepDependencies(AttributeBase::List &prevStepDependencies, AttributeBase::List &attributeDependencies, AttributeBase::List &modifiedAttributes);
virtual void mnaCompAddPostStepDependencies(AttributeBase::List &prevStepDependencies, AttributeBase::List &attributeDependencies, AttributeBase::List &modifiedAttributes, Attribute<Matrix>::Ptr &leftVector);
virtual void mnaCompInitializeHarm(Real omega, Real timeStep, std::vector<Attribute<Matrix>::Ptr> leftVector);
virtual void mnaCompApplySystemMatrixStampHarm(SparseMatrixRow& systemMatrix, Int freqIdx);
virtual void mnaCompApplyRightSideVectorStampHarm(Matrix& sourceVector);
virtual void mnaCompApplyRightSideVectorStampHarm(Matrix& sourceVector, Int freqIdx);

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:

void DP::Ph1::Resistor::mnaCompInitialize(Real omega, Real timeStep, Attribute<Matrix>::Ptr leftVector) {
  updateMatrixNodeIndices();

  **mRightVector = Matrix::Zero(0, 0);
  //...
}

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.)

  1. 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).
  2. 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.
  3. MNA-init stage (mnaCompInitialize()). Unchanged; already recurses into sub-components.

CompositePowerComp<VarType>::initializeFromNodesAndTerminals() is final and sequences these stages:

void initializeFromNodesAndTerminals(Real frequency) 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 (auto subComp : 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.

// DP_Ph1_PiLine.cpp
DP::Ph1::PiLine::PiLine(String uid, String name, Logger::Level logLevel)
  : Base::Ph1::PiLine(mAttributes),
  // Call the constructor of CompositePowerComp and enable automatic pre- and post-step creation
  CompositePowerComp<Complex>(uid, name, true, true, logLevel)
{
  //...
}

void DP::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);
  //...
}

void DP::Ph1::PiLine::initializeParentFromNodesAndTerminals(Real frequency) {
  //...
  // Frequency-dependent values go here, not in createSubComponents().
  Real omega = 2. * PI * frequency;
  Complex impedance = {**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:

void DP::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);
}
void DP::Ph1::PiLine::mnaCompAddPreStepDependencies(AttributeBase::List &prevStepDependencies, AttributeBase::List &attributeDependencies, AttributeBase::List &modifiedAttributes) {
  // Manually add pre-step dependencies of subcomponents
  for (auto subComp : 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:

CPS::{DP,EMT,SP}::{Ph1,Ph3}::{Name}
CPS::Signal::{Name}

File names encode the same information, so the example model needs two files:

  • dpsim-models/include/dpsim-models/DP/DP_Ph3_Inductor.h
  • dpsim-models/src/DP/DP_Ph3_Inductor.cpp

declaring the class CPS::DP::Ph3::Inductor.

Choosing a base class

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.

How to declare, read and derive attributes is described under attributes and attribute usage.

Tasks and step functions

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:

py::class_<CPS::DP::Ph1::Resistor, std::shared_ptr<CPS::DP::Ph1::Resistor>,
           CPS::SimPowerComp<CPS::Complex>>(mDPPh1, "Resistor", py::multiple_inheritance())
    .def("set_parameters", &CPS::DP::Ph1::Resistor::setParameters, "R"_a);

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:

my-project
  |- CMakeLists.txt
  |- source
      |- my-scenario.cpp
  |- dpsim (as submodule)

CMake File

Your CMakeLists.txt could look like this:

cmake_minimum_required(VERSION 3.14)
project(my-project CXX)

add_subdirectory(dpsim)

add_executable(my-scenario source/my-scenario.cpp)
target_link_libraries(my-scenario dpsim)

Build the Project

The build process is similar to the one of DPsim:

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

ClassPurpose
DPsim::DataLoggerNumerical results to CSV, one row per step
DPsim::RealTimeDataLoggerThe same results, buffered in memory for real-time runs
DPsim::DataLoggerInterfaceThe seam both implement, and the one to implement for a new sink
CPS::LoggerThe 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 methods themselves are derived under alternative solution methods.

3.4.1 - The MNA Solver

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:

  1. Run the inner Newton-Raphson solve to convergence (as described above).
  2. 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.
  3. 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.
  4. 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.

The methods are derived under alternative solution methods. This page covers the code and the configuration.

DAESolver

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

ImplementationAdapterNotes
KLUKLUAdapterDefault, and the fallback when the choice is Undef
SparseLUSparseLUAdapterEigen’s sparse LU
DenseLUDenseLUAdapterDense, for small systems
CUDADenseGpuDenseAdapterRequires a CUDA build
CUDASparseGpuSparseAdapterRequires a CUDA build
CUDAMagmaGpuMagmaAdapterRequires a CUDA build with Magma
Pluginloaded at runtimeFor 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.

For the components that support extraction in each domain, see state-space extraction support.

Usage

In C++, state-space extraction can be enabled as follows. The example below uses EMT Ph3; replace Domain::EMT with Domain::DP for DP Ph1:

Simulation sim("Example");
sim.setDomain(Domain::EMT);
sim.setSolverType(Solver::Type::MNA);
sim.doStateSpaceExtraction(true);
sim.run();

const auto &extractor = sim.getStateSpaceExtractor();
const Matrix &Ad = extractor.getDiscreteStateMatrix();

In Python, the corresponding API is shown below. Replace dpsimpy.Domain.EMT with dpsimpy.Domain.DP for DP Ph1:

sim = dpsimpy.Simulation("Example")
sim.set_domain(dpsimpy.Domain.EMT)
sim.set_solver(dpsimpy.Solver.MNA)
sim.do_state_space_extraction(True)
sim.run()

extractor = sim.get_state_space_extractor()
Ad = extractor.get_discrete_state_matrix()

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.

Examples

The feature is demonstrated in:

Equivalent Python notebooks are available in examples/Notebooks/StateSpace.

3.5 - Model Implementations

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.

The equations are derived under reduced order machine models. This page covers only their arrangement in code.

Class hierarchy

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].

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.

MemberSymbol
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_ssubtransient 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.

Source code

References

  • [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.

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

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/
  • Loads: DP_Ph1_RXLoad, EMT_Ph3_RXLoad, DP_Ph1_RXLoadSwitch, DP_Ph1_PQLoadCS, SP_Ph1_Load, {SP,DP}_Ph1_Shunt, EMT_Ph3_Shunt

3.5.3 - Source Implementation

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.

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.

The models are derived under network injection and compensation. This page covers only the code.

NetworkInjection

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.

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.

Source

Availability per domain is in model availability.

3.5.5 - SSN Component Implementation

The SSN base class hierarchy and what a new SSN component has to provide.

The method is derived under state-space nodal components and state-space nodal. This page covers only the code.

Base hierarchy

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:

Matrix aMatrix = Matrix::Zero(3, 3);   // x = i_abc
Matrix bMatrix = inductance.inverse(); // u = v_abc
Matrix cMatrix = Matrix::Identity(3, 3);
Matrix dMatrix = 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 same Math::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.

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.

The models are derived under converter control and signal processing blocks. This page covers only the code.

Base and scheduling

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:

**mStateCurr = **mStatePrev + mTimeStep / 2.0 * **mInputCurr
                            + mTimeStep / 2.0 * **mInputPrev;
**mOutputCurr = **mStateCurr;

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

mA << 0, mKi, 0, 0;
mB << 1, mKp, 0, 1;
mC << 1, 0, 0, 1;
mD << 0, 0, 0, 0;

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.

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.

The equations are derived under DP Ph1 averaged voltage source inverter. This page covers only their arrangement in code.

Class and base

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.

IndexNameKind
0PsiPLL angle deviation from the nominal carrier phase
1PhiPLLPLL integrator
2, 3PFiltered, QFilteredpower filter
4, 5PhiD, PhiQouter power control integrators
6, 7GammaD, GammaQinner current control integrators
8, 9VcRe, VcImfilter capacitor voltage envelope
10, 11IfRe, IfImfilter 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

initializeFromNodesAndTerminals derives the initial state from the connected node voltage, so the operating point comes from the powerflow rather than from user supplied states.

Source and examples

3.5.8 - DP Ph3 Averaged VSI Implementation

How the three-phase dynamic phasor averaged inverter is arranged in code.

The equations are derived under DP Ph3 averaged voltage source inverter. This page covers only their arrangement in code.

Class and base

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.

IndexNameKind
0–5VcAReVcCImfilter capacitor voltage envelope, per phase
6–11IfAReIfCImfilter inductor current envelope, per phase
12PsiPLL angle deviation from the nominal carrier phase
13PhiPLLPLL integrator
14, 15PFiltered, QFilteredpower filter
16, 17PhiD, PhiQouter power control integrators
18, 19GammaD, GammaQinner current control integrators
20, 21GammaND, GammaNQnegative-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.

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.

The theory behind the loop is derived under DP Ph3 averaged VSI.

Source and examples

3.5.9 - EMT Ph3 Averaged VSI Implementation

How the EMT averaged inverter is arranged in code and interfaced to the solver.

The equations are derived under EMT Ph3 averaged voltage source inverter. This page covers only their arrangement in code.

Class and base

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.

IndexNameKind
0ThetaPLLPLL angle
1PhiPLLPLL integrator
2, 3PFiltered, QFilteredpower filter
4, 5PhiD, PhiQouter power control integrators
6, 7GammaD, GammaQinner current control integrators
8–10VcA, VcB, VcCfilter capacitor voltage, per phase
11–13IfA, IfB, IfCfilter 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.

Source and examples

3.5.10 - EMT Ph3 Grid-Forming VSI Implementation

How the grid-forming inverter is linearized, stamped and configured.

The equations and the linearization mathematics are derived under EMT Ph3 grid-forming voltage source inverter. This page covers only their arrangement in code.

Class and base

EMT::Ph3::SSN_GFM is final and derives from EMT::Ph3::TwoTerminalVTypeVariableSSNComp. All seventeen states are real.

State layout

IndexNameKind
0, 1PFiltered, QFilteredpower filter
2, 3Omega, Thetadroop frequency and angle
4VoltageMagnitudevoltage droop output
5, 6VoltageIntegratorD, VoltageIntegratorQouter voltage control
7, 8CurrentIntegratorD, CurrentIntegratorQinner current control
9, 10DelayVoltageD, DelayVoltageQmodulation delay
11–13VcA, VcB, VcCfilter capacitor voltage, per phase
14–16IfA, IfB, IfCfilter 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.

Source and examples

4 - Concepts

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$.

$$\boldsymbol{Y} \boldsymbol{v} = \boldsymbol{i}$$

Assembling the System by Stamping

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

$$i(t) = \frac{2C}{\Delta t} v(t) - \left( \frac{2C}{\Delta t} v(t - \Delta t) + i(t - \Delta t) \right),$$

and the same treatment of an inductor gives

$$i(t) = \frac{\Delta t}{2L} v(t) + \left( \frac{\Delta t}{2L} v(t - \Delta t) + i(t - \Delta t) \right).$$

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

$$\begin{bmatrix} \boldsymbol{Y} & \boldsymbol{A} \\ \boldsymbol{A}^{\mathsf{T}} & \boldsymbol{0} \end{bmatrix} \begin{bmatrix} \boldsymbol{v} \\ \boldsymbol{j} \end{bmatrix} = \begin{bmatrix} \boldsymbol{i} \\ \boldsymbol{u} \end{bmatrix},$$

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.

Such a signal can be written exactly as

$$x(t) = \operatorname{Re}\{\, X(t)\, e^{j \omega_s t} \,\},$$

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

$$\frac{dx}{dt} = \operatorname{Re}\left\{ \left( \frac{dX}{dt} + j \omega_s X \right) e^{j \omega_s t} \right\},$$

so in envelope coordinates the time derivative becomes

$$\frac{d}{dt} \;\longrightarrow\; \frac{d}{dt} + j \omega_s .$$

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,

$$X_k(t) = \frac{1}{T} \int_{t-T}^{t} x(\tau)\, e^{-j k \omega_s \tau} \, d\tau .$$

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,

$$\frac{dX_k}{dt} = \left\langle \frac{dx}{dt} \right\rangle_k - j k \omega_s X_k ,$$

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 TypeKnownUnknown
$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:

$$-\textbf{J}(\vec{x}) \Delta \vec{x} = \vec{f} (\vec{x})$$

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

$$\left [ \begin{array}{c} \Delta \vec{P} \\ \Delta \vec{Q} \end{array} \right ]$$

or the current mismatch $\Delta \vec{I}$ in terms of

$$\left [ \begin{array}{c} \Delta \vec{I_{real}} \\ \Delta \vec{I_{imag}} \end{array} \right ]$$

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

$$\left [ \begin{array}{c} \vec{\delta} \\ \vert \vec{V} \vert \end{array} \right ]$$

or rectangular coordinates

$$\left [ \begin{array}{c} \vec{V_{real}} \\ \vec{V_{imag}} \end{array} \right ]$$

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:

$$I_{k} = \sum_{j=1}^{N} Y_{kj} V_{j}$$

Substitution yields:

$$\begin{align} S_{k} &= V_{k} \left ( \sum_{j=1}^{N} Y_{kj} V_{j} \right )^{*} \nonumber \\ &= V_{k} \sum_{j=1}^{N} Y_{kj}^{*} V_{j} ^{*} \nonumber \end{align}$$

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:

$$\begin{align} S_{k} &= V_{k} \sum_{j=1}^{N} Y_{kj}^{*} V_{j}^{*} \nonumber \\ &= \vert V_{k} \vert \angle \theta_{k} \sum_{j=1}^{N} (G_{kj} + jB_{kj})^{*} ( \vert V_{j} \vert \angle \theta_{j})^{*} \nonumber \\ &= \vert V_{k} \vert \angle \theta_{k} \sum_{j=1}^{N} (G_{kj} - jB_{kj}) ( \vert V_{j} \vert \angle - \theta_{j}) \nonumber \\ &= \sum_{j=1} ^{N} \vert V_{k} \vert \angle \theta_{k} ( \vert V_{j} \vert \angle - \theta_{j}) (G_{kj} - jB_{kj}) \nonumber \\ &= \sum_{j=1} ^{N} \left ( \vert V_{k} \vert \vert V_{j} \vert \angle (\theta_{k} - \theta_{j}) \right ) (G_{kj} - jB_{kj}) \nonumber \\ &= \sum_{j=1} ^{N} \vert V_{k} \vert \vert V_{j} \vert \left ( cos(\theta_{k} - \theta_{j}) + jsin(\theta_{k} - \theta_{j}) \right ) (G_{kj} - jB_{kj}) \end{align}$$

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:

$$\begin{align} {P}_{k} = \sum_{j=1}^{N} \vert V_{k} \vert \vert V_{j} \vert \left ( G_{kj}cos(\theta_{k} - \theta_{j}) + B_{kj} sin(\theta_{k} - \theta_{j}) \right ) \\ {Q}_{k} = \sum_{j=1}^{N} \vert V_{k} \vert \vert V_{j} \vert \left ( G_{kj}sin(\theta_{k} - \theta_{j}) - B_{kj} cos(\theta_{k} - \theta_{j}) \right ) \end{align}$$

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$:

$$\begin{align} \vec{x} = \left[ \begin{array}{c} \vec{\theta} \\ \vert \vec{V} \vert \\ \end{array} \right ] = \left[ \begin{array}{c} \theta_{2} \\ \theta_{3} \\ \vdots \\ \theta_{N} \\ \vert V_{N_{PV+1}} \vert \\ \vert V_{N_{PV+2}} \vert \\ \vdots \\ \vert V_{N} \vert \end{array} \right] \end{align}$$

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:

$$\begin{align} P_{k} = P_{k} (\vec{x}) \Rightarrow P_{k}(\vec{x}) - P_{k} &= 0 \quad \quad k = 2,...,N \\ Q_{k} = Q_{k} (\vec{x}) \Rightarrow Q_{k} (\vec{x}) - Q_{k} &= 0 \quad \quad k = N_{PV}+1,...,N \end{align}$$

We now define the mismatch vector $\vec{f} (\vec{x})$ as:

$$\begin{align} \vec{f} (\vec{x}) = \left [ \begin{array}{c} f_{1}(\vec{x}) \\ \vdots \\ f_{N-1}(\vec{x}) \\ ------ \\ f_{N}(\vec{x}) \\ \vdots \\ f_{2N-N_{PV} -1}(\vec{x}) \end{array} \right ] = \left [ \begin{array}{c} P_{2}(\vec{x}) - P_{2} \\ \vdots \\ P_{N}(\vec{x}) - P_{N} \\ --------- \\ Q_{N_{PV}+1}(\vec{x}) - Q_{N_{PV}+1} \\ \vdots \\ Q_{N}(\vec{x}) - Q_{N} \end{array} \right] = \left [ \begin{array}{c} \Delta P_{2} \\ \vdots \\ \Delta P_{N} \\ ------ \\ \Delta Q_{N_{PV}+1} \\ \vdots \\ \Delta Q_{N} \end{array} \right ] = \vec{0} \end{align}$$

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:

$$\begin{align} J_{jk}^{P \theta} &= \frac{\partial P_{j} (\vec{x} ) } {\partial \theta_{k}} = \vert V_{j} \vert \vert V_{k} \vert \left ( G_{jk} sin(\theta_{j} - \theta_{k}) - B_{jk} cos(\theta_{j} - \theta_{k} ) \right ) \\ J_{jj}^{P \theta} &= \frac{\partial P_{j}(\vec{x})}{\partial \theta_{j}} = -Q_{j} (\vec{x} ) - B_{jj} \vert V_{j} \vert ^{2} \\ J_{jk}^{Q \theta} &= \frac{\partial Q_{j}(\vec{x})}{\partial \theta_{k}} = - \vert V_{j} \vert \vert V_{k} \vert \left ( G_{jk} cos(\theta_{j} - \theta_{k}) + B_{jk} sin(\theta_{j} - \theta_{k}) \right ) \\ J_{jj}^{Q \theta} &= \frac{\partial Q_{j}(\vec{x})}{\partial \theta_{k}} = P_{j} (\vec{x} ) - G_{jj} \vert V_{j} \vert ^{2} \\ J_{jk}^{PV} &= \frac{\partial P_{j} (\vec{x} ) } {\partial \vert V_{k} \vert } = \vert V_{j} \vert \left ( G_{jk} cos(\theta_{j} - \theta_{k}) + B_{jk} sin(\theta_{j} - \theta_{k}) \right ) \\ J_{jj}^{PV} &= \frac{\partial P_{j}(\vec{x})}{\partial \vert V_{j} \vert } = \frac{P_{j} (\vec{x} )}{\vert V_{j} \vert} + G_{jj} \vert V_{j} \vert \\ J_{jk}^{QV} &= \frac{\partial Q_{j} (\vec{x} ) } {\partial \vert V_{k} \vert } = \vert V_{j} \vert \left ( G_{jk} sin(\theta_{j} - \theta_{k}) + B_{jk} cos(\theta_{j} - \theta_{k}) \right ) \\ J_{jj}^{QV} &= \frac{\partial Q_{j}(\vec{x})}{\partial \vert V_{j} \vert } = \frac{Q_{j} (\vec{x} )}{\vert V_{j} \vert} - B_{jj} \vert V_{j} \vert \\ \end{align}$$

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:

$$\begin{align} -\left [ \begin{array}{cccccc} \frac{\partial \Delta P_{2} }{\partial \theta_{2}} & \cdots & \frac{\partial \Delta P_{2} }{\partial \theta_{N}} & \frac{\partial \Delta P_{2} }{\partial \vert V_{N_{G+1}} \vert} & \cdots & \frac{\partial \Delta P_{2} }{\partial \vert V_{N} \vert} \\ \vdots & \ddots & \vdots & \vdots & \ddots & \vdots \\ \frac{\partial \Delta P_{N} }{\partial \theta_{2}} & \cdots & \frac{\partial \Delta P_{N}}{\partial \theta_{N}} & \frac{\partial \Delta P_{N}}{\partial \vert V_{N_{G+1}} \vert } & \cdots & \frac{\partial \Delta P_{N}}{\partial \vert V_{N} \vert} \\ \frac{\partial \Delta Q_{N_{G+1}} }{\partial \theta_{2}} & \cdots & \frac{\partial \Delta Q_{N_{G+1}} }{\partial \theta_{N}} & \frac{\partial \Delta Q_{N_{G+1}} }{\partial \vert V_{N_{G+1}} \vert } & \cdots & \frac{\partial \Delta Q_{N_{G+1}} }{\partial \vert V_{N} \vert} \\ \vdots & \ddots & \vdots & \vdots & \ddots & \vdots \\ \frac{\partial \Delta Q_{N}}{\partial \theta_{2}} & \cdots & \frac{\partial \Delta Q_{N}}{\partial \theta_{N}} & \frac{\partial \Delta Q_{N}}{\partial \vert V_{N_{G+1}} \vert } & \cdots & \frac{\partial \Delta Q_{N}}{\partial \vert V_{N} \vert} \end{array} \right ] \left [ \begin{array}{c} \Delta \theta_{2} \\ \vdots \\ \Delta \theta_{N} \\ \Delta \vert V_{N_{G+1}} \vert \\ \vdots \\ \Delta \vert V_{N} \vert \end{array} \right ] = \left [ \begin{array}{c} \Delta P_{2} \\ \vdots \\ \Delta P_{N} \\ \Delta Q_{N_{G+1}} \\ \vdots \\ \Delta Q_{N} \end{array} \right ] \end{align}$$

Solution of the Problem

The solution update formula is given by:

$$\begin{align} \vec{x}^{(i+1)} = \vec{x}^{(i)} + \Delta \vec{x}^{(i)} = \vec{x}^{(i)} - \textbf{J}^{-1} \vec{f} (\vec{x}^{(i)}) \end{align}$$

To sum up, the NR algorithm, for application to the power flow problem is:

  1. Set the iteration counter to $i=1$. Use the initial solution $V_{i} = 1 \angle 0^{\circ}$
  2. Compute the mismatch vector $\vec{f}({\vec{x}})$ using the power flow equations
  3. 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.
  4. Evaluate the Jacobian matrix $\textbf{J}^{(i)}$ and compute $\Delta \vec{x}^{(i)}$.
  5. Compute the update solution vector $\vec{x}^{(i+1)}$. Return to step 3.
  6. 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:

$$\alpha = \min \left( 1,\ \frac{\Delta\theta_{max}}{\max_k \vert \Delta\theta_k \vert},\ \frac{\Delta V_{max}}{\max_k \vert \Delta V_k / V_k \vert} \right)$$

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

$$\frac{d\mathbf{x}}{dt} = \mathbf{A}\mathbf{x} + \mathbf{B}\mathbf{u}, \qquad \mathbf{y} = \mathbf{C}\mathbf{x} + \mathbf{D}\mathbf{u},$$

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 construction of the stamp itself is unchanged and is derived under state-space nodal components.

The instantaneous case

Here the state equation is integrated as written,

$$\dot{\boldsymbol{x}} = \boldsymbol{A}\boldsymbol{x} + \boldsymbol{B}\boldsymbol{u},$$

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

$$\frac{d}{dt} \; \longrightarrow \; \frac{d}{dt} + j\omega_s ,$$

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,

$$\begin{bmatrix} \dot{\boldsymbol{x}}_{re} \\ \dot{\boldsymbol{x}}_{im} \end{bmatrix} = \begin{bmatrix} \boldsymbol{A} & \omega_s \boldsymbol{I} \\ -\omega_s \boldsymbol{I} & \boldsymbol{A} \end{bmatrix} \begin{bmatrix} \boldsymbol{x}_{re} \\ \boldsymbol{x}_{im} \end{bmatrix} + \begin{bmatrix} \boldsymbol{B} & \boldsymbol{0} \\ \boldsymbol{0} & \boldsymbol{B} \end{bmatrix} \begin{bmatrix} \boldsymbol{u}_{re} \\ \boldsymbol{u}_{im} \end{bmatrix}.$$

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.

Assembled Form

The extracted model is assembled in the form:

$$\mathbf{x}[k+1] = \mathbf{A}_{d,\mathrm{local}} \mathbf{x}[k] + \mathbf{B}_{d,\mathrm{MNA}} \mathbf{x}_{\mathrm{MNA}}[k+1]$$
$$\mathbf{Y} \mathbf{x}_{\mathrm{MNA}}[k+1] = \mathbf{C}_{d,\mathrm{MNA}} \mathbf{x}[k]$$

where:

  • $\mathbf{x}$ is the extraction-state vector,
  • $\mathbf{x}_{\mathrm{MNA}}$ is the full MNA unknown vector,
  • $\mathbf{Y}$ is the active MNA system matrix,
  • $\mathbf{A}_{d,\mathrm{local}}$ contains local component state-transition contributions,
  • $\mathbf{B}_{d,\mathrm{MNA}}$ maps MNA unknowns to the state update,
  • $\mathbf{C}_{d,\mathrm{MNA}}$ maps extraction states to MNA current injections.

Eliminating the MNA unknown vector gives the global discrete-time state matrix

$$\mathbf{A}_{d} = \mathbf{A}_{d,\mathrm{local}} + \mathbf{B}_{d,\mathrm{MNA}} \operatorname{solve} \left( \mathbf{Y}, \mathbf{C}_{d,\mathrm{MNA}} \right)$$

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

$$\lambda = \frac{2}{\Delta t} \, \frac{z - 1}{z + 1}.$$

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,

$$\boldsymbol{F}\!\left(t, \boldsymbol{x}, \dot{\boldsymbol{x}}\right) = \boldsymbol{0},$$

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,

$$\boldsymbol{v} = \boldsymbol{Y}^{-1}\boldsymbol{i} - \boldsymbol{Y}^{-1}\boldsymbol{K} \left( \boldsymbol{Z}_{tear} + \boldsymbol{K}^{\mathsf{T}}\boldsymbol{Y}^{-1}\boldsymbol{K} \right)^{-1} \boldsymbol{K}^{\mathsf{T}}\boldsymbol{Y}^{-1}\boldsymbol{i},$$

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.

Interfaces and compensation. SSN components covers components solved simultaneously with the network, the ideal transformer model the general way of splitting a network, and network injection and compensation the representation of the grid beyond the boundary.

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$.

$$i_L(t) = i_L(t - \Delta t) + \frac{1}{L} \ \int_{t - \Delta t}^{t} v_L(\tau) \ \mathrm{d} \tau$$

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:

$$\int_{t - \Delta t}^{t} f(\tau) \ \mathrm{d} \tau \approx \frac{\Delta t}{2}(f(t) + f(t - \Delta t))$$

Applying the trapezoidal rule to leads to

$$i_L(t) = i_L(t - \Delta t) + \frac{\Delta t}{2L}(v_L(t) + v_L(t - \Delta t))$$

This can be rewritten in terms of an equivalent conductance and current source and the number of time steps $k$ with size $\Delta t$.

$$i_L(k) = g_L v_L(k) + i_{L,equiv}(k-1)$$
$$i_{L,equiv}(k-1) = i_L(k-1) + \frac{\Delta t}{2L} v_L(k-1)$$
$$g_L = \frac{\Delta t}{2L}$$

Hence, components described by differential equations are transformed into a DC equivalent circuit as depicted in the figure below.

inductance resistive companion

Capacitance

The same procedure can be applied to a capacitance. Integration on both side yields

$$i_C(t) = C \frac{\mathrm{d}}{\mathrm{d}t} \ v_C(t)$$
$$v_C(t) = v_C(t - \Delta t) + \frac{1}{C} \int_{t - \Delta t}^t i_C(\tau) \mathrm{d} \tau$$

Finally, the equivalent circuit is described by a current source and a conductance.

$$i_{C}(k) = g_{C} v_C(k) + i_{C,equiv}(k-1)$$
$$i_{C,equiv}(k-1) = -i_{C}(k-1) - g_C v_C(k-1)$$
$$g_{C} = \frac{2C}{\Delta t}$$

This equation set is visualized in the figure below.

capacitance resistive companion

Hence, the vector of unknowns $\boldsymbol{x}$ and the source vector $\boldsymbol{b}$ become time dependent and this leads to the system description:

$$\boldsymbol{A} \boldsymbol{x}(t) = \boldsymbol{b}(t)$$

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}$$
$$\langle i_L \rangle(t) = \langle i_L \rangle(t - \Delta t) + \int_{t - \Delta t}^t \frac{1}{L} \langle v_L \rangle(\tau) - j \omega \ \langle i_L \rangle(\tau) \mathrm{d} \tau$$

Applying the trapezoidal method leads to the finite difference equation:

$$\begin{aligned} \langle i_L \rangle(k) = \langle i_L \rangle(k-1) + \frac{\Delta t}{2} \bigg[ \frac{1}{L} (\langle v_L \rangle(k) + \langle v_L \rangle(k-1)) - j \omega (\langle i_L \rangle(t) + \langle i_L \rangle(k-1) \bigg] \end{aligned}$$

Solving this for $\langle i_L \rangle(k)$ results in the \ac{DP} equivalent circuit model:

$$\langle i_L \rangle(k) = \frac{a - jab}{1 + b^2} \langle v_L \rangle(k) + \langle i_{L,equiv} \rangle(k-1)$$

with

$$a = \frac{\Delta t}{2L}, \qquad b = \frac{\Delta t \omega}{2}$$
$$\langle i_{L,equiv} \rangle(k-1) = \frac{1 - b^2 - j2b}{1 + b^2} \langle i_L \rangle(k-1) + \frac{a - jab}{1 + b^2} \langle v_L \rangle(k-1)$$

Capacitance

Similarly, a capacitance is described by as follows

$$\langle i_C \rangle(k) = C \ \frac{\mathrm{d} \langle v_C \rangle}{\mathrm{d} t} + j \omega C \ \langle v_C \rangle(t)$$
$$v_C(t) = v_C(t- \Delta t) + \int_{t- \Delta t}^{t} \frac{1}{C} \ i_C(\tau) -j \omega \ v_C(\tau) \ \mathrm{d} \tau$$

Applying the trapezoidal rule for the capacitance equation leads to the finite difference equation:

$$\begin{aligned} \langle v_C \rangle(k) = \langle v_C \rangle(k-1) + \frac{\Delta t}{2} \bigg[ \frac{1}{C} \ \langle i_C \rangle(k) - j \omega \ \langle v_C \rangle(k) \\ + \frac{1}{C} \ \langle i_C \rangle(k-1) - j \omega \ \langle v_C \rangle(k-1) \bigg] \end{aligned}$$

The DP model for the capacitance is defined by

$$\langle i_C \rangle(k) = \frac{1+jb}{a} \ \langle v_C \rangle(k) + \langle i_{C,equiv} \rangle(k-1)$$

with

$$a = \frac{\Delta t}{2C}, \qquad b = \frac{\Delta t \omega}{2}$$
$$\langle i_{C,equiv} \rangle(k-1) = - \frac{1-jb}{a} \ \langle v_C \rangle(k-1) - \langle i_C \rangle(k-1)$$

RL-series element

In dynamic phasors the integration of the inductance equation yields

$$\langle v \rangle(t) = L \frac{\mathrm{d}}{dt} \langle i \rangle(t) + j \omega L \ \langle i \rangle(t) + R \ \langle i \rangle(t)$$
$$\langle i \rangle(t) = \langle i \rangle(t - \Delta t) + \int_{t - \Delta t}^t \frac{1}{L} \langle v \rangle(\tau) - j \omega \ \langle i \rangle(\tau) - \frac{R}{L} \ \langle i \rangle(\tau) \mathrm{d} \tau$$

Applying the trapezoidal method leads to the finite difference equation:

$$\begin{aligned} \langle i \rangle(k) = \langle i \rangle(k-1) + \frac{\Delta t}{2} \bigg[ \frac{1}{L} (\langle v \rangle(k) + \langle v \rangle(k-1)) - \left( j \omega + \frac{R}{L} \right) (\langle i \rangle(k) + \langle i \rangle(k-1)) \bigg] \end{aligned}$$

Solving this for $\langle i \rangle(k)$ results in the \ac{DP} equivalent circuit model:

$$\langle i \rangle(k) = \frac{a + Ra^2 - jab}{(1+Ra)^2 + b^2} \langle v \rangle(k) + \langle i_{equiv} \rangle(k-1)$$

with

$$a = \frac{\Delta t}{2L}, \qquad b = \frac{\Delta t \omega}{2}$$
$$\langle i_{equiv} \rangle(k-1) = \frac{1 - b^2 - j2b + 2Ra + (Ra)^2 - j2Rab}{(1+Ra^2) + b^2} \langle i \rangle(k-1) + \frac{a + Ra^2 - jab}{(1+Ra)^2 + b^2} \langle v \rangle(k-1)$$

4.9.2 - Branches

Line models connecting two nodes of the network.

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:

$$\underline{Y}_{shunt} = \frac{G + j \omega C}{2}$$

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,

$$Z_c = \sqrt{\frac{L}{C}}, \qquad \tau = \sqrt{L C}.$$

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.

Transformer

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.

ITM
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,

$$G = \begin{cases} 1 / R_{closed} & \text{closed} \\ 1 / R_{open} & \text{open} \end{cases}$$

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,

$$L = \frac{X}{\omega} \quad (X > 0), \qquad C = -\frac{1}{\omega X} \quad (X < 0).$$

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,

$$\underline{I} = \left( \frac{S}{V_{nom}} \right)^{*},$$

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$,

$$T_r \dot{B} = K_r \, \frac{V_{meas} - V_{ref}}{V_{nom}} - B ,$$

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]

Reduced order machines from third to sixth order are derived under reduced order machine models. How any of these are stamped into the solver is covered under reduced order generator implementation.

Basic Equations

The equations of the stator and rotor voltages are

$$\begin{align} \mathbf{v}_{abcs} &= \mathbf{R}_s \mathbf{i}_{abcs} + \frac{d}{dt} \boldsymbol{\lambda}_{abcs} \\ \mathbf{v}_{dqr} &= \mathbf{R}_r \mathbf{i}_{dqr} + \frac{d}{dt} \boldsymbol{\lambda}_{dqr} \end{align}$$

where

$$\begin{align} \mathbf{v}_{abcs} &= \begin{pmatrix} v_{as} & v_{bs} & v_{cs} \end{pmatrix}^T \\ % \mathbf{v}_{dqr} &= \begin{pmatrix} v_{fd} & v_{kd} & v_{kq1} & v_{kq2} \end{pmatrix}^T \\ % \mathbf{i}_{abcs} &= \begin{pmatrix} i_{as} & i_{bs} & i_{cs} \end{pmatrix}^T \\ % \mathbf{i}_{dqr} &= \begin{pmatrix} i_{fd} & i_{kd} & i_{kq1} & i_{kq2} \end{pmatrix}^T \\ % \boldsymbol{\lambda}_{abcs} &= \begin{pmatrix} \lambda_{as} & \lambda_{bs} & \lambda_{cs} \end{pmatrix}^T \\ % \boldsymbol{\lambda}_{dqr} &= \begin{pmatrix} \lambda_{fd} & \lambda_{kd} & \lambda_{kq1} & \lambda_{kq2} \end{pmatrix}^T \\ % \mathbf{R}_s &= diag \begin{bmatrix} R_s & R_s & R_s \end{bmatrix} \\ % \mathbf{R}_r &= diag \begin{bmatrix} R_{fd} & R_{kd} & R_{kq1} & R_{kq2} \end{bmatrix} \end{align}$$

The flux linkage equations are defined as

$$\begin{equation} \begin{bmatrix} \boldsymbol{\lambda}_{abcs} \\ \boldsymbol{\lambda}_{dqr} \end{bmatrix} = \begin{bmatrix} \mathbf{L}_s & \mathbf{L}_{rs} \\ {(\mathbf{L}_{rs})}^{T} & \mathbf{L}_r \end{bmatrix} \begin{bmatrix} \mathbf{i}_{abcs} \\ \mathbf{i}_{dqr} \end{bmatrix} \end{equation}$$

The inductance matrices are varying with the rotor position $\theta_r$ which varies with time.

The mechanical equations are:

$$\begin{align} \frac{d\theta_r}{dt} &= \omega_r \\ \frac{d\omega_r}{dt} &= \frac{P}{2J} (T_e-T_m) \end{align}$$

$\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:

$$\begin{equation} \begin{bmatrix} \mathbf{v}_{dq0s} \\ \mathbf{v}_{dqr} \end{bmatrix} = \mathbf{R}_{sr} \begin{bmatrix} \mathbf{i}_{dq0s} \\ \mathbf{i}_{dqr} \end{bmatrix} + \frac{d}{dt} \begin{bmatrix} \boldsymbol{\lambda}_{dq0s} \\ \boldsymbol{\lambda}_{dqr} \end{bmatrix} + \omega_r \begin{bmatrix} \boldsymbol{\lambda}_{qds} \\ 0 \end{bmatrix} \end{equation}$$

where

$$\begin{align} \mathbf{v}_{dq0s} &= \begin{pmatrix} v_{ds} & v_{qs} & v_{0s} \end{pmatrix}^T \nonumber \\ % \mathbf{i}_{dq0s} &= \begin{pmatrix} i_{ds} & i_{qs} & i_{0s} \end{pmatrix}^T \nonumber \\ % \boldsymbol{\lambda}_{dq0s} &= \begin{pmatrix} \lambda_{ds} & \lambda_{qs} & \lambda_{0s} \end{pmatrix}^T \nonumber \\ % \mathbf{R}_{sr} &= diag \begin{bmatrix} R_s & R_s & R_s & R_{fd} & R_{kd} & R_{kq1} & R_{kq2} \end{bmatrix} \nonumber \\ % \boldsymbol{\lambda}_{dqs} &= \begin{pmatrix} -\lambda_{qs} & \lambda_{ds} & 0 \end{pmatrix}^T. \end{align}$$

The flux linkages are:

$$\begin{equation} \begin{pmatrix} \boldsymbol{\lambda}_{dq0s} \\ \boldsymbol{\lambda}_{dqr} \end{pmatrix} = \begin{bmatrix} \mathbf{L}_{dqss} & \mathbf{L}_{dqsr} \\ \mathbf{L}_{dqrs} & \mathbf{L}_{dqrr} \end{bmatrix} \begin{pmatrix} \mathbf{i}_{dq0s} \\ \mathbf{i}_{dqr} \end{pmatrix} \end{equation}$$

where

$$\begin{align} \mathbf{L}_{dqss} &= \begin{bmatrix} L_{d} & 0 & 0 \\ 0 & L_{q} & 0 \\ 0 & 0 & L_{ls} \end{bmatrix} \nonumber \\ \mathbf{L}_{dqsr} &= \begin{bmatrix} L_{md} & L_{md} & 0 & 0 \\ 0 & 0 & L_{mq} & L_{mq} \\ 0 & 0 & 0 & 0 \end{bmatrix} \nonumber \\ \mathbf{L}_{dqrs} &= \begin{bmatrix} L_{md} & 0 & 0 \\ L_{md} & 0 & 0 \\ 0 & L_{mq} & 0 \\ 0 & L_{mq} & 0 \end{bmatrix} \nonumber \\ \mathbf{L}_{rr} &= \begin{bmatrix} L_{fd} & L_{md} & 0 & 0 \\ L_{md} & L_{kd} & 0 & 0 \\ 0 & 0 & L_{kq1} & L_{mq} \\ 0 & 0 & L_{mq} & L_{kq2} \end{bmatrix} \nonumber \\ \end{align}$$

with

$$\begin{align} L_{d} &= L_{ls} + L_{md} \nonumber \\ L_{q} &= L_{ls} + L_{mq} \nonumber \\ L_{fd} &= L_{lfd} + L_{md} \nonumber \\ L_{kd} &= L_{lkd} + L_{md} \nonumber \\ L_{kq1} &= L_{lkq1} + L_{mq} \nonumber \\ L_{kq2} &= L_{lkq2} + L_{mq}. \end{align}$$

The mechanical equations in per unit become:

$$\begin{align} T_e &= \lambda_{qs} i_{ds} - \lambda_{ds} i_{qs} \\ \frac{d \omega_r}{dt} &= \omega_r \\ \frac{1}{\omega_b} \frac{d \omega_r}{dt} &= \frac{1}{2H} (T_m - T_e). \end{align}$$

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]:

$$\begin{align} \lambda_{md} &= L_{md} \left( i_{ds} + i_{fd} + i_{kd} \right) \nonumber \\ \lambda_{mq} &= L_{mq} \left( i_{qs} + i_{kq1} + i_{kq2} \right) \end{align}$$

Using the flux linkages results in a simpler equation set for the fluxes:

$$\begin{align} \lambda_{ds} &= L_{ls} i_{ds} + L_{md} \left( i_{ds} + i_{fd} + i_{kd} \right) \nonumber \\ \lambda_{qs} &= L_{ls} i_{qs} + L_{mq} \left( i_{qs} + i_{kq1} + i_{kq2} \right) \nonumber \\ \lambda_{0s} &= L_{ls} i_{0s} \nonumber \\ \lambda_{fd} &= L_{ls} i_{fd} + L_{md} \left( i_{ds} + i_{fd} + i_{kd} \right) \nonumber \\ \lambda_{kd} &= L_{ls} i_{kd} + L_{md} \left( i_{ds} + i_{fd} + i_{kd} \right) \nonumber \\ \lambda_{kq1} &= L_{ls} i_{kq1} + L_{mq} \left( i_{qs} + i_{kq1} + i_{kq2} \right) \nonumber \\ \lambda_{kq2} &= L_{ls} i_{kq2} + L_{mq} \left( i_{qs} + i_{kq1} + i_{kq2} \right) \end{align}$$
$$\begin{align} \lambda_{ds} &= L_{ls} i_{ds} + \lambda_{md} \nonumber \\ \lambda_{qs} &= L_{ls} i_{qs} + \lambda_{mq} \nonumber \\ \lambda_{0s} &= L_{ls} i_{0s} \nonumber \\ \lambda_{fd} &= L_{lfd} i_{fd} + \lambda_{md} \nonumber \\ \lambda_{kd} &= L_{lkd} i_{kd} + \lambda_{md} \nonumber \\ \lambda_{kq1} &= L_{lkq1} i_{kq1} + \lambda_{mq} \nonumber \\ \lambda_{kq2} &= L_{lkq2} i_{kq2} + \lambda_{mq} \end{align}$$

Dynamic Phasor Model

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.

$$\begin{align} \langle i_{ds} \rangle_{0} &= \mathbf{Re} \left\{ \langle i_{p} \rangle_1 \ \mathrm{e}^{-j \delta} \right\} \nonumber \\ \langle i_{qs} \rangle_{0} &= \mathbf{Im} \left\{ \langle i_{p} \rangle_1 \ \mathrm{e}^{-j \delta} \right\} \nonumber \\ \langle i_{ds} \rangle_{2} &= \mathbf{Re} \left\{ \langle i_{n} \rangle_{1}^* \ \mathrm{e}^{-j \delta} \right\} \nonumber \\ \langle i_{qs} \rangle_{2} &= \mathbf{Im} \left\{ \langle i_{n} \rangle_{1}^* \ \mathrm{e}^{-j \delta} \right\} \nonumber \\ \langle i_{0s} \rangle_{1} &= \mathbf{Re} \left\{ \langle i_{z} \rangle_1 \right\} \end{align}$$

The winding currents for positive and zero sequence components can be expressed as

$$\begin{align} \langle i_{ds} \rangle_0 &= \frac{\langle \lambda_{ds} \rangle_0 - \langle \lambda_{md} \rangle_0 }{L_{ls}} \nonumber \\ \langle i_{qs} \rangle_0 &= \frac{\langle \lambda_{qs} \rangle_0 - \langle \lambda_{mq} \rangle_0}{L_{ls}} \nonumber \\ \langle i_{0s} \rangle_1 &= \frac{\langle \lambda_{0s} \rangle_1}{L_{ls}} \nonumber \\ \langle i_{fd} \rangle_0 &= \frac{\langle \lambda_{fd} \rangle_0 - \langle \lambda_{md} \rangle_0}{L_{lfd}} \nonumber \\ \langle i_{kd} \rangle_0 &= \frac{\langle \lambda_{kd} \rangle_0 - \langle \lambda_{md} \rangle_0}{L_{lkd}} \nonumber \\ \langle i_{kq1} \rangle_0 &= \frac{\langle \lambda_{kq1} \rangle_0 - \langle \lambda_{mq} \rangle_0}{L_{lkq1}} \nonumber \\ \langle i_{kq2} \rangle_0 &= \frac{\langle \lambda_{kq2} \rangle_0 - \langle \lambda_{mq} \rangle_0}{L_{lkq2}}. \end{align}$$
$$\begin{align} \frac{d}{dt} \langle \lambda_{ds} \rangle_0 &= \langle v_{ds} \rangle_0 + \langle \omega_r \rangle_0 \langle \lambda_{qs} \rangle_0 + \frac{R_s}{L_{ls}} \left( \langle \lambda_{md} \rangle_0 - \langle \lambda_{ds} \rangle_0 \right) \nonumber \\ \frac{d}{dt} \langle \lambda_{qs} \rangle_0 &= \langle v_{qs} \rangle_0 - \langle \omega_r \rangle_0 \langle \lambda_{ds} \rangle_0 + \frac{R_s}{L_{ls}} \left( \langle \lambda_{mq} \rangle_0 - \langle \lambda_{qs} \rangle_0 \right) \nonumber \\ \frac{d}{dt} \langle \lambda_{0s} \rangle_1 &= \langle v_{0s} \rangle_1 - \frac{R_s}{L_{ls}} \langle \lambda_{0s} \rangle_1 -j \omega_s \langle \lambda_{0s} \rangle_1 \nonumber \\ \frac{d}{dt} \langle \lambda_{fd} \rangle_0 &= \langle v_{fd} \rangle_0 + \frac{R_{fd}}{L_{lfd}} \left( \langle \lambda_{md} \rangle_0 - \langle \lambda_{fd} \rangle_0 \right) \nonumber \\ \frac{d}{dt} \langle \lambda_{kd} \rangle_0 &= \frac{R_{kd}}{L_{lkd}} \left( \langle \lambda_{md} \rangle_0 - \langle \lambda_{kd} \rangle_0 \right) \nonumber \\ \frac{d}{dt} \langle \lambda_{kq1} \rangle_0 &= \frac{R_{kq1}}{L_{lkq1}} \left( \langle \lambda_{mq} \rangle_0 - \langle \lambda_{kq1} \rangle_0 \right) \nonumber \\ \frac{d}{dt} \langle \lambda_{kq2} \rangle_0 &= \frac{R_{kq2}}{L_{lkq2}} \left( \langle \lambda_{mq} \rangle_0 - \langle \lambda_{kq2} \rangle_0 \right). \end{align}$$

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:

$$\begin{align} \langle \lambda_{md} \rangle_0 &= L_{ad} \left( \frac{\langle \lambda_{ds} \rangle_0}{L_{ls}} + \frac{\langle \lambda_{fd} \rangle_0}{L_{lfd}} + \frac{\langle \lambda_{kd} \rangle_0}{L_{lkd}} \right) \nonumber \\ \langle \lambda_{mq} \rangle_0 &= L_{aq} \left( \frac{\langle \lambda_{qs} \rangle_0}{L_{ls}} + \frac{\langle \lambda_{kq1} \rangle_0}{L_{lkq1}} + \frac{\langle \lambda_{kq2} \rangle_0}{L_{lkq2}} \right) \end{align}$$

where

$$\begin{align} L_{ad} &= \left( \frac{1}{L_{md}} + \frac{1}{L_{ls}} + \frac{1}{L_{lfd}} + \frac{1}{L_{lkd}} \right) \nonumber \\ L_{aq} &= \left( \frac{1}{L_{mq}} + \frac{1}{L_{ls}} + \frac{1}{L_{lkq1}} + \frac{1}{L_{lkq2}} \right). \end{align}$$

The mechanical equations in dynamic phasors are:

$$\begin{align} T_e &= \langle \lambda_{qs} \rangle_0 \langle i_{ds} \rangle_0 - \langle \lambda_{ds} \rangle_0 \langle i_{qs} \rangle_0 \\ \frac{1}{\omega_s} \frac{d \delta_r}{dt} &= \omega_r - 1 \\ \frac{d \omega_r}{dt} &= \frac{1}{2H} (T_m - T_e). \end{align}$$

Transient Stability Model

4.9.9.1 - Reduced Order Machine Models

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

$$V_{base} = \frac{\sqrt{2}}{\sqrt{3}} V_n, \qquad I_{base} = \frac{S_n}{\frac{3}{2} V_{base}}, \qquad Z_{base} = \frac{V_n}{S_n / V_n}.$$

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

$$E_d' = V_d - L_q' I_q, \qquad E_q' = V_q + L_d' I_d,$$

and analogously for the subtransient pair with $L_d’’$ and $L_q’’$.

OrderElectrical statesPhysical 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,

$$E_{h,d} = A_d' \, I_q + B_d' \, E_d', \qquad E_{h,q} = A_q' \, I_d + B_q' \, E_q' + D_q' \left( E_{f}[k-1] + E_{f}[k] \right),$$

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:

$$A_d' = \frac{\Delta t \, Z_d'}{2 T_{q0}' + \Delta t}, \qquad B_d' = \frac{2 T_{q0}' - \Delta t}{2 T_{q0}' + \Delta t},$$
$$A_q' = \frac{-\Delta t \, Z_q'}{2 T_{d0}' + \Delta t}, \qquad B_q' = \frac{2 T_{d0}' - \Delta t}{2 T_{d0}' + \Delta t}, \qquad D_q' = \frac{\Delta t \left( 1 - T_f \right)}{2 T_{d0}' + \Delta t}.$$

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,

$$Y_d = \frac{T_{d0}''}{T_{d0}'} \frac{L_d''}{L_d'} \left( L_d - L_d' \right), \qquad Y_q = \frac{T_{q0}''}{T_{q0}'} \frac{L_q''}{L_q'} \left( L_q - L_q' \right), \qquad T_f = \frac{T_{aa}}{T_{d0}'}.$$

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.

Mechanical equations

The rotor obeys the swing equation in per unit,

$$2H \frac{d \omega_r}{dt} = T_m - T_e, \qquad \frac{d \theta}{dt} = \omega_{base} \, \omega_r,$$

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.
  • [5] M. Eremia; M. Shahidehpour, “Handbook of Electrical Power System Dynamics: Modeling, Stability, and Control”, https://ieeexplore.ieee.org/book/6480471
  • [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

DC1_exciter
Fig. 1: Control diagram of the IEEE Type DC1 exciter
Adapted 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_{R} \frac{d}{dt} v_{R}(t) = v_{h}(t) - v_{R}(t) $$

$$ 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)

DC1A_exciter

Fig. 2: Control diagram of the IEEE Type DC1 exciter
Adapted 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

Exciter static
Fig. 3: Control diagram of the Static Exciter
Adapted 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.
Exciter static split
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:

$$ 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), $$ $$ X_b(k + \Delta t) = \frac{\Delta t}{T_{b}} (v_{in}(k) - x_{b}(k)) + x_{b}(k), $$ $$ v_e(k) = K_{a} (C_{b} x_{b}(k) + C_{a} v_{in} (k)) , $$ $$ e_{fd}(k + \Delta t) = \frac{\Delta t}{T_{e}}(v_{e}(k) - e_{fd}(k)) + e_{fd}(k). $$

To consider the saturation of $e_{fd}$ there are two different implementations, which is automatically selected depending of value of the parameter $K_{bc}$:

Standard ($K_{bc}=0$):

$$ e^{}{fd} = e{fd, max} \quad \quad if \quad \quad e^{}{fd} > e{fd, max} \ e^{}{fd} = e{fd, min} \quad \quad if \quad \quad e^{}{fd} < e{fd, min}, $$

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.

Exciter static split
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:

$$ v_{e} = C_{a} v_{in} + C_{b} x_{b} - K_{bc} (e_{fd} - e_{fd}^{*}) $$

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}(t=0) = v_{h}(t=0), $$

$$ v_{e}(t=0) = \frac{e_{fd}(t=0)}{K_{a}}, $$

$$ v_{in}(t=0) = \frac{v_{e}(t=0)}{C_{a}+C_{b}}, $$

$$ x_{b}(t=0) = v_{in}(t=0), $$

$$ v_{ref}(t=0) = v_{in}(t=0) + v_{r}(t=0) $$

Power System Stabilizer (PSS)

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

DC1_exciter
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

TG_Type1_governor

Fig. 7: Control diagram of the turbine governor type 1
Source: 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

Steam Governor

Fig. 8: Control diagram of the steam turbine governor
Adapted 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.

Steam Governor split

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.

Steam Governor anti-windup

Fig. 10: Control diagram of the steam turbine governor with anti-windup strategy

The forward-Euler discretised equations are: $$ \Delta \omega (k-\Delta t) = \omega_{ref} - \omega (k-\Delta t), $$ $$ p_{1}(k) = p_{1}(k-\Delta t) + \frac{\Delta t}{T_{1}} \left(\Delta \omega (k-\Delta t) \cdot \frac{T_{1} - T_{2}}{T_{1}} - p_{1}(k-\Delta t)\right), $$ $$ p(k-\Delta t) = \frac{1}{R} \left(p_{1}(k-\Delta t) + \Delta \omega(k-\Delta t) \cdot \frac{T_{2}}{T_{1}}\right), $$ $$ \dot{p}(k-\Delta t) = \frac{1}{T_{3}}\left(p(k-\Delta t) + p_{ref} - p_{gv}(k-\Delta t)\right) - K_{bc} \left(p_{gv}^{}(k-\Delta t) - p_{gv}(k-\Delta t)\right), $$ $$ p_{gv}^{}(k) = p_{gv}^{*}(k-\Delta t) + \Delta t \cdot \dot{p}(k-\Delta t), $$

and

$$ p_{gv}(k) = p_{gv}^{}(k) \quad \text{if} \quad P_{m,\min} \leq p_{gv}^{}(k) \leq P_{m,\max}, \ p_{gv}(k) = P_{m,\max} \quad \text{if} \quad p_{gv}^{}(k) > P_{m,\max}, \ p_{gv}(k) = P_{m,\min} \quad \text{if} \quad p_{gv}^{}(k) < P_{m,\min}. $$

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

Steam Turbine

Fig. 11: Control diagram of the steam turbine
Adapted 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:

$$ p_{hp}(k) = p_{hp}(k-\Delta t) + \frac{\Delta t}{T_{CH}} \left(p_{gv}(k-\Delta t) - p_{hp}(k-\Delta t)\right), $$ $$ p_{ip}(k) = p_{ip}(k-\Delta t) + \frac{\Delta t}{T_{RH}} \left(p_{hp}(k-\Delta t) - p_{ip}(k-\Delta t)\right), $$ $$ p_{lp}(k) = p_{lp}(k-\Delta t) + \frac{\Delta t}{T_{CO}} \left(p_{ip}(k-\Delta t) - p_{lp}(k-\Delta t)\right), $$ $$ p_{m}(k) = F_{HP} \cdot p_{hp}(k) + F_{IP} \cdot p_{ip}(k) + F_{LP} \cdot p_{lp}(k). $$

Assuming the simulation starts in steady state (all derivatives zero), the initial values are: $$ p_{hp}(t=0) = p_{gv}(t=0), \quad p_{ip}(t=0) = p_{hp}(t=0), \quad p_{lp}(t=0) = p_{ip}(t=0), $$ $$ p_{m}(t=0) = F_{HP} \cdot p_{hp}(t=0) + F_{IP} \cdot p_{ip}(t=0) + F_{LP} \cdot p_{lp}(t=0). $$

Hydro Turbine Governor

Hydro Governor

Fig. 12: Control diagram of a hydro turbine governor
Adapted 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.

Hydro Governor split

Fig. 13: Control diagram of a hydro turbine governor after partial-fraction decomposition

The forward-Euler discretised equations are: $$ x_{1}(k) = x_{1}(k-\Delta t) + \frac{\Delta t}{T_{1}} \left(\Delta\omega(k-\Delta t) - x_{1}(k-\Delta t)\right), $$ $$ x_{2}(k) = x_{2}(k-\Delta t) + \frac{\Delta t}{T_{3}} \left(\Delta\omega(k-\Delta t) - x_{2}(k-\Delta t)\right), $$ $$ p^{*}{gv}(k) = \frac{1}{R}\left(A \cdot x{1}(k) + B \cdot x_{2}(k)\right) + p_{ref}, $$

where $$ A = \frac{T_{1}-T_{2}}{T_{1}-T_{3}}, \qquad B = \frac{T_{2}-T_{3}}{T_{1}-T_{3}}, $$

and the output limiter is applied as: $$ p_{gv}(k) = \begin{cases} P_{m,\max} & \text{if } p^{}{gv}(k) > P{m,\max}, \ P_{m,\min} & \text{if } p^{}{gv}(k) < P{m,\min}, \ p^{*}_{gv}(k) & \text{otherwise.} \end{cases} $$

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

Hydro Turbine

Fig. 14: Control diagram of a hydro turbine
Adapted 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.

Hydro Turbine split

Fig. 15: Control diagram of a hydro turbine after decomposition
Adapted from [6]

The forward-Euler discretised equations are: $$ x_{1}(k) = x_{1}(k-\Delta t) + \frac{\Delta t}{0.5,T_{W}} \left(p_{gv}(k-\Delta t) - x_{1}(k-\Delta t)\right), $$ $$ p_{m}(k) = 3,x_{1}(k) - 2,p_{gv}(k). $$

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.

The terminal input is the PCC voltage vector

$$\mathbf{u} = \begin{bmatrix} u_a & u_b & u_c \end{bmatrix}^\top ,$$

and the state vector is

$$\mathbf{x} = \begin{bmatrix} \theta_{\mathrm{PLL}} & \phi_{\mathrm{PLL}} & P & Q & \phi_d & \phi_q & \gamma_d & \gamma_q & v_{c,a} & v_{c,b} & v_{c,c} & i_{f,a} & i_{f,b} & i_{f,c} \end{bmatrix}^\top .$$

The model output is the interface current injected into the MNA system,

$$\mathbf{y} = \frac{\mathbf{u} - \mathbf{v}_c}{R_c}.$$

Model equations

The controller uses the opposite current direction, i.e. positive current denotes inverter injection into the grid,

$$\mathbf{i}_{rc} = \frac{\mathbf{v}_c - \mathbf{u}}{R_c}.$$

The Park transformation with PLL angle $\theta_{\mathrm{PLL}}$ is used to obtain dq quantities,

$$\begin{bmatrix} v_{c,d} \\ v_{c,q} \end{bmatrix} = \mathbf{T}(\theta_{\mathrm{PLL}})\mathbf{v}_c, \qquad \begin{bmatrix} i_{rc,d} \\ i_{rc,q} \end{bmatrix} = \mathbf{T}(\theta_{\mathrm{PLL}})\mathbf{i}_{rc}.$$

The instantaneous active and reactive powers are calculated as

$$p = v_{c,d} i_{rc,d} + v_{c,q} i_{rc,q},$$
$$q = -v_{c,d} i_{rc,q} + v_{c,q} i_{rc,d}.$$

The PLL and power-filter dynamics are

$$\dot{\theta}_{\mathrm{PLL}} = \omega_n + K_{p,\mathrm{PLL}} v_{c,q} + K_{i,\mathrm{PLL}} \phi_{\mathrm{PLL}},$$
$$\dot{\phi}_{\mathrm{PLL}} = v_{c,q},$$
$$\dot{P} = \omega_c(p - P), \qquad \dot{Q} = \omega_c(q - Q).$$

The outer power-control integrators and current references are

$$\dot{\phi}_d = P_{\mathrm{ref}} - P, \qquad \dot{\phi}_q = Q - Q_{\mathrm{ref}},$$
$$i_{d,\mathrm{ref}} = K_{p,P}(P_{\mathrm{ref}} - P) + K_{i,P}\phi_d,$$
$$i_{q,\mathrm{ref}} = K_{p,P}(Q - Q_{\mathrm{ref}}) + K_{i,P}\phi_q.$$

The inner current-control integrators and voltage references are

$$\dot{\gamma}_d = i_{d,\mathrm{ref}} - i_{rc,d}, \qquad \dot{\gamma}_q = i_{q,\mathrm{ref}} - i_{rc,q},$$
$$v_{d,\mathrm{ref}} = K_{p,I}(i_{d,\mathrm{ref}} - i_{rc,d}) + K_{i,I}\gamma_d,$$
$$v_{q,\mathrm{ref}} = K_{p,I}(i_{q,\mathrm{ref}} - i_{rc,q}) + K_{i,I}\gamma_q.$$

The reference voltage is transformed back to abc coordinates,

$$\mathbf{v}_{\mathrm{ref}} = \mathbf{T}^{-1}(\theta_{\mathrm{PLL}}) \begin{bmatrix} v_{d,\mathrm{ref}} \\ v_{q,\mathrm{ref}} \end{bmatrix}.$$

The LC filter dynamics are

$$\dot{\mathbf{v}}_c = \frac{1}{C_f}\mathbf{i}_f + \frac{1}{C_f R_c}(\mathbf{u} - \mathbf{v}_c),$$
$$\dot{\mathbf{i}}_f = \frac{1}{L_f} \left( \mathbf{v}_{\mathrm{ref}} - \mathbf{v}_c - R_f \mathbf{i}_f \right).$$

At each simulation step, the nonlinear model is locally linearized into the affine state-space form

$$\dot{\mathbf{x}} \approx \mathbf{A}\mathbf{x} + \mathbf{B}\mathbf{u} + \mathbf{E},$$
$$\mathbf{y} \approx \mathbf{C}\mathbf{x} + \mathbf{D}\mathbf{u} + \mathbf{F},$$

which is then discretized and stamped into the EMT MNA system.

How this is arranged in code, together with the source and the runnable examples, is covered under EMT Ph3 averaged VSI implementation.

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

$$\mathbf{x} = \begin{bmatrix} \psi & \phi_{\mathrm{PLL}} & P & Q & \phi_d & \phi_q & \gamma_d & \gamma_q & \operatorname{Re}\{V_c\} & \operatorname{Im}\{V_c\} & \operatorname{Re}\{I_f\} & \operatorname{Im}\{I_f\} \end{bmatrix}^\top ,$$

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}}$,

$$V_{c,dq} = V_c\, e^{-j\psi}, \qquad I_{rc,dq} = I_{rc}\, e^{-j\psi},$$

with $v_{c,d} = \operatorname{Re}{V_{c,dq}}$, $v_{c,q} = \operatorname{Im}{V_{c,dq}}$, and likewise for $i_{rc,d}$, $i_{rc,q}$.

The instantaneous active and reactive powers are calculated as

$$p = v_{c,d} i_{rc,d} + v_{c,q} i_{rc,q},$$
$$q = -v_{c,d} i_{rc,q} + v_{c,q} i_{rc,d},$$

identical in form to EMT’s; DP::Ph1’s own voltage/current scale already represents total power directly, with no three-phase multiplier.

The PLL and power-filter dynamics are

$$\dot{\psi} = K_{p,\mathrm{PLL}} v_{c,q} + K_{i,\mathrm{PLL}} \phi_{\mathrm{PLL}},$$
$$\dot{\phi}_{\mathrm{PLL}} = v_{c,q},$$
$$\dot{P} = \omega_c(p - P), \qquad \dot{Q} = \omega_c(q - Q).$$

The outer power-control integrators and current references are

$$\dot{\phi}_d = P_{\mathrm{ref}} - P, \qquad \dot{\phi}_q = Q - Q_{\mathrm{ref}},$$
$$i_{d,\mathrm{ref}} = K_{p,P}(P_{\mathrm{ref}} - P) + K_{i,P}\phi_d,$$
$$i_{q,\mathrm{ref}} = K_{p,P}(Q - Q_{\mathrm{ref}}) + K_{i,P}\phi_q.$$

The inner current-control integrators and voltage references are

$$\dot{\gamma}_d = i_{d,\mathrm{ref}} - i_{rc,d}, \qquad \dot{\gamma}_q = i_{q,\mathrm{ref}} - i_{rc,q},$$
$$v_{d,\mathrm{ref}} = K_{p,I}(i_{d,\mathrm{ref}} - i_{rc,d}) + K_{i,I}\gamma_d,$$
$$v_{q,\mathrm{ref}} = K_{p,I}(i_{q,\mathrm{ref}} - i_{rc,q}) + K_{i,I}\gamma_q.$$

The reference voltage is transformed back to a complex envelope, rotating by $\psi$,

$$V_{\mathrm{ref}} = (v_{d,\mathrm{ref}} + j v_{q,\mathrm{ref}})\, e^{j\psi}.$$

The LC filter dynamics carry the envelope’s carrier shift explicitly,

$$\dot{V}_c = \frac{1}{C_f} I_f + \frac{1}{C_f R_c}(U - V_c) - j\omega_n V_c,$$
$$\dot{I}_f = \frac{1}{L_f} \left( V_{\mathrm{ref}} - V_c - R_f I_f \right) - j\omega_n I_f.$$

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,

$$\dot{\mathbf{x}} \approx \mathbf{A}\mathbf{x} + \mathbf{B}\mathbf{u} + \mathbf{E},$$
$$\mathbf{y} \approx \mathbf{C}\mathbf{x} + \mathbf{D}\mathbf{u} + \mathbf{F},$$

which is then discretized and stamped into the network equations.

How this is arranged in code, together with the source and the runnable examples, is covered under DP Ph1 averaged VSI implementation.

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,

$$u = \begin{bmatrix} U_a & U_b & U_c \end{bmatrix}^\top ,$$

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,

$$\mathbf{x} = \big[\, V_{c,a} \;\; V_{c,b} \;\; V_{c,c} \;\; I_{f,a} \;\; I_{f,b} \;\; I_{f,c} \;\; \psi \;\; \phi_{\mathrm{PLL}} \;\; P \;\; Q \;\; \phi_d \;\; \phi_q \;\; \gamma_d \;\; \gamma_q \,\big]^\top ,$$

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,

$$y_p = \frac{U_p - V_{c,p}}{R_c}, \qquad p \in \{a, b, c\}.$$

Model equations

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,

$$\underline{V}_c = V_{c,a} + a\, V_{c,b} + a^2 V_{c,c}, \qquad a = e^{\,j 2\pi/3},$$

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$,

$$V_{c,dq} = \tfrac{1}{2}\sqrt{\tfrac{2}{3}}\, e^{-j\psi}\, \underline{V}_c, \qquad I_{rc,dq} = \tfrac{1}{2}\sqrt{\tfrac{2}{3}}\, e^{-j\psi}\, \underline{I}_{rc},$$

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

$$p = v_{c,d} i_{rc,d} + v_{c,q} i_{rc,q}, \qquad q = -v_{c,d} i_{rc,q} + v_{c,q} i_{rc,d},$$

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

$$\dot{\psi} = K_{p,\mathrm{PLL}} v_{c,q} + K_{i,\mathrm{PLL}} \phi_{\mathrm{PLL}}, \qquad \dot{\phi}_{\mathrm{PLL}} = v_{c,q},$$
$$\dot{P} = \omega_c(p - P), \qquad \dot{Q} = \omega_c(q - Q).$$

The outer power-control integrators and current references are

$$\dot{\phi}_d = P_{\mathrm{ref}} - P, \qquad \dot{\phi}_q = Q - Q_{\mathrm{ref}},$$
$$i_{d,\mathrm{ref}} = K_{p,P}(P_{\mathrm{ref}} - P) + K_{i,P}\phi_d, \qquad i_{q,\mathrm{ref}} = K_{p,P}(Q - Q_{\mathrm{ref}}) + K_{i,P}\phi_q,$$

and the inner current-control integrators and voltage references are

$$\dot{\gamma}_d = i_{d,\mathrm{ref}} - i_{rc,d}, \qquad \dot{\gamma}_q = i_{q,\mathrm{ref}} - i_{rc,q},$$
$$v_{d,\mathrm{ref}} = K_{p,I}(i_{d,\mathrm{ref}} - i_{rc,d}) + K_{i,I}\gamma_d, \qquad v_{q,\mathrm{ref}} = K_{p,I}(i_{q,\mathrm{ref}} - i_{rc,q}) + K_{i,I}\gamma_q.$$

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,

$$V_{\mathrm{ref},p} = \bar{a}_p \sqrt{\tfrac{2}{3}}\, V_{\mathrm{ref},dq}\, e^{j\psi}, \qquad \bar{a}_{a/b/c} = \{1,\; a^2,\; a\},$$

so that all three phases are driven by the same positive-sequence command.

The LC-filter dynamics are decoupled per phase within the plant and carry the carrier shift of the envelope explicitly,

$$\dot{V}_{c,p} = \frac{1}{C_f} I_{f,p} + \frac{1}{C_f R_c}(U_p - V_{c,p}) - j\omega_n V_{c,p},$$
$$\dot{I}_{f,p} = \frac{1}{L_f} \left( V_{\mathrm{ref},p} - V_{c,p} - R_f I_{f,p} \right) - j\omega_n I_{f,p},$$

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,

$$\dot{\mathbf{x}} \approx \mathbf{A}\mathbf{x} + \mathbf{B}\mathbf{u} + \mathbf{E}, \qquad \mathbf{y} \approx \mathbf{C}\mathbf{x} + \mathbf{D}\mathbf{u} + \mathbf{F},$$

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,

$$\underline{V}_c^- = V_{c,a} + a^2 V_{c,b} + a\, V_{c,c}, \qquad \underline{I}_{rc}^- = \frac{\underline{V}_c^- - \underline{U}^-}{R_c}.$$

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$,

$$I_{rc,dq}^- = \tfrac{1}{2}\sqrt{\tfrac{2}{3}}\, e^{\,j\psi}\, \overline{\underline{I}_{rc}^-} .$$

The loop itself is the same PI structure as the positive-sequence inner loop,

$$\dot{\gamma}_{nd} = i_{nd,\mathrm{ref}} - i_{rc,nd}, \qquad \dot{\gamma}_{nq} = i_{nq,\mathrm{ref}} - i_{rc,nq},$$
$$v_{nd,\mathrm{ref}} = K_{p,I}(i_{nd,\mathrm{ref}} - i_{rc,nd}) + K_{i,I}\gamma_{nd}, \qquad v_{nq,\mathrm{ref}} = K_{p,I}(i_{nq,\mathrm{ref}} - i_{rc,nq}) + K_{i,I}\gamma_{nq},$$

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,

$$V_{\mathrm{ref},p} = \bar{a}_p \sqrt{\tfrac{2}{3}}\, V_{\mathrm{ref},dq}\, e^{j\psi} + a_p \sqrt{\tfrac{2}{3}}\, \overline{V_{\mathrm{ref},dq}^-}\, e^{j\psi}, \qquad a_{a/b/c} = \{1,\; a,\; a^2\}.$$

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

How this is arranged in code, together with the source and the runnable examples, is covered under DP Ph3 averaged VSI implementation.

4.9.11.4 - EMT Ph3 Grid-Forming Inverter

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.

The terminal input is the PCC voltage vector

$$\mathbf{u} = \begin{bmatrix} u_a & u_b & u_c \end{bmatrix}^\top ,$$

and the 17-element state vector is

$$\mathbf{x} = \begin{bmatrix} P & Q & \omega & \theta & E & \xi_{v,d} & \xi_{v,q} & \xi_{i,d} & \xi_{i,q} & v_{\mathrm{del},d} & v_{\mathrm{del},q} & v_{c,a} & v_{c,b} & v_{c,c} & i_{f,a} & i_{f,b} & i_{f,c} \end{bmatrix}^\top ,$$

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,

$$\mathbf{y} = \frac{\mathbf{u} - \mathbf{v}_c}{R_c}.$$

Control structure

graph LR U["PCC voltage u"] --> FILT["LC filter
vc, if"] FILT --> MEAS["Power measurement
p, q"] MEAS --> PF["Measurement filters
P, Q"] PF -->|P| SWING["VSG swing
omega, theta"] PF -->|Q| EXC["Excitation /
Q-V droop -> E"] EXC --> VZ["Virtual impedance
E - Zv*if"] VZ --> VCTRL["Voltage controller
-> i_ref"] VCTRL --> ICTRL["Current controller
-> v_conv"] ICTRL --> DELAY["Converter delay"] DELAY --> FILT SWING -->|theta| VCTRL FILT --> Y["Interface current y"]

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

$$\mathbf{i}_g = \frac{\mathbf{v}_c - \mathbf{u}}{R_c}.$$

All dq quantities use the VSG angle $\theta$ (amplitude-invariant Park transform $\mathbf{T}(\theta)$),

$$\mathbf{v}_{c,dq} = \mathbf{T}(\theta)\mathbf{v}_c, \qquad \mathbf{i}_{f,dq} = \mathbf{T}(\theta)\mathbf{i}_f, \qquad \mathbf{i}_{g,dq} = \mathbf{T}(\theta)\mathbf{i}_g,$$

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$,

$$p = \tfrac{3}{2}\,(v_{c,d} i_{g,d} + v_{c,q} i_{g,q}), \qquad q = \tfrac{3}{2}\,(v_{c,q} i_{g,d} - v_{c,d} i_{g,q}),$$

and the PCC voltage magnitude is $U_{\mathrm{pcc}} = \sqrt{v_{c,d}^2 + v_{c,q}^2}$.

The measurement filters are first-order lags,

$$\dot{P} = \omega_c(p - P), \qquad \dot{Q} = \omega_c(q - Q).$$

The VSG swing equation sets the angle from the active-power balance,

$$J\dot{\omega} = \frac{P_{\mathrm{ref}} - P}{\omega} - D(\omega - \omega_n), \qquad \dot{\theta} = \omega,$$

with virtual inertia $J$ and damping $D$. The reactive-power/voltage excitation controller sets the internal magnitude,

$$\dot{E} = K_q(Q_{\mathrm{ref}} - Q) + K_u(U_n - U_{\mathrm{pcc}}),$$

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,

$$\dot{\xi}_{v,d} = v_{d,\mathrm{ref}} - v_{c,d}, \qquad \dot{\xi}_{v,q} = v_{q,\mathrm{ref}} - v_{c,q},$$
$$i_{d,\mathrm{ref}} = i_{g,d} - \omega C_f v_{c,q} + K_{p,V}(v_{d,\mathrm{ref}} - v_{c,d}) + K_{i,V}\xi_{v,d},$$
$$i_{q,\mathrm{ref}} = i_{g,q} + \omega C_f v_{c,d} + K_{p,V}(v_{q,\mathrm{ref}} - v_{c,q}) + K_{i,V}\xi_{v,q}.$$

The current controller integrates the current error and forms the converter voltage reference, with inductor decoupling and optional active damping on the capacitor current,

$$\dot{\xi}_{i,d} = i_{d,\mathrm{ref}} - i_{f,d}, \qquad \dot{\xi}_{i,q} = i_{q,\mathrm{ref}} - i_{f,q},$$
$$v_{d,\mathrm{conv}} = v_{c,d} - \omega L_f i_{f,q} + K_{p,I}(i_{d,\mathrm{ref}} - i_{f,d}) + K_{i,I}\xi_{i,d} - K_{ad} i_{\mathrm{cap},d},$$
$$v_{q,\mathrm{conv}} = v_{c,q} + \omega L_f i_{f,d} + K_{p,I}(i_{q,\mathrm{ref}} - i_{f,q}) + K_{i,I}\xi_{i,q} - K_{ad} i_{\mathrm{cap},q}.$$

A first-order lag approximates the converter/digital delay,

$$\dot{v}_{\mathrm{del},d} = \omega_d(v_{d,\mathrm{conv}} - v_{\mathrm{del},d}), \qquad \dot{v}_{\mathrm{del},q} = \omega_d(v_{q,\mathrm{conv}} - v_{\mathrm{del},q}),$$

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,

$$\dot{\mathbf{v}}_c = \frac{1}{C_f}\left(\mathbf{i}_f + \frac{\mathbf{u} - \mathbf{v}_c}{R_c}\right), \qquad \dot{\mathbf{i}}_f = \frac{1}{L_f}\left(\mathbf{v}_{\mathrm{inv}} - \mathbf{v}_c - R_f\mathbf{i}_f\right).$$

Grid-connected control extensions

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}$,

$$v_{d,\mathrm{ref}} + j v_{q,\mathrm{ref}} = E - Z_v\,(i_{f,d} + j i_{f,q}),$$

i.e.

$$v_{d,\mathrm{ref}} = E - (R_v i_{f,d} - X_v i_{f,q}), \qquad v_{q,\mathrm{ref}} = -(R_v i_{f,q} + X_v i_{f,d}).$$

$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,

$$i_{d,\mathrm{ref}} = \kappa\, i_{g,d} - \omega C_f v_{c,q} + \dots, \qquad i_{q,\mathrm{ref}} = \kappa\, i_{g,q} + \omega C_f v_{c,d} + \dots,$$

with $\kappa = 1$ the default full feed-forward.

Proportional reactive-power droop. When a cutoff $\omega_q > 0$ is set, the integral excitation is replaced by a proportional Q-V droop,

$$\dot{E} = \omega_q\big(E_{\mathrm{set}} + D_q(Q_{\mathrm{ref}} - Q) - E\big),$$

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}$,

$$\mathbf{A} = \frac{\partial \mathbf{f}}{\partial \mathbf{x}},\quad \mathbf{B} = \frac{\partial \mathbf{f}}{\partial \mathbf{u}},\quad \mathbf{C} = \frac{\partial \mathbf{g}}{\partial \mathbf{x}},\quad \mathbf{D} = \frac{\partial \mathbf{g}}{\partial \mathbf{u}},$$

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,

$$\mathbf{E} = \mathbf{f}(\mathbf{x}_0,\mathbf{u}_0) - \mathbf{A}\mathbf{x}_0 - \mathbf{B}\mathbf{u}_0, \qquad \mathbf{F} = \mathbf{g}(\mathbf{x}_0,\mathbf{u}_0) - \mathbf{C}\mathbf{x}_0 - \mathbf{D}\mathbf{u}_0,$$

giving the affine state-space form

$$\dot{\mathbf{x}} \approx \mathbf{A}\mathbf{x} + \mathbf{B}\mathbf{u} + \mathbf{E}, \qquad \mathbf{y} \approx \mathbf{C}\mathbf{x} + \mathbf{D}\mathbf{u} + \mathbf{F},$$

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,

$$\dot{\boldsymbol{x}} = \boldsymbol{A}\boldsymbol{x} + \boldsymbol{B}\boldsymbol{u}, \qquad \boldsymbol{y} = \boldsymbol{C}\boldsymbol{x} + \boldsymbol{D}\boldsymbol{u}.$$

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

$$\boldsymbol{A}_d = \left( \boldsymbol{I} - \tfrac{\Delta t}{2}\boldsymbol{A} \right)^{-1} \left( \boldsymbol{I} + \tfrac{\Delta t}{2}\boldsymbol{A} \right), \qquad \boldsymbol{B}_d = \left( \boldsymbol{I} - \tfrac{\Delta t}{2}\boldsymbol{A} \right)^{-1} \tfrac{\Delta t}{2} \boldsymbol{B}.$$

Substituting the discrete state into the output equation separates the output into a part that depends on the present input and a part that does not,

$$\boldsymbol{y} = \boldsymbol{W}\boldsymbol{u} + \boldsymbol{y}_{hist}, \qquad \boldsymbol{W} = \boldsymbol{C}\boldsymbol{B}_d + \boldsymbol{D}, \qquad \boldsymbol{y}_{hist} = \boldsymbol{C}\left( \boldsymbol{A}_d \boldsymbol{x} + \boldsymbol{B}_d \boldsymbol{u} \right).$$

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

$$\boldsymbol{A} = \boldsymbol{0}, \quad \boldsymbol{B} = \boldsymbol{L}^{-1}, \quad \boldsymbol{C} = \boldsymbol{I}, \quad \boldsymbol{D} = \boldsymbol{0}.$$

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,

$$\boldsymbol{x} = \left( j\omega \boldsymbol{I} - \boldsymbol{A} \right)^{-1} \boldsymbol{B} \boldsymbol{u},$$

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,

$$x[k] = x[k-1] + \frac{\Delta t}{2} \left( u[k] + u[k-1] \right),$$

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.

5 - Reference

Generated API documentation and lookup tables.

The Sphinx documentation describes the Python API.

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

ModelSP::Ph1SP::Ph3DP::Ph1DP::Ph3EMT::Ph1EMT::Ph3
Resistor
Inductor
Capacitor
VoltageSource
CurrentSource
VoltageSourceNorton
VoltageSourceRamp
ProfileVoltageSource
ControlledVoltageSource
ControlledCurrentSource
NetworkInjection

Branches

ModelSP::Ph1SP::Ph3DP::Ph1DP::Ph3EMT::Ph1EMT::Ph3
PiLine
RxLine
RXLine
SeriesResistor
ResIndSeries
Transformer
SolidStateTransformer

Switches and loads

ModelSP::Ph1SP::Ph3DP::Ph1DP::Ph3EMT::Ph1EMT::Ph3
Switch
SeriesSwitch
varResSwitch
RXLoad
RXLoadSwitch
PQLoadCS
Load
Shunt
SVC

Synchronous generators

ModelSP::Ph1SP::Ph3DP::Ph1DP::Ph3EMT::Ph1EMT::Ph3
SynchronGenerator
SynchronGeneratorDQ
SynchronGeneratorDQODE
SynchronGeneratorDQTrapez
SynchronGeneratorVBR
SynchronGenerator3OrderVBR
SynchronGenerator4OrderVBR
SynchronGenerator5OrderVBR
SynchronGenerator6aOrderVBR
SynchronGenerator6bOrderVBR
SynchronGenerator4OrderPCM
SynchronGenerator6OrderPCM
SynchronGenerator4OrderTPM
SynchronGeneratorIdeal
SynchronGeneratorIter
SynchronGeneratorTrStab

Power electronics

ModelSP::Ph1SP::Ph3DP::Ph1DP::Ph3EMT::Ph1EMT::Ph3
AvVoltageSourceInverterDQ
AvVoltSourceInverterStateSpace
Inverter
VoltageSourceInverter
VSIVoltageControlVCO
SSN_GFM

State-space nodal components

ModelSP::Ph1SP::Ph3DP::Ph1DP::Ph3EMT::Ph1EMT::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.

ModelSP::Ph1SP::Ph3DP::Ph1DP::Ph3EMT::Ph1EMT::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.

ModelDescription
ExciterDC1Standard IEEE type DC1 exciter
ExciterDC1SimpSimplified version of the IEEE type DC1 exciter
ExciterST1SimpSimplified static exciter
ExciterStaticStatic exciter, with an anti-windup strategy for the integral component

Power system stabiliser

ModelDescription
PSS1ASimplified 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.

ModelDescription
SteamTurbineSteam turbine, used in series with its governor
SteamTurbineGovernorGovernor for the steam turbine, instantiated separately from it
HydroTurbineHydro turbine, used in series with its governor
HydroTurbineGovernorGovernor for the hydro turbine, instantiated separately from it
TurbineGovernorType1Turbine and governor combined in one component
TurbineGovernorTurbine 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.

ModelDescription
PowerControllerVSIPower control loop used by the averaged grid-following inverter models
VoltageControllerVSIVoltage control loop used by the grid-forming inverter models
PLLPhase-locked loop
VCOVoltage-controlled oscillator

Signal generators

Drive sources and setpoints from a prescribed waveform rather than a constant.

ModelDescription
SignalGeneratorBase class for the generators below
SineWaveGeneratorSine wave
CosineFMGeneratorFrequency-modulated cosine
FrequencyRampGeneratorFrequency ramp
DCGeneratorConstant value

Filters and maths

ModelDescription
FIRFilterFinite impulse response filter
IntegratorIntegrator 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.

ModelDomains
DecouplingLineDP::Ph1
DecouplingLineEMTEMT::Ph1
DecouplingLineEMT_Ph3EMT::Ph3
DecouplingIdealTransformerDP::Ph1, EMT::Ph1, EMT::Ph3, SP::Ph1

References

5.3 - State-Space Extraction Support

Components that contribute states to the extracted state-space model.

For how extraction works and how to enable it, see state-space extraction.

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

  1. Fork the repository and clone your fork.

  2. Run pre-commit install to activate the automated checks (formatting, notebook output stripping).

  3. Create a branch with a descriptive prefix (feature/, fix/, docs/).

  4. Commit with a sign-off, using the conventional commit style:

    git commit -s -m "fix: correct node voltage initialization in DiakopticsSolver"
    
  5. 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
    
  6. Open a pull request from your fork against sogno-platform/dpsim:master.

Pull Requests

There are no strict formal requirements besides the following:

  1. Developer Certificate of Origin (DCO)

    We require a Developer Certificate of Origin. See more here.

  2. 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.

  3. Development in Forks Only

    We accept contributions made in forks only. The main repository is not intended for contributor-specific branches.

  4. 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.

  5. 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
    
  6. 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:

    jupyter nbconvert --ClearOutputPreprocessor.enabled=True --inplace <notebook.ipynb>
    

    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.

You can also check the Issues List or the Pull Requests on GitHub.

Provenance and responsibility

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.

SectionFor someone who wants toNames C++ classes
User Guideinstall DPsim and run a simulationno
Tutorialslearn by working through one idea at a timeonly Python API calls
Developer Guidechange DPsim, or understand how it works insideyes
Conceptsknow the mathematics behind a model or methodnever
Referencelook up what exists and wheregenerated
Contributingcontribute to the projectn/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 %}}
ColourUse it forTitle starts with
infoa prerequisite: a build flag, an optional dependency, a package that is not installed by defaultRequires
warninga trap: correct code that silently does the wrong thing, a parameter that means two things, an ordering that mattersWatch out:
dangera known defect in DPsim itself, not something the reader can avoid by being carefulDefect: or Suspected defect:
primarya genuine aside that is neither a prerequisite nor a hazardNote

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:

curl -sS -X POST "$LLM_BASE_URL/chat/completions" \
  -H "Authorization: Bearer $RWTH_LLM_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"model\":\"$LLM_MODEL\",\"messages\":[{\"role\":\"user\",\"content\":\"ok\"}]}"

Running locally

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
export LLM_BASE_URL='<openai-compatible-base-url>'
export LLM_MODEL='<model-identifier>'
export LLM_CHAT_PATH='/chat/completions'
export LLM_API_KEY="$RWTH_LLM_TOKEN"
export BASE_SHA=$(git rev-parse origin/main) HEAD_SHA=$(git rev-parse HEAD) PR_NUMBER=0
python3 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:

@software{dpsim,
  title  = {DPsim},
  author = {{The DPsim Authors}},
  url    = {https://github.com/sogno-platform/dpsim},
  note   = {Version 1.2.1}
}

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:

Real-time and co-simulation: