Overview
DPsim is a real-time capable power system simulator that supports dynamic phasor and electromagnetic transient simulation as well as continuous powerflow. It primarily targets large-scale scenarios on commercial off-the-sheld hardware that require deterministic time steps in the range of micro- to milliseconds.
DPsim supports the CIM format as native input for the description of electrical network topologies, component parameters and load flow data, which is used for initialization. For this purpose, CIM++ is integrated in DPsim.
Users interact with the C++ simulation kernel via Python bindings, which can be used to script the execution, schedule events, change parameters and retrieve results. Supported by the availability of existing Python frameworks like Numpy, Pandas and Matplotlib, Python scripts have been proven as an easy and flexible way to codify the complete workflow of a simulation from modelling to analysis and plotting, for example in Jupyter notebooks.
The DPsim simulation kernel is implemented in C++ and uses the Eigen linear algebra library. By using a system programming language like C++ and a highly optimized math library, optimal performance and real-time execution can be guaranteed.
The integration into the VILLASframework allows DPsim to be used in large-scale co-simulations.
Licensing
The project is released under the terms of the MPL 2.0.
Where should I go next
1 - Architecture
Modules and Dependencies
The figure below shows the main components of the DPsim library and their dependencies on other software projects.
All functionality is implemented in the C++ core, which can be used standalone or together with the Python interface.
The Python interface is a thin wrapper of the C++ core.
Jupyter notebooks can either use the DPsim Python interface to run simulations or call executables implemented in C++.
The data analysis and plotting is always done in Python using common libraries like Matplotlib.
To collect the simulation results from within Python, one can use the villas-dataprocessing Python package.

Another approach to get data in or out of DPsim is the VILLASnode interface, which does not depend on Python at all.
The main purpose of the VILLASnode interface is to exchange data during the simulation runtime, for example, in real-time simulation experiments.
The data could be send to other simulators, hardware or other software components like databases.
Storing the data in databases can be another way of managing (also offline) simulation results if the Python CSV method is not desireable.
The CIM reader is based on the CIM++ library and provides a comfortable alternative to defining the grid manually in C++ or Python.
In principle, it calls the same functions to create elements, which are also used in the C++ defined example scenarios, but automatically.
DPsim also provides a way to visualize the defined networks before simulation.
The main solver of DPsim is currently the MNA solver because it enables a rather deterministic computation time per simulation time step, which is necessary for real-time simulation.
Apart from that, it is also well established in offline circuit simulation.
The only dependency of the MNA solver is the linear algebra library Eigen.
For some component models, it is possible to use the Sundials ODE solver in combination with the MNA solver. In that case, the component is solved by the ODE solver whereas the network is still handled by the MNA solver.
A DAE solver is currently under development.
Its main purpose will be offline simulation, for example, to provide reference results where simulation runtime and real-time execution are not relevant.
The component models depend mostly on the Eigen library.
Even if components are used in combination with Sundials ODE / DAE solvers, we try to keep the specific functions required by these solvers independent of the Sundials package.
Class Hierarchy
The Simulation class holds references to instances of Interface, Solver, Logger and SystemTopology.
For a simulation scenario, the minimum description would include a SystemTopology and a solver type.
The Solver instance is then created by the Simulation.

An important function of the Simulation is to collect all tasks, which have to be executed during the simulation.
These tasks include computation steps of the individual power system component models as well as read and write tasks of the interfaces and logging variables etc.
Before the scheduling is done, Simulation calls getTasks() to retrieve the tasks from three of these classes: Solver, Interface and Logger.
The power system component and signal-model tasks are collected by the Solver instances and relayed to the Simulation, while interfaces and loggers contribute their own tasks directly.
All power system element classes inherit from the IdentifiedObject class.
This class corresponds with the IdentifiedObject of the IEC61970 CIM and has a uid and name attribute as well.

The next layer of specialization includes information on the topological connection between network elements.
An electrical bus and network nodes in general are represented by the TopologiclaNode class.
The connection of electrical components, TopologicalPowerComp, is managed via terminals of type TopologicalTerminal.
These three types describe the electrical connections of the network, which are bidirectional and include voltages and currents.
The signal type elements, TopologicalSignalComp, can only have unidirectional components, which are not expressed using node and terminals.
Instead, the attribute system is used to define signal type connections.
2 - Attributes
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 - 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:
| List | Meaning |
|---|
mAttributeDependencies | Attributes this task reads in execute() |
mModifiedAttributes | Attributes this task writes in execute() |
mPrevStepDependencies | Attributes whose value from the previous timestep this task needs |
All three lists hold AttributeBase::Ptr objects, the same pointers used throughout the component model.
See Attributes for details on the attribute system.
Only attributes can participate in scheduling.
Plain C++ member variables (a Real, a Matrix, an internal state struct) are invisible to the scheduler, so no dependency edge can be formed around them.
The same constraint applies to simulation data recording: both the CSV logger (DataLogger) and the real-time data logger (RealTimeDataLogger) implement DataLoggerInterface, whose logAttribute() member function only accepts AttributeBase::Ptr.
The VILLASnode interface works the same way.
Any value that needs to cross a task boundary, be recorded to a data file, or be exchanged with an external tool must be stored in an Attribute<T>.
The component text logger (CPS::Logger, backed by spdlog) is a separate mechanism used for human-readable debug and diagnostic output.
It is not part of the scheduling system and can print any value regardless of whether it is an attribute.
For practical rules on when a variable should be an attribute versus a plain member variable, see Attribute Usage Guidelines.
Common component task conventions
The names below are component and solver conventions, not scheduler-level concepts.
The scheduler only sees the attribute dependencies a task declares; it has no notion of a “PreStep” or “PostStep” and never orders tasks by these names.
MNA components typically define two task classes per component:
| Task | Typical responsibility |
|---|
MnaPreStep | Component-specific preparation before the matrix solve, often updating internal state and stamping the right-hand-side contribution |
MnaPostStep | Component-specific update after the matrix solve, often reading the solution vector to update interface voltages and currents |
This is a common pattern rather than a fixed rule; the exact work each task does is component-specific.
Signal-domain components (regulators, governors, control blocks) define their own task list via getTasks(); many separate previous-step state handling from output updates, for example a PreStep that copies state from the previous step and a Step that updates the block outputs.
The solver itself contributes a task that solves the MNA system; individual components do not depend on it by name, they depend on leftVector instead (see below).
Building the schedule
Task collection
Simulation::prepSchedule() collects all tasks before the first timestep from three top-level sources:
- Solvers: each solver contributes its task list via
Solver::getTasks(). For MNA solvers this list bundles:- the matrix-solve task,
- MNA component pre-/post-step tasks from
MNASimPowerComp::mnaTasks() (built during solver initialization via mnaAddPreStepDependencies() / mnaAddPostStepDependencies()), - signal-domain component tasks returned by
SimSignalComp::getTasks(), - optional solver-side tasks, such as state-space extraction, when enabled.
- Interfaces: each interface contributes its own tasks via
Interface::getTasks(). These typically depend on the attributes exchanged with external systems. - Loggers: each logger contributes a logging task via
Logger::getTask(), depending on the logged attributes so values are written after the producing tasks have run.
All tasks are placed in a flat Task::List and handed to the scheduler.
Dependency resolution
Scheduler::resolveDeps() (dpsim/src/Scheduler.cpp) translates the attribute-level declarations into directed edges between tasks.
For every attribute in mModifiedAttributes, it finds all tasks that list that attribute in their mAttributeDependencies and adds an edge:
task A writes attr_X
task B reads attr_X
────────────────────────
edge: A → B (A must run before B)
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:
level 0 │ T1 T2 T3 ← no dependencies; can all start at once
level 1 │ T4 T5 ← depend only on level-0 tasks
level 2 │ T6 ← depends on T4 or T5
Tasks within the same level have no data dependencies between them and can execute in parallel.
The scheduler guarantees that all tasks in level k finish before any task in level k+1 starts.

Scheduler variants
| Class | Parallelism strategy |
|---|
SequentialScheduler | Single-threaded; follows topological order |
ThreadLevelScheduler | Distributes each level across N worker threads |
ThreadListScheduler | Distributes tasks greedily across N threads |
OpenMPLevelScheduler | Uses #pragma omp parallel for per level |
The scheduler is chosen at Simulation construction time; SequentialScheduler is the default.
Per-timestep execution
Scheduler::step(time, timeStepCount) is called once per timestep.
For the sequential scheduler:
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.
Developer guide: adding tasks to a component
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.
Missing a declaration does not always cause a crash; it silently produces incorrect results or a wrong execution order, which is harder to debug.
Two common failure modes follow from the pruning step:
- A
PreStep or PostStep task is dropped entirely because none of its declared modified attributes is needed by another task, a logger, an interface, or a previous-step dependency. The simulation then runs but its results are always wrong. - The same task appears to work only when a particular variable is logged or exchanged by an interface, because that logger or interface adds a dependency on the attribute and keeps the producing task reachable. The results then depend on logger or interface configuration even though the physical model did not change.
Declare dependencies conservatively.
4 - 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.
Naming constraint. 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-comp hook and will be called by the solver rather than by 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. - Late-registered virtual nodes: any virtual node that appears for the first time after
collectVirtualNodes() (Step 3) will not have a matrix index assigned, and the solver will crash or silently produce wrong results. All virtual nodes must be declared in the constructor or 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.
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.
5 - State-Space Extraction
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.
Supported scope
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.
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()
Examples
The feature is demonstrated in:
Equivalent Python notebooks are available in
examples/Notebooks/StateSpace.
6 - Interfaces
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.
Interfacing with VILLASnode
This feature requires the compilation of DPsim with the WITH_VILLAS feature flag. For use of the VILLASnode interface in python, the dpsimpyvillas target has to built in addition to the normal dpsimpy package.
The VILLASnode interface is designed to make use of the various node types and protocols supported by the VILLASframework.
By utilizing the nodes provided by VILLASnode, it can be configured to import and export attributes using a wide range of protocols.
There are two interface implementations for VILLASnode: InterfaceVillasQueued and InterfaceVillasQueueless.
InterfaceVillasQueued 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.
InterfaceVillasQueued 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 InterfaceVillasQueued 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.
Note for InterfaceVillasQueueless:
The queueless interface expects the first input signal in the VILLASnode configuration to be a sequence number that is incremented every time step.
If the value of the sequence number is not incremented by one between two consecutive time steps, an overrun is detected.
Because logging outputs can cause large delays and overruns should not occur spuriously, the interface only reports a warning when a large number of overruns occur.
After the object is created, the exportAttribute and importAttribute methods can be used to set up the data exchange between the DPsim simulation and the configured node.
The attributes given as the first parameter to these methods are attributes belonging to components in the simulation which should be read or updated by the interface.
As an example, for exporting and importing attributes via the MQTT protocol, the VILLASnode interfaces can be configured as follows:
Using C++:
// JSON configuration adhering to the VILLASnode documentation
std::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<InterfaceVillasQueued> intf = std::make_shared<InterfaceVillasQueued>(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);
This method will add two new Tasks to the simulation. The interface’s PreStep task is set to modify all attributes that are imported from the environment and is therefore scheduled to execute before any other simulation tasks that depend on these attributes.
The interface’s PostStep task is set to depend on all attributes that are exported to the environment and is therefore scheduled to execute after any other simulation tasks that might modify these attributes. To prevent the scheduler from just dropping the PostStep task since it does not modify any attributes and is therefore not seen as relevant to the simulation, the task is set to modify the Scheduler::external attribute.
Note that the execution of these tasks might not necessarily coincide with the point in time at which the values are actually written out to or read from the environment.
This is because the interface internally spawns two new threads for exchanging data with the environment and then uses a lock-free queue for communication between these reader and writer threads, and the simulation. Because of this, time-intensive import or export operations will not block
the main simulation thread unless this is explicitly configured in the interface’s importAttribute and exportAttribute methods.
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.
7 - Interfacing with the MNA Solver
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.
8 - Subcomponent Handling
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.
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);
}