1 - Install
Installing DPsim and its Python package.
DPsim is a Python module and C++ library for dynamic power system simulation.
The quickest route to a result is the Python module: install it, then work through the
tutorials, which build up one idea at a time from a source and a
resistor. If you would rather read a finished study than build one, the
example notebooks run complete scenarios and plot them.
Building from source is only needed for a platform without a published wheel, or to work on DPsim
itself; see build.
Try it without installing
The example notebooks run in the browser with no local installation:

Python package
DPsim is published on PyPI and installs like any other Python package:
python3 -m venv venv
source venv/bin/activate
pip install dpsim
Requires Linux for the published wheels
Two limitations are worth knowing before you start.
Only Linux wheels are currently published, for CPython 3.9 through 3.13, so on Windows and macOS
you have to build from source for now.
The package also contains only the simulation core; the example notebooks additionally need
plotting and data handling packages, which are listed in the import section of each notebook.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.
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 to | Go to |
|---|
| Learn how a simulation is put together | Tutorials |
| See a complete study with plots | Notebooks, below |
| Use DPsim from an application | C++ examples, below |
| Look up what a model does | Concepts |
Notebooks
Run them in the browser with no local installation:

Locally, they live under
examples/Notebooks and
need the Python package on the path, as described in the
build section.
| Category | Contents |
|---|
| Quickstart Guide | A single notebook covering a first simulation end to end |
| Circuits | Small networks exercising one modelling aspect at a time |
| Components | One component at a time, often comparing domains against each other |
| Grids | Published test systems such as the WSCC 9 bus and CIGRE networks |
| Features | Cross-cutting capabilities rather than a specific network |
| Performance | Timing and scaling comparisons |
| StateSpace | State-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.
C++ examples
Under dpsim/examples/cxx,
built as part of a normal build and produced as executables in the build directory.
| Directory | Contents |
|---|
| Circuits | Networks assembled directly in C++ |
| Components | Single component scenarios |
| CIM | Reading network data from CIM and CGMES files |
| StateSpace | State-space extraction |
| RealTime | Scenarios run against the wall clock |
| DAE, signals, timer | Smaller scenarios for the DAE solver, signal models and timing |
The target name is the source file name without its extension, regardless of which directory the
source sits in, and the executables are written flat into the build tree. So to build and run a
single example from your build directory:
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.
3 - Logging Results
Choosing what a simulation records, where it goes, and what it costs.
A simulation records nothing unless asked. Every quantity you want afterwards has to be named before
the run, because the solver keeps only what the current step needs and discards the rest.
dpsimpy.Logger is the one to reach for. It is the CSV data logger and it is what nearly every
example and notebook uses. There is also a real-time data logger, which is the right choice under a
real-time timer for the reason given below, and an interface for sending results somewhere other
than a file.
Only attributes can be logged
Watch out: only attributes can be logged
A logger records an attribute and nothing else. It cannot record an arbitrary expression, a
plain member variable, or a quantity a component computes internally without publishing.
The same constraint governs co-simulation and task scheduling, so a value that is not an attribute
cannot be logged, exchanged with another tool, or depended on by another task. Making it one is a
change to the model, not to the call site.
So the question is never “how do I log this value” but “is this value an attribute”. If it is, one
line records it. If it is not, no logger option will reach it, and the answer is to expose it as an
attribute in the model, which is a code change described under
attributes.
print_attribute_list() on any object prints what it publishes, which is the reliable way to find
out. Anything absent from that list cannot be logged.
The same fact explains a convenience: because attributes are the unit, a derived quantity that is
itself an attribute is logged the same way as a terminal voltage, with no special handling.
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.
Note: down-sampling is not reachable from Python
The C++ DataLogger additionally accepts a down-sampling factor, writing every n-th step. That
argument is not exposed on the Python Logger, which takes only a name, so from Python the step
size is the only control over how many rows you get.Data logging against diagnostic logging
The word covers two unrelated things and they are easy to confuse.
What this page describes is the data logger: numerical results, CSV, opt-in per attribute. The
other is the diagnostic log, the text file recording what the solver did, controlled by
dpsimpy.LogLevel and passed to component constructors. Raising a component’s log level makes it
describe its own initialization and stamping in prose; it has no effect on the CSV.
They land in the same logs/ directory side by side, one as .csv and the other as .log, which
is why they get mistaken for each other. A component constructed with LogLevel.debug produces a
great deal of text and no additional results.
The real-time data logger mentioned at the top is a data logger like the first, not a third kind of
log. It records the same results and differs only in buffering them in memory and writing at the
end, because a disk write inside a real-time step has no bound on how long it takes. See
real-time.
4 - Real-Time
Running a simulation against a wall clock, and what that requires.
Normally a simulation runs as fast as it can: a one-second study finishes in whatever time the
solver needs. In a real-time simulation, one second of simulated time takes one second of wall clock
time, no faster and no slower.
When you need it
Only when something outside the simulation has its own clock. A controller running on real hardware,
another simulator you are coupled to, or a person turning a dial all move at their own pace, and the
simulation has to move with them. If nothing outside is waiting, running in real time only makes the
study slower.
This is why co-simulation and real-time usually appear together.
What it changes
Two things. The simulation waits at the end of each step until the wall clock catches up, so a run
takes as long as the time it simulates. And a step that takes longer than its own duration is an
overrun: the simulation has missed its deadline and can no longer claim to be in step with the
outside world.
Overruns are the whole difficulty. Everything else about real-time execution is arranging for them
not to happen: keeping the step’s work bounded, and keeping the operating system from interrupting
it.
Watch out: an overrun does not stop the simulation
A missed deadline is reported, not fatal. The run continues and its results remain numerically
correct; what is no longer true is that it kept pace with anything external. A run that overran
repeatedly is not a real-time run, however normal its output looks.What it takes
A time step that comfortably exceeds the work done in it, models that do nothing slow inside the
step, and a host that will not interrupt at the wrong moment. Millisecond steps are undemanding;
microsecond steps need a tuned kernel and careful models, and are where most of the effort goes.
The requirements on the host and on the models are in
real-time execution. With that
tuning, steps as low as 5 us synchronised to an FPGA through VILLASnode have been achieved.
Running a Real-Time Simulation
Before running a simulation, you can run the following commands as root:
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
5 - Co-simulation and Interfaces
Exchanging signals with other simulators, services or hardware during a run.
Interfaces can be used to exchange simulation signals between a DPsim simulation and other soft- or hardware, for example an MQTT-broker or an FPGA.
Simulation signals in the form of Attributes can be imported or exported once per simulation time step.
Interfaces are subclasses of Interface and implement the methods addExport and addImport, which add dependencies to the passed attribute that forward the attribute value from or to the interface.
This way, attributes that are imported are read from the interface before they are used in any DPsim component.
Attributes that are exported are written to the interface after they are set by a DPsim component.
Where the boundary is, and where to read further
This page documents only DPsim’s side of the interface: which attributes are exchanged, when they
are read and written relative to the time step, and how the simulation is synchronized. Everything
on the other side of the JSON configuration belongs to VILLASnode and is documented there rather
than here, so the two should be read together.
The parts most often needed are these:
- Node types is the reference for the
type key and its per-type options. Which protocols are available, and what each one requires,
is decided here rather than in DPsim. - Nodes covers the surrounding configuration
structure that the node object sits in.
- Hooks describe the processing that can be
applied to samples in transit, such as scaling, limiting or statistics. Anything that can be done
with a hook does not need to be done in the simulation, which is usually the better place for it.
Watch out: the signal mapping is positional, not by name
Two things about the split are worth knowing before configuring anything. The signal ordering in
the VILLASnode configuration must match the order in which attributes are exported and imported,
because the mapping is positional rather than by name; a mismatch produces a running simulation
exchanging the wrong quantities. And the queueless interface additionally reserves the first input
signal for a sequence number, so its signal list is offset by one relative to a queued
configuration carrying otherwise identical data. That is described with the rest of the
configuration under
interfaces.