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.
1 - Component and Solver Initialization
How DPsim initializes components and solvers before the first simulation timestep.
Initialization is the phase between constructing the system topology and running the first timestep.
Its job is to size the system matrices, derive initial state from power-flow results, register MNA tasks, and stamp static conductances.
Two constraints drive its structure:
- The system matrix size depends on the total number of simulation nodes, including virtual nodes declared by composite components and their sub-components. All virtual nodes must therefore be known before the matrices are allocated.
- Component parameter values (impedances, initial phasors) depend on terminal voltages and powers, which are only available after a power-flow solve.
These two constraints impose an ordering that is captured in the solver’s initialization sequence.
MNA Solver Initialization Sequence
MnaSolver::initialize() executes the following steps in order.
flowchart TD
start([Simulation::run]) --> init[MnaSolver::initialize]
init --> s1["S1: identifyTopologyObjects()\nSort into mMNAComponents,\nmSimSignalComps, ..."]
s1 --> s2["S2: createSubComponents() pre-pass\nRecursively instantiate sub-components\nso all virtual nodes exist"]
s2 --> s3["S3: collectVirtualNodes()\nassignMatrixNodeIndices()\nMatrix size is now fixed"]
s3 --> s4["S4: createEmptyVectors()\ncreateEmptySystemMatrix()"]
s4 --> s5a["S5a: initializeFromNodesAndTerminals(freq)\nfor each SimPowerComp"]
s5a --> s5b["S5b: initialize(omega, dt)\nfor each SimSignalComp"]
s5b --> s5c["S5c: mnaInitialize(omega, dt, v)\nfor each MNAInterface component"]
s5c --> cond{mSteadyStateInit?}
cond -- yes --> s6["S6: steadyStateInitialization()\nIterate MNA until phasors converge"]
s6 --> s7
cond -- no --> s7["S7: setBehaviour(MNASimulation)\non all components"]
s7 --> s8["S8: initializeSystem()\nStamp static elements,\ncompute LU factorizations"]
s8 --> done([Ready for timesteps])
Step 1 — Identify topology objects
identifyTopologyObjects() iterates over SystemTopology::mComponents and sorts each component into one of four lists:
| List | Contents |
|---|
mMNAComponents | Static MNA power components |
mMNAIntfVariableComps | Variable-stamp MNA components (e.g. under MNAVariableCompInterface) |
mMNAIntfSwitches | Components with a switch interface |
mSimSignalComps | Signal components (SimSignalComp) |
Ground nodes are excluded here.
Step 2 — Create sub-components (pre-pass)
Before the matrix can be sized, every composite component’s sub-component tree must be fully instantiated so that all virtual nodes are visible.
The solver calls createSubComponents() recursively on every MNA component:
- Only sub-components newly registered by this call are recursed into, because eagerly-constructed sub-components (created in the constructor before
connect() has run) are not yet safe to recurse into. - This step is a pre-pass only — it must not set parameter values derived from terminal data or frequency.
For details on the three-stage composite lifecycle (createSubComponents, initializeParentFromNodesAndTerminals, mnaCompInitialize), see Subcomponent Handling.
Step 3 — Collect virtual nodes and assign indices
collectVirtualNodes() visits every component and calls virtualNodes() to collect all virtual SimNode objects, then appends them to the solver’s node list.
assignMatrixNodeIndices() then assigns a contiguous integer index to every simulation node (real and virtual), which determines the row/column layout of the system matrices.
After this step the matrix size is fixed.
Step 4 — Allocate empty matrices
createEmptyVectors() and createEmptySystemMatrix() allocate the left-side vector, right-side vector, system matrix (dense or sparse depending on the solver variant), and switch-variant copies.
For sparse solvers, mBaseSystemMatrix and mLuFactorizations are also allocated here, with one variant per switch combination.
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:
checkForUnconnectedTerminals() validates connectivity.- If
mInitFromNodesAndTerminals is set (the default), initializeFromNodesAndTerminals(mSystem.mSystemFrequency) is called.
This is where components read their terminal voltages and powers and derive physical parameters (impedances, initial phasor values, per-unit quantities).
For composite components initializeFromNodesAndTerminals() is final in CompositePowerComp and sequences the three lifecycle stages automatically; non-composite power components override it directly.
5b — Signal components: initialize(omega, timeStep)
Each SimSignalComp in mSimSignalComps receives initialize(mSystem.mSystemOmega, mTimeStep).
This is the hook for signal-domain components (regulators, governors, PSS blocks) to allocate their state buffers, set initial values, and wire up attribute connections.
Watch out: do not name a hook initialize(Real)
Do not use initialize(Real) or initialize(Real, Real) as a user-facing initialization hook
for power components. Those signatures match the solver’s signal-component hook, so the solver
calls them rather than the component author’s intent. Use initializeFromNodesAndTerminals()
or a named method such as initializeStates() instead.5c — MNA components: mnaInitialize
Each MNA component (including switches) receives mnaInitialize(omega, timeStep, leftVector).
In MNASimPowerComp this method:
- Clears and re-registers
MNAPreStep / MNAPostStep tasks according to the hasPreStep / hasPostStep flags. - Initializes
mRightVector to zero with the correct size. - Calls
mnaCompInitialize(omega, timeStep, leftVector) on the component.
In mnaCompInitialize, component classes call updateMatrixNodeIndices() and perform any one-time MNA setup that requires the final node layout (e.g. allocating per-component history vectors sized to the system).
Nodes are initialized last via SimNode::initialize(), which zeros the node voltage.
Step 6 — Optional steady-state initialization
If mSteadyStateInit is set, steadyStateInitialization() iterates the MNA solve until the phasor solution converges.
The flag mIsInInitialization is set to true for this sub-phase so that components can distinguish initialization solves from simulation solves via mBehaviour (see below).
Step 7 — Set simulation behaviour
After initialization solves are complete, the solver calls setBehaviour(TopologicalPowerComp::Behaviour::MNASimulation) on every TopologicalPowerComp and setBehaviour(SimSignalComp::Behaviour::Simulation) on every SimSignalComp.
The Behaviour enum (defined in TopologicalPowerComp) has three values:
| Value | When active | Typical use |
|---|
Behaviour::Initialization | During PF steady-state init pass | Components may disable transient update equations |
Behaviour::PFSimulation | During PFSolver run | Activates power-flow-specific stamping |
Behaviour::MNASimulation | After initialize() completes | Normal simulation; components should be in their run-time mode |
Components that need different behaviour between initialization and simulation check mBehaviour in their pre/post-step methods or in mnaCompPreStep.
Step 8 — Initialize system matrices (initializeSystem)
initializeSystem() selects one of three paths:
- Parallel frequencies (
initializeSystemWithParallelFrequencies): stamps each frequency into a separate thread. - Variable matrix (
initializeSystemWithVariableMatrix): used by MnaSolverSysRecomp; saves static switch matrices as base matrices and adds variable elements on top. - Precomputed matrices (
initializeSystemWithPrecomputedMatrices): the common path. Calls switchedMatrixStamp() for each switch combination, which iterates over all static MNA components and calls mnaApplySystemMatrixStamp() and mnaApplyRightSideVectorStamp(). LU factorizations are computed for each variant.
After this step the solver is ready to execute timesteps.
Component Class Hierarchy and Init Hooks
The following diagram shows which initialization methods live in which class, and the override points for component authors.
classDiagram
class TopologicalPowerComp {
+Behaviour mBehaviour
+setBehaviour(b)
}
class SimPowerComp~T~ {
+initialize(Matrix frequencies)
+initializeFromNodesAndTerminals(Real freq)
+virtualNodes()
}
class MNASimPowerComp~T~ {
+mnaInitialize(omega, dt, v) final
+mnaCompInitialize(omega, dt, v)*
+mnaCompApplySystemMatrixStamp()*
+mnaCompPreStep()*
+mnaCompPostStep()*
}
class CompositePowerComp~T~ {
+createSubComponents()*
+initializeFromNodesAndTerminals(freq) final
+initializeParentFromNodesAndTerminals(freq)*
+mnaParentInitialize(omega, dt, v)*
+mnaParentPreStep()*
+mnaParentPostStep()*
}
class SimSignalComp {
+initialize(Real omega, Real dt)*
}
TopologicalPowerComp <|-- SimPowerComp
SimPowerComp <|-- MNASimPowerComp
MNASimPowerComp <|-- CompositePowerComp
Methods marked * are the virtual override points for component authors.
Methods marked final must not be overridden; the base class sequences them correctly.
Component Method Contracts
The table below summarizes which initialization method has which responsibilities. A tick means the operation belongs in that method; a cross means it must not appear there.
| Responsibility | Constructor / setParameters | createSubComponents | initializeFromNodesAndTerminals | mnaCompInitialize |
|---|
| Declare virtual node count | ✓ | — | — | — |
| Allocate sub-component objects | — | ✓ | — | — |
connect() sub-components to virtual nodes | — | ✓ | — | — |
addMNASubComponent() registration | — | ✓ | — | — |
| Read terminal voltage / power | ✗ | ✗ | ✓ | — |
| Read system frequency | ✗ | ✗ | ✓ (via argument) | ✓ (via omega) |
| Compute impedance / admittance | — | ✗ | ✓ | — |
Call setParameters() on sub-components | — | — | ✓ | — |
Call updateMatrixNodeIndices() | — | — | — | ✓ |
| Allocate per-step vectors (history, right vector) | — | — | — | ✓ |
| Register MNA tasks (handled by base class) | — | — | — | ✓ (via mnaCompInitialize) |
Common pitfalls
- Accessing terminals in the constructor or
createSubComponents: terminal data (initial voltage, connected power) is not yet populated. The topology is set up but power-flow has not run. - Accessing
mFrequencies(0,0) in createSubComponents: the system frequency matrix is set on SimPowerComp via initialize(Matrix) which only runs later. Use the frequency argument passed to initializeParentFromNodesAndTerminals or the omega argument in mnaCompInitialize. - Zero-valued shunt branches: a capacitor or reactor with zero admittance injects a zero row/column into the system matrix, which makes the LU factorization singular. Guard with a strict
> 0 check and omit the branch rather than inserting a zero stamp.
Watch out: virtual nodes must exist before the solver collects them
A virtual node created for the first time after collectVirtualNodes() (step 3) never gets a
matrix index, and the solver then crashes or silently produces wrong results. Declare every virtual
node in the constructor or in setParameters().
Composite Component Initialization Sequence
The following diagram shows how the solver and a composite component interact during initialization. For further details see Subcomponent Handling.
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.
Watch out: overriding this hook makes you responsible for the base call
It is not a hook for component authors — a component that overrides it takes over responsibility for calling the base class version, which is easy to forget.The recommended path is:
- For power components, use
initializeFromNodesAndTerminals() or initializeParentFromNodesAndTerminals(). - For signal components, use the
initialize(Real omega, Real timeStep) hook provided by SimSignalComp. - For anything else (e.g. setting up state-space matrices), add a named helper called from one of the above.
The base implementation of SimPowerComp::initialize(Matrix) should be renamed to something that cannot be accidentally overridden (e.g. propagateFrequencies()), and an override guard should be added to catch accidental overrides.
Sub-component construction in constructors
Some components create and register sub-components eagerly in their constructor before connect() has been called on those sub-components.
This works today because the solver’s createSubComponents pre-pass skips already-registered sub-components, but it couples topology creation to object construction and makes components harder to reason about.
The long-term goal is to migrate all sub-component construction to createSubComponents(), giving a clear rule: the constructor only allocates and the topology stage wires.
Signal component initialize not sequenced with power flow
Signal components receive initialize(omega, timeStep) 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.
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.
Watch out: a single allocation can miss a deadline
No allocating memory, no mutexes or other interrupt-driven synchronisation, and no reading or
writing files inside the step. Any one of these can block for longer than the step, and the failure
appears as an occasional overrun rather than as an error, so it is easy to miss in a short test run.Turn logging off when the step is in the low milliseconds. RealTimeDataLogger exists for the cases
that still need results: it buffers in memory and writes at the end rather than touching the disk
inside the step.
Watch out: the real-time logger preallocates everything
RealTimeDataLogger allocates the memory for the entire run up front, which is what keeps it off the
critical path. A long run, or too many logged attributes, can therefore exhaust memory before the
simulation starts. See
loggers.
3 - 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.
4 - Subcomponent Handling
Building a component out of other components with CompositePowerComp.
In DPsim, there are many components which can be broken down into individual subcomponents. Examples are the PiLine, consisting of an inductor, three resistors, and two capacitors, or the NetworkInjection which contains a voltage source.
On the C++ class level, these subcomponents are represented by member variables within the larger component class. In this guide, all components which have subcomponents are called composite components.
Creating Composite Components
While normal components are usually subclasses of SimPowerComp<T> or MNASimPowerComp<T>, there exists a special base class for composite
components called CompositePowerComp<T>. This class provides multiple methods and parameters for configuring how the subcomponents should be
handled with respect to the MNAPreStep and MNAPostStep tasks.
The main idea here is that the subcomponents do not register their own MNA tasks, but instead their MNA methods like mnaPreStep and mnaPostStep are called explicitly in the tasks of the composite component.
In the constructor of CompositePowerComp<T>, the parameters hasPreStep and hasPostStep can
be set to automatically create and register a MNAPreStep or MNAPostStep task that will call the mnaCompPreStep or mnaCompPostStep method on execution.
Additionally, all subcomponents should be registered as soon as they are created using the addMNASubComponent-method. This method takes
multiple parameters defining how and in what order the subcomponent’s pre- and post- steps should be called, as well as if the subcomponent
should be stamped into the system rightVector.
Initialization lifecycle
Composite components are initialized in three stages, each with a defined role. (These are distinct from the electrical phases A/B/C of a three-phase component.)
- Topology stage (
createSubComponents()). Decides which sub-components exist and how they are wired: make_shared, connect() to network/virtual nodes, and addMNASubComponent(). This runs in a pre-pass before the MNA system matrix is sized, so any virtual nodes owned by sub-components are visible to collectVirtualNodes(). Because it runs before power-flow results or the simulation frequency are guaranteed to be available, createSubComponents() must not read terminal data (initialSingleVoltage(), singleActivePower(), …), system frequency (mFrequencies(0,0)), or compute any power-/impedance-derived value. It must be idempotent — guard the body with mSubCompCreated (a protected field inherited from CompositePowerComp). - Parameterization stage (
initializeParentFromNodesAndTerminals(Real frequency)). Sets the values the sub-components created in stage 1 will use. This is where terminal reads, frequency-dependent impedance/admittance calculations, and setParameters() calls on sub-components belong. The simulation frequency is passed in as a direct argument, so there is no need to access mFrequencies(0,0). This is the hook concrete composites must implement — do not override initializeFromNodesAndTerminals() directly; the base class owns that method and calls this hook at the right time. - MNA-init stage (
mnaCompInitialize()). Unchanged; already recurses into sub-components.
CompositePowerComp<VarType>::initializeFromNodesAndTerminals() is final and sequences these stages:
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.
Watch out: a degenerate parameter injects NaN across the whole matrix
Value derivation in the parameterization stage must be numerically safe at degenerate inputs. Sub-component values come from terminal power, voltage, and frequency, so a zero rated power, zero reactance, or zero capacitance can produce a division by zero and inject NaN/inf into the system matrix or source vector, which then persists for the rest of the simulation. Guard such expressions: use admittance form (Y = jwC, branch current V * Y) rather than impedance form (V / Z), and create optional shunt branches only when their value is strictly positive — a zero-valued shunt is just an open circuit. A degenerate value that slips through will produce a wrong but non-crashing topology, so an explicit check with a log message is better than assuming the value is nonzero.// DP_Ph1_PiLine.cpp
DP::Ph1::PiLine::PiLine(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);
}
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.hdpsim-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.cppdpsim-models/include/dpsim-models/Components.h, adding the header so that including that one
file gives access to every componentdpsim/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.
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
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
}
]
}
]
}
8 - Logger Implementation
The classes called a logger, and the seam for adding another.
Using the loggers is covered under logging results.
This page covers the classes.
Distinct things share the name
| Class | Purpose |
|---|
DPsim::DataLogger | Numerical results to CSV, one row per step |
DPsim::RealTimeDataLogger | The same results, buffered in memory for real-time runs |
DPsim::DataLoggerInterface | The seam both implement, and the one to implement for a new sink |
CPS::Logger | The diagnostic text log, controlled by LogLevel |
Only the data loggers have anything to do with results. CPS::Logger is a different subsystem that
happens to share the word, and conflating the two is the most common confusion here. A component
constructed with Logger::Level::debug writes prose about its own initialization and contributes
nothing to any CSV.
DataLogger
Holds a map from column name to attribute and appends a row per step. log(Real time, Int timeStepCount) returns early when the logger is disabled or when
timeStepCount % mDownsampling != 0, so down-sampling is a modulo on the step counter rather than a
time comparison, and it is exact regardless of step size.
The header is written lazily on the first row, by testing mLogFile.tellp() == 0. That means the
column set is fixed by whatever was registered before the first log call; registering an attribute
afterwards would produce rows that no longer match the header.
Values are written with std::scientific in fixed-width columns, which is what makes the output
readable as a table and also what makes it larger than a minimal CSV would be.
The constructor takes (name, enabled, downsampling). The Python binding exposes only the name, so
enabled and downsampling are unreachable from Python. A binding that took all three would make
down-sampling available to notebook users, who currently have only the time step.
RealTimeDataLogger
Exists because writing to disk inside a real-time step is not acceptable: the file system offers no
bound on how long a write takes, and one slow write overruns the step. It preallocates
mAttributeData from either the final time and step size or an explicit row count, fills it during
the run, and writes at the end.
The preallocation is the point, and it is also the constraint: the row count must be known before
the run, so a real-time simulation of indefinite length needs a different arrangement.
DataLoggerInterface
The abstract seam. Implement it to send results somewhere other than a file, which is what the
co-simulation interfaces do rather than logging and re-reading. Simulation::addLogger accepts
anything implementing it.
Scheduling
A logger contributes a task like any other component, so the scheduler places it by its declared
attribute dependencies. A logged attribute therefore keeps alive the task that produces it, which
has a consequence worth knowing: logging an attribute can change which tasks the scheduler considers
reachable. A model whose results change when a logger is added is exhibiting a missing dependency
declaration elsewhere, not a logging bug. See
adding tasks to a component.
Source
dpsim/src/DataLogger.cpp, dpsim/src/RealTimeDataLogger.cpp,
dpsim/include/dpsim/DataLoggerInterface.h, and dpsim-models/include/dpsim-models/Logger.h for
the unrelated diagnostic logger.