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 a function, e.g. getTasks()
, in order to retrieve the tasks from instances of the four classes mentioned previously.
The power system model element tasks are collected by the Solver
instances and relayed to the Simulation
.
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 - Scheduling
DPsim implements level scheduling.
A task T4 that depends on data modified by task T1 is scheduled to the level following the level of task T1.
In the simplest case, all tasks of a level have to be finished before tasks of the next level are started.
The dependencies of tasks on data are determined by referencing the attributes that are read or modified by the task.
The scheduler computes the schedule prior to the simulation from the task dependency graph resulting from the tasks’ data dependencies.
4 - 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
.
5 - 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
:
// 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::initializeFromNodesAndTerminals(Real frequency) {
//...
// Create series sub components
mSubSeriesResistor = std::make_shared<DP::Ph1::Resistor>(**mName + "_res", mLogLevel);
// Setup mSubSeriesResistor...
// 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);
//...
}
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);
}
6 - Interfaces
Interfaces can be used to share data between a DPsim simulation and other, external services, for example an MQTT-broker. For the purpose of this guide, all services receiving and transmitting data besides the running DPsim instance are grouped in the term environment. Therefore, interfaces
provide a way for a DPsim simulation to exchange data with the environment.
This data is stored in the form of Attributes and can be imported or exported in every simulation time step. Exporting an attribute means that on every time step, the current value of that attribute is read and written out to the environment.
Importing an attribute means that on every time step, a new value is read from the environment and the attribute in the simulation is updated to match this value.
Configuring an Interface
On the configuration level, an interface is an instance of the Interface
class.
Because the base Interface
class requires an instance of an InterfaceWorker
to construct, it is recommended to not use this base class directly,
but instead construct a subclass derived from Interface
which internally handles the construction of the InterfaceWorker
. Currently, there exists
only one such subclass in DPsim which is the InterfaceVillas
.
Configuring the InterfaceVillas
This feature requires the compilation of DPsim with the WITH_VILLAS
feature flag. For use of the InterfaceVillas
in python, the dpsimpyvillas
target has to built in addition to the normal dpsimpy
package.
The InterfaceVillas
is an interface designed to make use of the various node types and protocols supported by the VILLASframework. By utilizing the nodes provided by VILLASnode, the InterfaceVillas
can be configured to import and export attributes from and to a wide range of external services. To create and configure an InterfaceVillas
instance, create a new shared pointer of type InterfaceVillas
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 that should be used for data import and export.
This means that the JSON contains a type
key describing what node type to use, as well as any additional configuration options required for this node type. The valid configuration keys can be found in the VILLASnode documentation.
After the InterfaceVillas
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. For an explanation of the various parameters, see the code documentation in InterfaceVillas.h
. 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 InterfaceVillas
can be configured as follows:
Using C++:
// JSON configuration adhering to the VILLASnode documentation
std::string mqttConfig = R"STRING({
"type": "mqtt",
"format": "json",
"host": "mqtt",
"in": {
"subscribe": "/mqtt-dpsim"
},
"out": {
"publish": "/dpsim-mqtt"
}
})STRING";
// Creating a new InterfaceVillas object
std::shared_ptr<InterfaceVillas> intf = std::make_shared<InterfaceVillas>(mqttConfig);
// Configuring the InterfaceVillas to import and export attributes
intf->importAttribute(evs->mVoltageRef, 0, true, true);
intf->exportAttribute(r12->mIntfCurrent->deriveCoeff<Complex>(0, 0), 1, true, "v_load");
Using Python:
# JSON configuration adhering to the VILLASnode documentation
mqtt_config = '''{
"type": "mqtt",
"format": "json",
"host": "mqtt",
"in": {
"subscribe": "/mqtt-dpsim"
},
"out": {
"publish": "/dpsim-mqtt"
}
}'''
# Creating a new InterfaceVillas object
intf = dpsimpyvillas.InterfaceVillas(name='dpsim-mqtt', config=mqtt_config)
# Configuring the InterfaceVillas to import and export attributes
intf.import_attribute(evs.attr('V_ref'), 0, True)
intf.export_attribute(r12.attr('i_intf').derive_coeff(0, 0), 0)
Adding an Interface to the Simulation
After a new interface has been created and configured, it can be added to a simulation using the Simulation::addInterface
method:
// Create and configure simulation
RealTimeSimulation sim(simName);
sim.setSystem(sys);
sim.setTimeStep(timeStep);
sim.setFinalTime(10.0);
// Create and configure interface
auto intf = //...
// Add interface to simulation
sim.addInterface(intf);
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.