Documentation
DPsim is a solver library for dynamic power system simulation.
- It supports both the electromagnetic transient (EMT) and dynamic phasor (DP) domain for dynamic simulation.
- A powerflow solver is included for standalone usage or to initialize dynamic simulations.
- It provides a Python module which can be embedded in any Python 3 application / scripts.
- The simulation core is implemented in highly-efficient C++ code.
- It supports real-time execution with time-steps down to 50 uS.
- It can load models in the IEC61970 Common Information Model (CIM) / Common Grid Model Exchange Standard (CGMES) XML format.
- It can be interfaced to a variety of protocols and interfaces via VILLASnode.
Connect
Using or want to use DPsim? Find out more here:
LF Energy Slack - Chat with other users and developers and get help in the #sogno or #sogno-dpsim channel.
You can also send a direct message to
- Markus Mirz
- Jan Dinkelbach
- Steffen Vogel
Contribute
If you want to get more involved by contributing to DPsim, please send us a Pull Request on GitHub.
Publications
If you are using DPsim for your research, please cite one of the following papers in your publications:
- M. Mirz, S. Vogel, G. Reinke, A. Monti, “DPsim — A dynamic phasor real-time simulator for power systems,” SoftwareX, Volume 10, July–December 2019, 100253.
- J. Dinkelbach, M. Moraga, and A. Monti, “Reduced-Order Synchronous Generator Modelling for Real-Time Simulation using Shifted Frequency Analysis,” in 2023 Open Source Modelling and Simulation of Energy Systems (OSMSES), 2023.
- J. Dinkelbach, L. Razik, M. Mirz, A. Benigni, and A. Monti, “Template-based generation of programming language specific code for smart grid modelling compliant with CIM and CGMES,” The Journal of Engineering, 2022.
- G. Nakti, J. Dinkelbach, M. Mirz, and A. Monti, “Comparative Assessment of Shifted Frequency Modeling in Transient Stability Analysis using the Open Source Simulator DPsim,” in 2022 Open Source Modelling and Simulation of Energy Systems (OSMSES), 2022.
- J. Dinkelbach, L. Schumacher, L. Razik, A. Benigni, and A. Monti, “Factorisation Path Based Refactorisation for High-Performance LU Decomposition in Real-Time Power System Simulation,” Energies, 2021.
- J. Dinkelbach, G. Nakti, M. Mirz, A. Monti, “Simulation of Low Inertia Power Systems Based on Shifted Frequency Analysis,” Energies, 2021.
- M. Mirz, A. Monti, and A. Benigni, “A Dynamic Phasor Real-Time Simulation Based Digital Twin for Power Systems,” E.ON Energy Research Center, RWTH Aachen University, 2020.
- M. Mirz, J. Dinkelbach, A. Monti, “DPsim — Advancements in Power Electronics Modelling Using Shifted Frequency Analysis and in Real-Time Simulation Capability by Parallelization,” Energies, 2020.
- M. Mirz, A. Estebsari, F. Arrigo, E. Bompard and A. Monti, “Dynamic phasors to enable distributed real-time simulation,” 2017 6th International Conference on Clean Electrical Power (ICCEP), Santa Margherita Ligure, 2017.
- S. Vogel, M. Mirz, L. Razik, A. Monti, “An Open Solution for Next-generation Real-time Power System Simulation,” 1st IEEE Conference on Energy Internet and Energy System Integration (IEEE-EI^2), Beijing, 2017.
- M. Mirz, A. Monti, A. Estebsari, F. Arrigo, E. Bompard, “Functionality of the releases of the real time solver V1,” RESERVE Library, 2017.
- M. Mirz, S. Vogel, A. Monti, “First Interconnection test of the nodes in pan-European simulation platform,” RESERVE Library, 2017.
1 - 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.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.
1.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.
1.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.
1.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
.
1.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);
}
1.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.
2 - Getting Started
How to install, build and run the DPsim project.
2.1 - Build
Docker based
Clone the repository
$ git clone git@github.com:sogno-platform/dpsim.git
or using https if you do not have an account
$ git clone https://github.com/sogno-platform/dpsim.git
In the repository, there is a Docker file with all required dependencies
$ cd dpsim
$ docker build -t sogno/dpsim:dev -f Packaging/Docker/Dockerfile.dev .
Alternatively, the image can be pulled from DockerHub like so
$ docker pull sogno/dpsim:dev
For OS specific instructions on how to install requirements, see the sections below.
Next, run a Docker container
$ cd dpsim
$ docker run -it -p 8888:8888 -v $(pwd):/dpsim --privileged sogno/dpsim:dev bash
The option -p
maps the port 8888 of the container to the docker host. This is required to access the jupyter lab instance inside the container. The option --privileged
is required for debug builds.
For Windows, you might need to specify the current directory with curly brackets
$ docker run -it -p 8888:8888 -v ${pwd}:/dpsim --privileged sogno/dpsim:dev bash
Now, you should be in an interactive session inside the docker container.
The DPsim C++ and Python library without C++ examples or documentation can be built as follows
$ cd /dpsim
$ mkdir build && cd build
$ cmake ..
$ cmake --build . --target dpsimpy
If you need other libraries that are not built by default, you need to target them specifically, for example if you need `dpsimpy´ and ´dpsimpyvillas´:
$ cmake --build . --target dpsimpy dpsimpyvillas
To build everything run
$ cmake --build .
To use other libraries that are installed, use the relevant option defined in the CMakeList.txt files, for example for GSL below, and then build as usual:
$ cmake .. -DWITH_GSL=ON
If you would like to use the Python package, it has to be added to the path.
The following command adds the dpsimpy C++/Python package as well as the dpsim pure Python package.
$ cd /dpsim/build
$ export PYTHONPATH=$(pwd):$(pwd)/../python/src/
If you are using conda
or other ways to develop with environments, please keep in mind that this will become specific for your setup. For this case, from within the environment already active:
$ cd /dpsim/build
$ conda develop $(pwd) && conda develop $(pwd)/Source/Python && conda develop $(pwd)/../Source/Python
To run jupyter lab
$ cd /dpsim
$ jupyter lab --ip="0.0.0.0" --allow-root --no-browser
To install dpsim run
$ cd /dpsim/build
$ sudo make install
CMake for Linux
The most recent list of requirements can be found in the Dockerfiles.
Make sure that the required dependencies are installed.
The fedora installation script in the DPsim repository is a good place to start from.
Note: There are currently no Debian packages for villas-node
and libcimpp16v29a
.
If you want to use these optional feature, you have to build them manually.
Install Sundials
$ git clone --branch v3.1.1 https://github.com/LLNL/sundials.git
$ mkdir sundials/build
$ pushd sundials/build
$ cmake .. \
-DBUILD_SHARED_LIBS=ON \
-DBUILD_STATIC_LIBS=OFF \
-DEXAMPLES_ENABLE_C=OFF
$ make -j$(nproc) install
$ popd
The following steps to clone, build and install are the same as for the Docker setup.
CMake for Windows
Make sure that the required dependecies are installed:
- Visual Studio 2017 with C++ Desktop development package
- CMake for Windows
- Git for Windows
- For Python support, install Python3, for example, Anaconda, and add Python to your PATH.
Clone the project as explained for Docker.
Open a windows command prompt and navigate into the new DPsim folder.
Generate a Visual Studio project with CMake and use it to build the project
$ mkdir build
$ cd build
$ cmake -G "Visual Studio 15 2017 Win64" ..
Open Visual Studio and load the Visual Studio project from the build directory within the DPsim folder.
You can either build the project from within Visual Studio or from the command line by running the following command in the windows command prompt
$ cmake --build .
To install the Python package use Visual Studio and the Release configuration to build the DPsim Python module and then build the INSTALL project.
CMake for macOS
Make sure that the required dependecies are installed
$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
$ brew install gcc9 git cmake graphviz python3 gsl eigen spdlog
$ sudo pip3 install numpy
Clone the source as explained for the Docker setup.
Compile
$ mkdir build
$ cmake ..
$ make -j$(sysctl -n hw.ncpu)
To install the generated Python module to your system
$ sudo make install
Python Package for pypi
Follow the previous steps to set up the Docker container.
To build the Python package run
$ python3 setup.py bdist_wheel
Documentation
Python
Install Sphinx or use the Docker image.
Generate the Python documentation by running Sphinx via CMake:
$ mkdir -p build && cd build
$ cmake ..
$ make docs
The resulting documentation will be generated in Documentation/html/
.
C++
Install Doxygen or use the Docker image.
Generate the C++ documentation by running Doxygen via CMake:
$ mkdir -p build && cd build
$ cmake ..
$ make docs_cxx
The resulting documentation will be generated in Documentation/html/Cxx
.
2.2 - Install
DPsim is a Python module / C++ library for complex power system
simulation. As a Python module, the easiest way to get to know DPsim is
via Jupyter Notebooks.
Docker
First, you need to install Docker.
Then, you could either build a docker image by yourself as described in the build instructions or download a prepared image from Docker Hub as described in the following.
To start a Jupyter session, run a DPsim Docker container
$ docker run -p 8888:8888 sogno/dpsim
And access the session by opening the following link: http://localhost:8888/lab?token=3adaa57df44cea75e60c0169e1b2a98ae8f7de130481b5bc
Python
Currently, the pypi packages are not maintained. Until we have updated the packages, please use the docker installation.
Prerequisites
First, you need to make sure that Python is installed and your version is compatible.
An easy way to install Python and all required packages is the Anaconda distribution.
To get started, install the latest installer for Python 3.x from the downloads section.
Then, run the Anaconda Prompt and create a new conda environment:
$ conda create -n dpsim python=3.6
After creating the environment you need to make sure that it is activated.
The current environment is displayed at the beginning of the command line in brackets.
It should read "(dpsim)…".
In case it is not activated, run:
$ activate dpsim
Pip Package Installation
Then, DPsim can be installed as a Python module:
$ pip install dpsim
From Source
To build and install DPsim from the source files, please refer to the build section.
2.3 - Real-Time
This page describes recommended techniques to optimize the host operating system for real-time execution of DPsim.
In principle, real-time execution is supported on all platforms.
However, we recommend to use an optimized Linux installation.
Operating System and Kernel
For minimum latency several kernel and driver settings can be optimized.
To get started, we recommend the Redhat Real-time Tuning Guide.
A PREEMPT_RT patched Linux kernel is recommended.
Precompiled kernels for Fedora can be found here: http://ccrma.stanford.edu/planetccrma/software/
Use the tuned tool for improving general real-time performance.
Please adjust the setting isolated_cpucores
according to your hardware and enable the realtime
profile as follows:
$ dnf install tuned-profiles-realtime
$ echo "realtime" > /etc/tuned/active_profile
$ echo "isolated_cpucores=6-7" >> /etc/tuned/realtime-variables.conf
$ systemctl enable tuned && systemctl start tuned
$ systemctl reboot
Running a real-time simulation
As a reference, real-time simulation examples are provided in the Examples/Cxx folder of the DPsim repository.
In order to run a real-time simulation, the simulation process must be started in a special way in order to change the execution priority, scheduler and CPU affinity.
For this purpose the chrt
and taskset
commands are used.
In the following example, we pin the execution of the simulation to CPU cores 6-7 which have been reserved previously by the tuned real-time profile (see above).
$ taskset --all-tasks --cpu-list 6-7 \
$ chrt --fifo 99 \
$ Examples/Cxx/RT_DP_CS_R_1
More details:
Recommended Hardware
Some proposals for the selection of appropriate server hardware:
- Server-grade CPU, e.g. Intel Xeon. A multi-core system enables true parallel execution of several decoupled systems
- Server-grade network cards, e.g. Intel PRO/1000. These allow offloading of UDP checksumming to the hardware
3 - Concepts
The book introduces the reader to the general concepts implemented in DPsim, a dynamic phasor (DP) real-time simulator, as well as the physical models of the power system components that are used in simulations.
The first chapters give an overview of dynamic phasors and nodal analysis which are the two pillars of the main solver implemented in DPsim.
The second part describes in detail what are the physical equations for each model and how they are transformed and implemented for dynamic phasor simulations and other domains that are also supported by DPsim.
In order to be able to run a dynamic simulation, DPsim also includes a loadflow solver to compute the initial state of the network if it is not included in the network data.
Besides DP simulations, DPsim also comes with EMT models for some components which are used as reference for testing the DP models.
3.1 - Dynamic Phasors
In the power systems community, dynamic phasors were initially introduced for power electronics analysis Sanders1991 as a more general approach than state-space averaging.
They were used to construct efficient models for the dynamics of switching gate phenomena with a high level of detail as shown in Mattavelli1999.
A few years later, dynamic phasors were also employed for power system simulation as described in Demiray2008.
In Strunz2006 the authors combine the dynamic phasor approach with the Electromagnetic Transients Program (EMTP) simulator concept which includes Modified Nodal Analysis (MNA).
Further research topics include fault and stability analysis under unbalanced conditions as presented in Stankovic2000 and also rotating machine models have been developed in dynamic phasors Zhang 2007.
Bandpass Signals and Baseband Representation
Although here, dynamic phasors are presented as a power system modelling tool, it should be noted that the concept is also known in other domains, for example, microwave and communications engineering Maas2003, Suarez2009, Haykin2009, Proakis2001.
In these domains, the approach is often denoted as base band representation or complex envelope.
Another common term coming from power electrical engineering is shifted frequency analysis (SFA).
In the following, the general approach of dynamic phasors for power system simulation is explained starting from the idea of bandpass signals.
This is because the 50 Hz or 60 Hz fundamental and small deviations from it can be seen as such a bandpass signal.
Futhermore, higher frequencies, for example, generated by power electronics can be modelled in a similar way.
3.2 - Nodal Analysis
A circuit with $b$ branches has $2b$ unknowns since there are $b$ voltages and $b$ currents.
Hence, $2b$ linear independent equations are required to solve the circuit.
If the circuit has $n$ nodes and $b$ branches, it has
- Kirchoff’s current law (KCL) equations
- Kirchoff’s voltage law (KVL) equations
- Characteristic equations (Ohm’s Law)
There are only $n-1$ KCLs since the nth equation is a linear combination of the remaining $n-1$.
At the same time, it can be demonstrated that if we can imagine a very high number of closed paths in the network, only $b-n+1$ are able to provide independent KVLs.
Finally there are $b$ characteristic equations, describing the behavior of the branch, making a total of $2b$ linear independent equations.
The nodal analysis method reduces the number of equations that need to be solved simultaneously.
$n-1$ voltage variables are defined and solved, writing $n-1$ KCL based equations.
A circuit can be solved using Nodal Analysis as follows
- Select a reference node (mathematical ground) and number the remaining $n-1$ nodes, that are the independent voltage variables
- Represent every branch current $i$ as a function of node voltage variables $v$ with the general expression $i = g(v)$
- Write $n-1$ KCL based equations in terms of node voltage variable.
The resulting equations can be written in matrix form and have to be solved for $v$.
$$\boldsymbol{Y} \boldsymbol{v} = \boldsymbol{i}$$
3.3 - Powerflow
The power flow problem is about the calculation of voltage magnitudes and angles for one set of buses.
The solution is obtained from a given set of voltage magnitudes and power levels for a specific model of the network configuration.
The power flow solution exhibits the voltages and angles at all buses and real and reactive flows can be deduced from the same.
Power System Model
Power systems are modeled as a network of buses (nodes) and branches (lines).
To a network bus, components such a generator, load, and transmission substation can be connected.
Each bus in the network is fully described by the following four electrical quantities:
- $\vert V_{k} \vert$: the voltage magnitude
- $\theta_{k}$: the voltage phase angle
- $P_{k}$: the active power
- $Q_{k}$: the reactive power
There are three types of networks buses: VD bus, PV bus and PQ bus.
Depending on the type of the bus, two of the four electrical quantities are specified as shown in the table below.
Bus Type | Known | Unknown |
---|
$VD$ | $\vert V_{k} \vert, \theta_{k}$ | $P_{k}, Q_{k}$ |
$PV$ | $P_{k}, \vert V_{k} \vert$ | $Q_{k}, \theta_{k}$ |
$PQ$ | $P_{k}, Q_{k}$ | $\vert V_{k} \vert, \theta_{k}$ |
Single Phase Power Flow Problem
The power flow problem can be expressed by the goal to bring a mismatch function $\vec{f}$ to zero.
The value of the mismatch function depends on a solution vector $\vec{x}$:
$$ \vec{f}(\vec{x}) = 0 $$
As $\vec{f}(\vec{x})$ will be nonlinear, the equation system will be solved with Newton-Raphson:
$$-\textbf{J}(\vec{x}) \Delta \vec{x} = \vec{f} (\vec{x})$$
where $\Delta \vec{x}$ is the correction of the solution vector and $\textbf{J}(\vec{x})$ is the Jacobian matrix.
The solution vector $\vec{x}$ represents the voltage $\vec{V}$ by polar or cartesian quantities.
The mismatch function $\vec{f}$ will either represent the power mismatch $\Delta \vec{S}$ in terms of
$$\left [ \begin{array}{c} \Delta \vec{P} \ \Delta \vec{Q} \end{array} \right ]$$
or the current mismatch $\Delta \vec{I}$ in terms of
$$\left [ \begin{array}{c} \Delta \vec{I_{real}} \ \Delta \vec{I_{imag}} \end{array} \right ]$$
where the vectors split the complex quantities into real and imaginary parts.
Futhermore, the solution vector $\vec{x}$ will represent $\vec{V}$ either by polar coordinates
$$\left [ \begin{array}{c} \vec{\delta} \ \vert \vec{V} \vert \end{array} \right ]$$
or rectangular coordinates
$$\left [ \begin{array}{c} \vec{V_{real}} \ \vec{V_{imag}} \end{array} \right ]$$
This results in four different formulations of the powerflow problem:
- with power mismatch function and polar coordinates
- with power mismatch function and rectangular coordinates
- with current mismatch function and polar coordinates
- with current mismatch function and rectangular coordinates
To solve the problem using NR, we need to formulate $\textbf{J} (\vec{x})$ and $\vec{f} (\vec{x})$ for each powerflow problem formulation.
Powerflow Problem with Power Mismatch Function and Polar Coordinates
The injected power at a node $k$ is given by:
$$S_{k} = V_{k} I _{k}^{*}$$
The current injection into any bus $k$ may be expressed as:
$$
I_{k} = \sum_{j=1}^{N} Y_{kj} V_{j}
$$
Substitution yields:
$$\begin{align}
S_{k} &= V_{k} \left ( \sum_{j=1}^{N} Y_{kj} V_{j} \right )^{*} \nonumber \\
&= V_{k} \sum_{j=1}^{N} Y_{kj}^{*} V_{j} ^{*} \nonumber
\end{align}$$
We may define $G_{kj}$ and $B_{kj}$ as the real and imaginary parts of the admittance matrix element $Y_{kj}$ respectively, so that $Y_{kj} = G_{kj} + jB_{kj}$.
Then we may rewrite the last equation:
$$\begin{align}
S_{k} &= V_{k} \sum_{j=1}^{N} Y_{kj}^{*} V_{j}^{*} \nonumber \\
&= \vert V_{k} \vert \angle \theta_{k} \sum_{j=1}^{N} (G_{kj} + jB_{kj})^{*} ( \vert V_{j} \vert \angle \theta_{j})^{*} \nonumber \\
&= \vert V_{k} \vert \angle \theta_{k} \sum_{j=1}^{N} (G_{kj} - jB_{kj}) ( \vert V_{j} \vert \angle - \theta_{j}) \nonumber \\
&= \sum_{j=1} ^{N} \vert V_{k} \vert \angle \theta_{k} ( \vert V_{j} \vert \angle - \theta_{j}) (G_{kj} - jB_{kj}) \nonumber \\
&= \sum_{j=1} ^{N} \left ( \vert V_{k} \vert \vert V_{j} \vert \angle (\theta_{k} - \theta_{j}) \right ) (G_{kj} - jB_{kj}) \nonumber \\
&= \sum_{j=1} ^{N} \vert V_{k} \vert \vert V_{j} \vert \left ( cos(\theta_{k} - \theta_{j}) + jsin(\theta_{k} - \theta_{j}) \right ) (G_{kj} - jB_{kj})
\end{align}$$
If we now perform the algebraic multiplication of the two terms inside the parentheses, and collect real and imaginary parts, and recall that $S_{k} = P_{k} + jQ_{k}$, we can express (1) as two equations: one for the real part, $P_{k}$, and one for the imaginary part, $Q_{k}$, according to:
$$\begin{align}
{P}_{k} = \sum_{j=1}^{N} \vert V_{k} \vert \vert V_{j} \vert \left ( G_{kj}cos(\theta_{k} - \theta_{j}) + B_{kj} sin(\theta_{k} - \theta_{j}) \right ) \\
{Q}_{k} = \sum_{j=1}^{N} \vert V_{k} \vert \vert V_{j} \vert \left ( G_{kj}sin(\theta_{k} - \theta_{j}) - B_{kj} cos(\theta_{k} - \theta_{j}) \right )
\end{align}$$
These equations are called the power flow equations, and they form the fundamental building block from which we solve the power flow problem.
We consider a power system network having $N$ buses. We assume one VD bus, $N_{PV}-1$ PV buses and $N-N_{PV}$ PQ buses.
We assume that the VD bus is numbered bus $1$, the PV buses are numbered $2,…,N_{PV}$, and the PQ buses are numbered $N_{PV}+1,…,N$.
We define the vector of unknown as the composite vector of unknown angles $\vec{\theta}$ and voltage magnitudes $\vert \vec{V} \vert$:
$$\begin{align}
\vec{x} = \left[ \begin{array}{c} \vec{\theta} \\ \vert \vec{V} \vert \\ \end{array} \right ]
= \left[ \begin{array}{c} \theta_{2} \\ \theta_{3} \\ \vdots \\ \theta_{N} \\ \vert V_{N_{PV+1}} \vert \\ \vert V_{N_{PV+2}} \vert \\ \vdots \\ \vert V_{N} \vert \end{array} \right]
\end{align}$$
The right-hand sides of equations (2) and (3) depend on the elements of the unknown vector $\vec{x}$.
Expressing this dependency more explicitly, we rewrite these equations as:
$$\begin{align}
P_{k} = P_{k} (\vec{x}) \Rightarrow P_{k}(\vec{x}) - P_{k} &= 0 \quad \quad k = 2,...,N \\
Q_{k} = Q_{k} (\vec{x}) \Rightarrow Q_{k} (\vec{x}) - Q_{k} &= 0 \quad \quad k = N_{PV}+1,...,N
\end{align}$$
We now define the mismatch vector $\vec{f} (\vec{x})$ as:
$$\begin{align}
\vec{f} (\vec{x}) = \left [ \begin{array}{c} f_{1}(\vec{x}) \\ \vdots \\ f_{N-1}(\vec{x}) \\ ------ \\ f_{N}(\vec{x}) \\ \vdots \\ f_{2N-N_{PV} -1}(\vec{x}) \end{array} \right ]
= \left [ \begin{array}{c} P_{2}(\vec{x}) - P_{2} \\ \vdots \\ P_{N}(\vec{x}) - P_{N} \\ --------- \\ Q_{N_{PV}+1}(\vec{x}) - Q_{N_{PV}+1} \\ \vdots \\ Q_{N}(\vec{x}) - Q_{N} \end{array} \right]
= \left [ \begin{array}{c} \Delta P_{2} \\ \vdots \\ \Delta P_{N} \\ ------ \\ \Delta Q_{N_{PV}+1} \\ \vdots \\ \Delta Q_{N} \end{array} \right ]
= \vec{0}
\end{align}$$
That is a system of nonlinear equations.
This nonlinearity comes from the fact that $P_{k}$ and $Q_{k}$ have terms containing products of some of the unknowns and also terms containing trigonometric functions of some the unknowns.
As discussed in the previous section, the power flow problem will be solved using the Newton-Raphson method. Here, the Jacobian matrix is obtained by taking all first-order partial derivates of the power mismatch functions with respect to the voltage angles $\theta_{k}$ and magnitudes $\vert V_{k} \vert$ as:
$$\begin{align}
J_{jk}^{P \theta} &= \frac{\partial P_{j} (\vec{x} ) } {\partial \theta_{k}} = \vert V_{j} \vert \vert V_{k} \vert \left ( G_{jk} sin(\theta_{j} - \theta_{k}) - B_{jk} cos(\theta_{j} - \theta_{k} ) \right ) \\
J_{jj}^{P \theta} &= \frac{\partial P_{j}(\vec{x})}{\partial \theta_{j}} = -Q_{j} (\vec{x} ) - B_{jj} \vert V_{j} \vert ^{2} \\
J_{jk}^{Q \theta} &= \frac{\partial Q_{j}(\vec{x})}{\partial \theta_{k}} = - \vert V_{j} \vert \vert V_{k} \vert \left ( G_{jk} cos(\theta_{j} - \theta_{k}) + B_{jk} sin(\theta_{j} - \theta_{k}) \right ) \\
J_{jj}^{Q \theta} &= \frac{\partial Q_{j}(\vec{x})}{\partial \theta_{k}} = P_{j} (\vec{x} ) - G_{jj} \vert V_{j} \vert ^{2} \\
J_{jk}^{PV} &= \frac{\partial P_{j} (\vec{x} ) } {\partial \vert V_{k} \vert } = \vert V_{j} \vert \left ( G_{jk} cos(\theta_{j} - \theta_{k}) + B_{jk} sin(\theta_{j} - \theta_{k}) \right ) \\
J_{jj}^{PV} &= \frac{\partial P_{j}(\vec{x})}{\partial \vert V_{j} \vert } = \frac{P_{j} (\vec{x} )}{\vert V_{j} \vert} + G_{jj} \vert V_{j} \vert \\
J_{jk}^{QV} &= \frac{\partial Q_{j} (\vec{x} ) } {\partial \vert V_{k} \vert } = \vert V_{j} \vert \left ( G_{jk} sin(\theta_{j} - \theta_{k}) + B_{jk} cos(\theta_{j} - \theta_{k}) \right ) \\
J_{jj}^{QV} &= \frac{\partial Q_{j}(\vec{x})}{\partial \vert V_{j} \vert } = \frac{Q_{j} (\vec{x} )}{\vert V_{j} \vert} - B_{jj} \vert V_{j} \vert \\
\end{align}$$
The linear system of equations that is solved in every Newton iteration can be written in matrix form as follows:
$$\begin{align}
-\left [ \begin{array}{cccccc}
\frac{\partial \Delta P_{2} }{\partial \theta_{2}} & \cdots & \frac{\partial \Delta P_{2} }{\partial \theta_{N}} &
\frac{\partial \Delta P_{2} }{\partial \vert V_{N_{G+1}} \vert} & \cdots & \frac{\partial \Delta P_{2} }{\partial \vert V_{N} \vert} \\
\vdots & \ddots & \vdots & \vdots & \ddots & \vdots \\
\frac{\partial \Delta P_{N} }{\partial \theta_{2}} & \cdots & \frac{\partial \Delta P_{N}}{\partial \theta_{N}} &
\frac{\partial \Delta P_{N}}{\partial \vert V_{N_{G+1}} \vert } & \cdots & \frac{\partial \Delta P_{N}}{\partial \vert V_{N} \vert} \\
\frac{\partial \Delta Q_{N_{G+1}} }{\partial \theta_{2}} & \cdots & \frac{\partial \Delta Q_{N_{G+1}} }{\partial \theta_{N}} &
\frac{\partial \Delta Q_{N_{G+1}} }{\partial \vert V_{N_{G+1}} \vert } & \cdots & \frac{\partial \Delta Q_{N_{G+1}} }{\partial \vert V_{N} \vert} \\
\vdots & \ddots & \vdots & \vdots & \ddots & \vdots \\
\frac{\partial \Delta Q_{N}}{\partial \theta_{2}} & \cdots & \frac{\partial \Delta Q_{N}}{\partial \theta_{N}} &
\frac{\partial \Delta Q_{N}}{\partial \vert V_{N_{G+1}} \vert } & \cdots & \frac{\partial \Delta Q_{N}}{\partial \vert V_{N} \vert}
\end{array} \right ]
\left [ \begin{array}{c} \Delta \theta_{2} \\ \vdots \\ \Delta \theta_{N} \\ \Delta \vert V_{N_{G+1}} \vert \\ \vdots \\ \Delta \vert V_{N} \vert \end{array} \right ]
= \left [ \begin{array}{c} \Delta P_{2} \\ \vdots \\ \Delta P_{N} \\ \Delta Q_{N_{G+1}} \\ \vdots \\ \Delta Q_{N} \end{array} \right ]
\end{align}$$
Solution of the Problem
The solution update formula is given by:
$$\begin{align}
\vec{x}^{(i+1)} = \vec{x}^{(i)} + \Delta \vec{x}^{(i)} = \vec{x}^{(i)} - \textbf{J}^{-1} \vec{f} (\vec{x}^{(i)})
\end{align}$$
To sum up, the NR algorithm, for application to the power flow problem is:
- Set the iteration counter to $i=1$. Use the initial solution $V_{i} = 1 \angle 0^{\circ}$
- Compute the mismatch vector $\vec{f}({\vec{x}})$ using the power flow equations
- Perform the following stopping criterion tests:
- If $\vert \Delta P_{i} \vert < \epsilon_{P}$ for all type PQ and PV buses and
- If $\vert \Delta Q_{i} \vert < \epsilon_{Q}$ for all type PQ
- Then go to step 6
- Otherwise, go to step 4.
- Evaluate the Jacobian matrix $\textbf{J}^{(i)}$ and compute $\Delta \vec{x}^{(i)}$.
- Compute the update solution vector $\vec{x}^{(i+1)}$. Return to step 3.
- Stop.
4 - Development
How to extend DPsim.
Environment
We recommend the following development tools:
Please follow the build instructions to checkout your code and install the basic dependencies and tools.
4.1 - Debugging
Mixed Python C++ Debugging
Prerequisites
Your vscode launch.json should have two configurations, one to launch the python process and one to attach gdb:
{
"version": "0.2.0",
"configurations": [
{
"name": "Python: Current File",
"type": "python",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal",
"stopOnEntry": true,
"env": {"PYTHONPATH": "${workspaceFolder}/build${pathSeparator}${env:PYTHONPATH}"}
},
{
"name": "(gdb) Attach",
"type": "cppdbg",
"request": "attach",
"program": "/usr/bin/python",
"processId": "${command:pickProcess}",
"MIMode": "gdb",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
]
}
]
}
The python debugger will stop on entry (“stopOnEntry”: true).
Make sure to adapt your PYTHONPATH variable if necessary.
The C++ code has to be build in debug mode
cmake .. -DCMAKE_BUILD_TYPE=Debug
Attaching C++ Debugger
- open the python example to be debugged
- go to the debug menu and select / run the “Python: Current File” configuration
- the python debugger should stop at entry
- set C++ breakpoints
- go to the debug menu and run the “(gdb) Attach” configuration
- select a process… choose the python process with the “—adapter-access-token” part
- you can view the whole description when you hover over the process with the mouse
- press play to continue Python debugging… the c++ debugger will stop at the next breakpoint
You can automate this by using the vscode extension “Python C++ Debugger” and by adding this configuration to the launch.json above:
{
"name": "Python C++ Debugger",
"type": "pythoncpp",
"request": "launch",
"pythonConfig": "custom",
"pythonLaunchName": "Python: Current File",
"cppConfig": "default (gdb) Attach"
}
This will automatically run both debuggers and select the current process.
It can take a while before the debugger hits the C++ breakpoints.
C++ Debugging
Use the following launch.json for vscode and set the program path:
{
"version": "0.2.0",
"configurations": [
{
"name": "(gdb) Launch",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/dpsim/build/Examples/Cxx/example",
"args": [],
"stopAtEntry": true,
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
]
}
]
}
4.2 - Guidelines
This is a summary of general guidelines for the development of DPsim.
Scaling of Voltages and Currents
Voltage quantities are expressed either as phase-to-phase RMS values (denominated as RMS3PH
) or as phase-to-ground peak values (denominated as PEAK1PH
):
- Initialisation quantities (e.g.
initialSingleVoltage
of SimPowerComp
) as RMS3PH
values - Simulation quantities in both
SP
and DP
domain (e.g. mIntfVoltage
of DP::Ph1::PiLine
) as RMS3PH values
- Simulation quantities in the
EMT
domain (e.g. mIntfVoltage
of EMT::Ph3::Transformer
) as PEAK1PH
values
Current quantities are expressed either as RMS
or as PEAK
values:
- Simulation quantities in both
SP
and DP
domain (e.g. mIntfCurrent
of DP::Ph1::PiLine
) as RMS
values - Simulation quantities in the
EMT
domain (e.g. mIntfCurrent
of EMT::Ph3::Transformer
) as PEAK
values
Logging
Debug or trace should be the default log level for information that might be nice to have but not necessary for every simulation case.
Calls to the logger that might occur during simulation must use spdlog macros, like SPDLOG_LOGGER_INFO.
Creating New Releases
Although DPsim currently does not have any conventions on versioning, the periodic creation of
new versions can help to mark significant changes and to analyze new portions of code using tools like SonarCloud.
A new version of DPsim has to be indicated as follows:
- Create a new tag with an increased version number
- Update setup.cfg
- Update CMakeLists.txt
- Update sonar-project.properties
Due to the creation of a new tag, a new PyPi package will be deployed automatically.
To release an updated Docker image, the container workflow needs to be triggered manually.
5 - Models
Mathematical description of the models implemented in DPsim.
The following models are currently available:
- Dynamic phasors
- inductor, capacitor, resistor
- current and voltage source
- load (PQ and Z type)
- pi-line
- transmission line (Bergeron)
- synchronous generator dq-frame full order (Kundur, Krause)
- inverter averaged
- inverter with harmonics (comparable to switched model)
- switch
- EMT
- inductor, capacitor, resistor
- current and voltage source
- load (Z type)
- pi-line
- transmission line (Bergeron)
- synchronous generator dq-frame full order (Kundur, Krause)
- inverter averaged
- switch
5.1 - Transformer
The transformer model is composed of an RL-segment and an ideal transformer.
The single line diagram is depicted in the figure below.
If node reduction is not applied, two virtual nodes are created to stamp this model into the system matrix.
Furthermore, the ideal transformer has an additional equation, which requires an extension of the system matrix.
The complete matrix stamp for the ideal transformer is
$$\begin{array}{c|c c c}
~ & j & k & l \cr
\hline
j & & & -1 \cr
k & & & T \cr
l & 1 & -T & 0
\end{array}
\begin{pmatrix}
v_j \cr
v_k \cr
i_{l} \cr
\end{pmatrix}
=
\begin{pmatrix}
\cr
\cr
0\cr
\end{pmatrix} $$
The variable $j$ denotes the high voltage node while $k$ is the low voltage node.
$l$ indicates the inserted row and column to accommodate the relation between the two voltages at the ends of the transformer.
The transformer ratio is defined as $T = V_{j} / V_{k}$.
A phase shift can be introduced if $T$ is considered as a complex number.
5.2 - Branches
RX-Line
PI-Line
5.3 - Induction Machine
5.4 - RLC-Elements
EMT Equations and Modified Nodal Analysis
Inductance
An inductance is described by
$$
v_j(t) - v_k(t) = v_L(t) = L \frac{\mathrm{d} i_L(t)}{\mathrm{d}t}
$$
Integration results in an equation to compute the current at time $t$ from a previous state at $t - \Delta t$.
$$
i_L(t) = i_L(t - \Delta t) + \frac{1}{L} \ \int_{t - \Delta t}^{t} v_L(\tau) \ \mathrm{d} \tau
$$
There are various methods to discretize this equation in order to solve it numerically.
The trapezoidal rule, an implicit second-order method, is commonly applied for circuit simulation:
$$
\int_{t - \Delta t}^{t} f(\tau) \ \mathrm{d} \tau \approx \frac{\Delta t}{2}(f(t) + f(t - \Delta t))
$$
Applying the trapezoidal rule to leads to
$$
i_L(t) = i_L(t - \Delta t) + \frac{\Delta t}{2L}(v_L(t) + v_L(t - \Delta t))
$$
This can be rewritten in terms of an equivalent conductance and current source and the number of time steps $k$ with size $\Delta t$.
$$
i_L(k) = g_L v_L(k) + i_{L,equiv}(k-1)
$$
$$
i_{L,equiv}(k-1) = i_L(k-1) + \frac{\Delta t}{2L} v_L(k-1)
$$
$$
g_L = \frac{\Delta t}{2L}
$$
Hence, components described by differential equations are transformed into a DC equivalent circuit as depicted in the figure below.
Capacitance
The same procedure can be applied to a capacitance.
Integration on both side yields
$$
i_C(t) = C \frac{\mathrm{d}}{\mathrm{d}t} \ v_C(t)
$$
$$
v_C(t) = v_C(t - \Delta t) + \frac{1}{C} \int_{t - \Delta t}^t i_C(\tau) \mathrm{d} \tau
$$
Finally, the equivalent circuit is described by a current source and a conductance.
$$
i_{C}(k) = g_{C} v_C(k) + i_{C,equiv}(k-1)
$$
$$
i_{C,equiv}(k-1) = -i_{C}(k-1) - g_C v_C(k-1)
$$
$$
g_{C} = \frac{2C}{\Delta t}
$$
This equation set is visualized in the figure below.
Hence, the vector of unknowns $\bm{x}$ and the source vector $\bm{b}$ become time dependent and this leads to the system description:
$$
\bm{A} \bm{x}(t) = \bm{b}(t)
$$
To simulate the transient behavior of circuits, this linear equation has to be solved repeatedly.
As long as the system topology and the time step is fixed, the system matrix is constant.
Extension with Dynamic Phasors
The dynamic phasor concept can be integrated with nodal analysis.
The overall procedure does not change but the system equations are rewritten using complex numbers and all variables need to be expressed in terms of dynamic phasors.
Therefore, the resistive companion representations of inductances and capacitances have to be adapted as well.
Inductance
In dynamic phasors the integration of the inductance equation yields
$$\begin{align}
\langle v_L \rangle(t) &= \Big \langle L \frac{\mathrm{d} i_L}{\mathrm{d}t} \Big \rangle(t) \nonumber \\
&= L \frac{\mathrm{d}}{dt} \langle i_L \rangle(t) + j \omega L \ \langle i_L \rangle(t)
\end{align}$$
$$
\langle i_L \rangle(t) = \langle i_L \rangle(t - \Delta t) + \int_{t - \Delta t}^t \frac{1}{L} \langle v_L \rangle(\tau) - j \omega \ \langle i_L \rangle(\tau) \mathrm{d} \tau
$$
Applying the trapezoidal method leads to the finite difference equation:
$$
\begin{split}
\langle i_L \rangle(k) = \langle i_L \rangle(k-1) + \frac{\Delta t}{2} \bigg[ \frac{1}{L} (\langle v_L \rangle(k) + \langle v_L \rangle(k-1))
- j \omega (\langle i_L \rangle(t) + \langle i_L \rangle(k-1) \bigg]
\end{split}
$$
Solving this for $\langle i_L \rangle(k)$ results in the \ac{DP} equivalent circuit model:
$$
\langle i_L \rangle(k) = \frac{a - jab}{1 + b^2} \langle v_L \rangle(k) + \langle i_{L,equiv} \rangle(k-1)
$$
with
$$
a = \frac{\Delta t}{2L}, \qquad b = \frac{\Delta t \omega}{2}
$$
$$
\langle i_{L,equiv} \rangle(k-1) = \frac{1 - b^2 - j2b}{1 + b^2} \langle i_L \rangle(k-1) + \frac{a - jab}{1 + b^2} \langle v_L \rangle(k-1)
$$
Capacitance
Similarly, a capacitance is described by as follows
$$
\langle i_C \rangle(k) = C \ \frac{\mathrm{d} \langle v_C \rangle}{\mathrm{d} t} + j \omega C \ \langle v_C \rangle(t)
$$
$$
v_C(t) = v_C(t- \Delta t) + \int_{t- \Delta t}^{t} \frac{1}{C} \ i_C(\tau) -j \omega \ v_C(\tau) \ \mathrm{d} \tau
$$
Applying the trapezoidal rule for the capacitance equation leads to the finite difference equation:
$$\begin{split}
\langle v_C \rangle(k) = \langle v_C \rangle(k-1)
+ \frac{\Delta t}{2} \bigg[ \frac{1}{C} \ \langle i_C \rangle(k) - j \omega \ \langle v_C \rangle(k) \\
+ \frac{1}{C} \ \langle i_C \rangle(k-1) - j \omega \ \langle v_C \rangle(k-1) \bigg]
\end{split}$$
The DP model for the capacitance is defined by
$$
\langle i_C \rangle(k) = \frac{1+jb}{a} \ \langle v_C \rangle(k) + \langle i_{C,equiv} \rangle(k-1)
$$
with
$$
a = \frac{\Delta t}{2C}, \qquad
b = \frac{\Delta t \omega}{2}
$$
$$
\langle i_{C,equiv} \rangle(k-1) = - \frac{1-jb}{a} \ \langle v_C \rangle(k-1) - \langle i_C \rangle(k-1)
$$
RL-series element
In dynamic phasors the integration of the inductance equation yields
$$
\langle v \rangle(t) = L \frac{\mathrm{d}}{dt} \langle i \rangle(t) + j \omega L \ \langle i \rangle(t) + R \ \langle i \rangle(t)
$$
$$
\langle i \rangle(t) = \langle i \rangle(t - \Delta t) + \int_{t - \Delta t}^t \frac{1}{L} \langle v \rangle(\tau) - j \omega \ \langle i \rangle(\tau) - \frac{R}{L} \ \langle i \rangle(\tau) \mathrm{d} \tau
$$
Applying the trapezoidal method leads to the finite difference equation:
$$
\begin{split}
\langle i \rangle(k) = \langle i \rangle(k-1) + \frac{\Delta t}{2} \bigg[ \frac{1}{L} (\langle v \rangle(k) + \langle v \rangle(k-1))
- \left( j \omega + \frac{R}{L} \right) (\langle i \rangle(k) + \langle i \rangle(k-1)) \bigg]
\end{split}
$$
Solving this for $\langle i \rangle(k)$ results in the \ac{DP} equivalent circuit model:
$$
\langle i \rangle(k) = \frac{a + Ra^2 - jab}{(1+Ra)^2 + b^2} \langle v \rangle(k) + \langle i_{equiv} \rangle(k-1)
$$
with
$$
a = \frac{\Delta t}{2L}, \qquad b = \frac{\Delta t \omega}{2}
$$
$$
\langle i_{equiv} \rangle(k-1) = \frac{1 - b^2 - j2b + 2Ra + (Ra)^2 - j2Rab}{(1+Ra^2) + b^2} \langle i \rangle(k-1) + \frac{a + Ra^2 - jab}{(1+Ra)^2 + b^2} \langle v \rangle(k-1)
$$
5.5 - Synchronous Generator
Two different synchronous machine models are currently available:
- the full order dq0 reference frame model (EMT, DP) [Kundur, Power system stability and control, 1994]
- and the much simpler transient stability model (DP) [Eremia, Handbook of Electrical Power System Dynamics, 2003]
The machine model is interfaced to the nodal analysis network solver through a current source, which only affects the source vector and not the system matrix Wang2010.
Basic Equations
The equations of the stator and rotor voltages are
$$\begin{align}
\mathbf{v}_{abcs} &= \mathbf{R}_s \mathbf{i}_{abcs} + \frac{d}{dt} \boldsymbol{\lambda}_{abcs} \\
\mathbf{v}_{dqr} &= \mathbf{R}_r \mathbf{i}_{dqr} + \frac{d}{dt} \boldsymbol{\lambda}_{dqr}
\end{align}$$
where
$$\begin{align}
\mathbf{v}_{abcs} &=
\begin{pmatrix}
v_{as} & v_{bs} & v_{cs}
\end{pmatrix}^T \\
%
\mathbf{v}_{dqr} &=
\begin{pmatrix}
v_{fd} & v_{kd} & v_{kq1} & v_{kq2}
\end{pmatrix}^T \\
%
\mathbf{i}_{abcs} &=
\begin{pmatrix}
i_{as} & i_{bs} & i_{cs}
\end{pmatrix}^T \\
%
\mathbf{i}_{dqr} &=
\begin{pmatrix}
i_{fd} & i_{kd} & i_{kq1} & i_{kq2}
\end{pmatrix}^T \\
%
\boldsymbol{\lambda}_{abcs} &=
\begin{pmatrix}
\lambda_{as} & \lambda_{bs} & \lambda_{cs}
\end{pmatrix}^T \\
%
\boldsymbol{\lambda}_{dqr} &=
\begin{pmatrix}
\lambda_{fd} & \lambda_{kd} & \lambda_{kq1} & \lambda_{kq2}
\end{pmatrix}^T \\
%
\mathbf{R}_s &= diag
\begin{bmatrix}
R_s & R_s & R_s
\end{bmatrix} \\
%
\mathbf{R}_r &= diag
\begin{bmatrix}
R_{fd} & R_{kd} & R_{kq1} & R_{kq2}
\end{bmatrix}
\end{align}$$
The flux linkage equations are defined as
$$\begin{equation}
\begin{bmatrix}
\boldsymbol{\lambda}_{abcs} \\
\boldsymbol{\lambda}_{dqr}
\end{bmatrix}
=
\begin{bmatrix}
\mathbf{L}_s & \mathbf{L}_{rs} \\
{(\mathbf{L}_{rs})}^{T} & \mathbf{L}_r
\end{bmatrix}
\begin{bmatrix}
\mathbf{i}_{abcs} \\
\mathbf{i}_{dqr}
\end{bmatrix}
\end{equation}$$
The inductance matrices are varying with the rotor position $\theta_r$ which varies with time.
The mechanical equations are:
$$\begin{align}
\frac{d\theta_r}{dt} &= \omega_r \\
\frac{d\omega_r}{dt} &= \frac{P}{2J} (T_e-T_m)
\end{align}$$
$\theta_r$ is the rotor position, $\omega_r$ is the angular electrical speed, $P$ is the number of poles, $J$ is the moment of inertia, $T_m$ and $T_e$ are the mechanical and electrical torque, respectively.
Motor convention is used for all models.
dq0 Reference Frame 9th Order Model
For stator referred variables, the base quantities for per unit are chosen as follows:
- $v_{s base}$ peak value of rated line-to-neutral voltage in V
- $i_{s base}$ peak value of rated line current in A
- $f_{base}$ rated frequency in Hz
The synchronous generator equations in terms of per unit values in the rotor reference frame become:
$$\begin{equation}
\begin{bmatrix}
\mathbf{v}_{dq0s} \\
\mathbf{v}_{dqr}
\end{bmatrix}
=
\mathbf{R}_{sr}
\begin{bmatrix}
\mathbf{i}_{dq0s} \\
\mathbf{i}_{dqr}
\end{bmatrix}
+
\frac{d}{dt}
\begin{bmatrix}
\boldsymbol{\lambda}_{dq0s} \\
\boldsymbol{\lambda}_{dqr}
\end{bmatrix}
+ \omega_r
\begin{bmatrix}
\boldsymbol{\lambda}_{qds} \\
0
\end{bmatrix}
\end{equation}$$
where
$$\begin{align}
\mathbf{v}_{dq0s} &=
\begin{pmatrix}
v_{ds} & v_{qs} & v_{0s}
\end{pmatrix}^T \nonumber \\
%
\mathbf{i}_{dq0s} &=
\begin{pmatrix}
i_{ds} & i_{qs} & i_{0s}
\end{pmatrix}^T \nonumber \\
%
\boldsymbol{\lambda}_{dq0s} &=
\begin{pmatrix}
\lambda_{ds} & \lambda_{qs} & \lambda_{0s}
\end{pmatrix}^T \nonumber \\
%
\mathbf{R}_{sr} &= diag
\begin{bmatrix}
R_s & R_s & R_s & R_{fd} & R_{kd} & R_{kq1} & R_{kq2}
\end{bmatrix} \nonumber \\
%
\boldsymbol{\lambda}_{dqs} &=
\begin{pmatrix}
-\lambda_{qs} & \lambda_{ds} & 0
\end{pmatrix}^T.
\end{align}$$
The flux linkages are:
$$\begin{equation}
\begin{pmatrix}
\boldsymbol{\lambda}_{dq0s} \\
\boldsymbol{\lambda}_{dqr}
\end{pmatrix}
=
\begin{bmatrix}
\mathbf{L}_{dqss} & \mathbf{L}_{dqsr} \\
\mathbf{L}_{dqrs} & \mathbf{L}_{dqrr}
\end{bmatrix}
\begin{pmatrix}
\mathbf{i}_{dq0s} \\
\mathbf{i}_{dqr}
\end{pmatrix}
\end{equation}$$
where
$$\begin{align}
\mathbf{L}_{dqss} &=
\begin{bmatrix}
L_{d} & 0 & 0 \\
0 & L_{q} & 0 \\
0 & 0 & L_{ls}
\end{bmatrix} \nonumber \\
\mathbf{L}_{dqsr} &=
\begin{bmatrix}
L_{md} & L_{md} & 0 & 0 \\
0 & 0 & L_{mq} & L_{mq} \\
0 & 0 & 0 & 0
\end{bmatrix} \nonumber \\
\mathbf{L}_{dqrs} &=
\begin{bmatrix}
L_{md} & 0 & 0 \\
L_{md} & 0 & 0 \\
0 & L_{mq} & 0 \\
0 & L_{mq} & 0
\end{bmatrix} \nonumber \\
\mathbf{L}_{rr} &=
\begin{bmatrix}
L_{fd} & L_{md} & 0 & 0 \\
L_{md} & L_{kd} & 0 & 0 \\
0 & 0 & L_{kq1} & L_{mq} \\
0 & 0 & L_{mq} & L_{kq2}
\end{bmatrix} \nonumber \\
\end{align}$$
with
$$\begin{align}
L_{d} &= L_{ls} + L_{md} \nonumber \\
L_{q} &= L_{ls} + L_{mq} \nonumber \\
L_{fd} &= L_{lfd} + L_{md} \nonumber \\
L_{kd} &= L_{lkd} + L_{md} \nonumber \\
L_{kq1} &= L_{lkq1} + L_{mq} \nonumber \\
L_{kq2} &= L_{lkq2} + L_{mq}.
\end{align}$$
The mechanical equations in per unit become:
$$\begin{align}
T_e &= \lambda_{qs} i_{ds} - \lambda_{ds} i_{qs} \\
\frac{d \omega_r}{dt} &= \omega_r \\
\frac{1}{\omega_b} \frac{d \omega_r}{dt} &= \frac{1}{2H} (T_m - T_e).
\end{align}$$
For the simulation, fluxes are chosen as state variables.
To avoid the calculation of currents from fluxes using the inverse of the inductance matrix, the equation set needs to be solved for the fluxes analytically.
To simplify the calculations, dq axis magnetizing flux linkages are defined [Krause, Analysis of electric machinery and drive systems, 2002]:
$$\begin{align}
\lambda_{md} &= L_{md} \left( i_{ds} + i_{fd} + i_{kd} \right) \nonumber \\
\lambda_{mq} &= L_{mq} \left( i_{qs} + i_{kq1} + i_{kq2} \right)
\end{align}$$
Using the flux linkages results in a simpler equation set for the fluxes:
$$\begin{align}
\lambda_{ds} &= L_{ls} i_{ds} + L_{md} \left( i_{ds} + i_{fd} + i_{kd} \right) \nonumber \\
\lambda_{qs} &= L_{ls} i_{qs} + L_{mq} \left( i_{qs} + i_{kq1} + i_{kq2} \right) \nonumber \\
\lambda_{0s} &= L_{ls} i_{0s} \nonumber \\
\lambda_{fd} &= L_{ls} i_{fd} + L_{md} \left( i_{ds} + i_{fd} + i_{kd} \right) \nonumber \\
\lambda_{kd} &= L_{ls} i_{kd} + L_{md} \left( i_{ds} + i_{fd} + i_{kd} \right) \nonumber \\
\lambda_{kq1} &= L_{ls} i_{kq1} + L_{mq} \left( i_{qs} + i_{kq1} + i_{kq2} \right) \nonumber \\
\lambda_{kq2} &= L_{ls} i_{kq2} + L_{mq} \left( i_{qs} + i_{kq1} + i_{kq2} \right)
\end{align}$$
$$\begin{align}
\lambda_{ds} &= L_{ls} i_{ds} + \lambda_{md} \nonumber \\
\lambda_{qs} &= L_{ls} i_{qs} + \lambda_{mq} \nonumber \\
\lambda_{0s} &= L_{ls} i_{0s} \nonumber \\
\lambda_{fd} &= L_{lfd} i_{fd} + \lambda_{md} \nonumber \\
\lambda_{kd} &= L_{lkd} i_{kd} + \lambda_{md} \nonumber \\
\lambda_{kq1} &= L_{lkq1} i_{kq1} + \lambda_{mq} \nonumber \\
\lambda_{kq2} &= L_{lkq2} i_{kq2} + \lambda_{mq}
\end{align}$$
Dynamic Phasor Model
The fundamental dynamic phasors are similar to the dq0 quantities for symmetrical conditions since both yield DC quantities in a rotating reference frame.
The network abc dynamic phasor quantities can be converted to dq0 dynamic phasors by applying the symmetrical components transformation and a rotation.
The angle $\delta$ is the orientation of the dq0 reference frame relative to the abc frame.
$$\begin{align}
\langle i_{ds} \rangle_{0} &= \mathbf{Re} \left\{ \langle i_{p} \rangle_1 \ \mathrm{e}^{-j \delta} \right\} \nonumber \\
\langle i_{qs} \rangle_{0} &= \mathbf{Im} \left\{ \langle i_{p} \rangle_1 \ \mathrm{e}^{-j \delta} \right\} \nonumber \\
\langle i_{ds} \rangle_{2} &= \mathbf{Re} \left\{ \langle i_{n} \rangle_{1}^* \ \mathrm{e}^{-j \delta} \right\} \nonumber \\
\langle i_{qs} \rangle_{2} &= \mathbf{Im} \left\{ \langle i_{n} \rangle_{1}^* \ \mathrm{e}^{-j \delta} \right\} \nonumber \\
\langle i_{0s} \rangle_{1} &= \mathbf{Re} \left\{ \langle i_{z} \rangle_1 \right\}
\end{align}$$
The winding currents for positive and zero sequence components can be expressed as
$$\begin{align}
\langle i_{ds} \rangle_0 &= \frac{\langle \lambda_{ds} \rangle_0 - \langle \lambda_{md} \rangle_0 }{L_{ls}} \nonumber \\
\langle i_{qs} \rangle_0 &= \frac{\langle \lambda_{qs} \rangle_0 - \langle \lambda_{mq} \rangle_0}{L_{ls}} \nonumber \\
\langle i_{0s} \rangle_1 &= \frac{\langle \lambda_{0s} \rangle_1}{L_{ls}} \nonumber \\
\langle i_{fd} \rangle_0 &= \frac{\langle \lambda_{fd} \rangle_0 - \langle \lambda_{md} \rangle_0}{L_{lfd}} \nonumber \\
\langle i_{kd} \rangle_0 &= \frac{\langle \lambda_{kd} \rangle_0 - \langle \lambda_{md} \rangle_0}{L_{lkd}} \nonumber \\
\langle i_{kq1} \rangle_0 &= \frac{\langle \lambda_{kq1} \rangle_0 - \langle \lambda_{mq} \rangle_0}{L_{lkq1}} \nonumber \\
\langle i_{kq2} \rangle_0 &= \frac{\langle \lambda_{kq2} \rangle_0 - \langle \lambda_{mq} \rangle_0}{L_{lkq2}}.
\end{align}$$
$$\begin{align}
\frac{d}{dt} \langle \lambda_{ds} \rangle_0 &= \langle v_{ds} \rangle_0 + \langle \omega_r \rangle_0 \langle \lambda_{qs} \rangle_0 + \frac{R_s}{L_{ls}} \left( \langle \lambda_{md} \rangle_0 - \langle \lambda_{ds} \rangle_0 \right) \nonumber \\
\frac{d}{dt} \langle \lambda_{qs} \rangle_0 &= \langle v_{qs} \rangle_0 - \langle \omega_r \rangle_0 \langle \lambda_{ds} \rangle_0 + \frac{R_s}{L_{ls}} \left( \langle \lambda_{mq} \rangle_0 - \langle \lambda_{qs} \rangle_0 \right) \nonumber \\
\frac{d}{dt} \langle \lambda_{0s} \rangle_1 &= \langle v_{0s} \rangle_1 - \frac{R_s}{L_{ls}} \langle \lambda_{0s} \rangle_1 -j \omega_s \langle \lambda_{0s} \rangle_1 \nonumber \\
\frac{d}{dt} \langle \lambda_{fd} \rangle_0 &= \langle v_{fd} \rangle_0 + \frac{R_{fd}}{L_{lfd}} \left( \langle \lambda_{md} \rangle_0 - \langle \lambda_{fd} \rangle_0 \right) \nonumber \\
\frac{d}{dt} \langle \lambda_{kd} \rangle_0 &= \frac{R_{kd}}{L_{lkd}} \left( \langle \lambda_{md} \rangle_0 - \langle \lambda_{kd} \rangle_0 \right) \nonumber \\
\frac{d}{dt} \langle \lambda_{kq1} \rangle_0 &= \frac{R_{kq1}}{L_{lkq1}} \left( \langle \lambda_{mq} \rangle_0 - \langle \lambda_{kq1} \rangle_0 \right) \nonumber \\
\frac{d}{dt} \langle \lambda_{kq2} \rangle_0 &= \frac{R_{kq2}}{L_{lkq2}} \left( \langle \lambda_{mq} \rangle_0 - \langle \lambda_{kq2} \rangle_0 \right).
\end{align}$$
In the dynamic phasor case, the equation for $\frac{d}{dt} \langle \lambda_{0s} \rangle_1$ has a frequency shift.
To complete the state model, the magnetizing flux linkages are expressed as:
$$\begin{align}
\langle \lambda_{md} \rangle_0 &= L_{ad} \left( \frac{\langle \lambda_{ds} \rangle_0}{L_{ls}} + \frac{\langle \lambda_{fd} \rangle_0}{L_{lfd}} + \frac{\langle \lambda_{kd} \rangle_0}{L_{lkd}} \right) \nonumber \\
\langle \lambda_{mq} \rangle_0 &= L_{aq} \left( \frac{\langle \lambda_{qs} \rangle_0}{L_{ls}} + \frac{\langle \lambda_{kq1} \rangle_0}{L_{lkq1}} + \frac{\langle \lambda_{kq2} \rangle_0}{L_{lkq2}} \right)
\end{align}$$
where
$$\begin{align}
L_{ad} &= \left( \frac{1}{L_{md}} + \frac{1}{L_{ls}} + \frac{1}{L_{lfd}} + \frac{1}{L_{lkd}} \right) \nonumber \\
L_{aq} &= \left( \frac{1}{L_{mq}} + \frac{1}{L_{ls}} + \frac{1}{L_{lkq1}} + \frac{1}{L_{lkq2}} \right).
\end{align}$$
The mechanical equations in dynamic phasors are:
$$\begin{align}
T_e &= \langle \lambda_{qs} \rangle_0 \langle i_{ds} \rangle_0 - \langle \lambda_{ds} \rangle_0 \langle i_{qs} \rangle_0 \\
\frac{1}{\omega_s} \frac{d \delta_r}{dt} &= \omega_r - 1 \\
\frac{d \omega_r}{dt} &= \frac{1}{2H} (T_m - T_e).
\end{align}$$
Transient Stability Model
5.6 - VS-Inverter
6 - Core Tasks
Description of typical simulation and development tasks.
Each task should give the user
- The prerequisites for this task, if any (this can be specified at the top of a multi-task page if they’re the same for all the page’s tasks. “All these tasks assume that you understand….and that you have already….”).
- What this task accomplishes.
- Instructions for the task. If it involves editing a file, running a command, or writing code, provide code-formatted example snippets to show the user what to do! If there are multiple steps, provide them as a numbered list.
- If appropriate, links to related concept, tutorial, or example pages.
6.1 - Add New Model
Extending the simulator with new component or control models.
Add a Component Model
In this section we will show the implementation of a new component model by means a three-phase dynamic phasor inductor model.
C++ OOP
DPsim implements component models in a sub project called CPowerSystems (CPS) that is located in the models folder.
This folder is added to the DPsim CMake project.
Every component in DPsim is represented by a C++ class.
DPsim supports different types of solvers (MNA, DAE, NRP).
Each solver requires certain member functions in the component class to be implemented.
These functions are specified by the solver interface classes: MNAInterface.h
, DAEInterface.h
, etc.
Directory / Namespace Structure
For the implementation of the new component, we add two new files
models/Source/DP/DP_Ph3_Inductor.cpp
models/Include/DP/DP_Ph3_Inductor.h
In these files, we will implement a new C++ class with the name CPS::DP::Ph3::Inductor
.
The general structure looks as follows.
Directories:
DPsim
|
|- Source
|- Include
\ models
|- Source
|- DP
|- EMT
|- Static
\ Signal
|- Include
|- DP
|- EMT
|- Static
\ Signal
Namespaces:
CPS::{DP,EMT,Static,Signal}::{Ph1,Ph3}::{Name}
Attributes
Each components has a list of attributes, which has to be specified when creating the components class.
TODO: explain attribute system
Tasks for Pre/Post-step Functions
TODO: add example task dependency graph
Adding the new Component to DPsim
After finishing the implementation of the new component, it needs to be added to the following files:
models/Include/cps/Components.h
models/Source/CMakeLists.txt
Sources/Python/Module.cpp
6.2 - Create New Simulation
Using DPsim for a new simulation scenario.
Here, we will show the implementation of a new simulation scenario defined in C++, which is using DPsim as a library.
Directory Structure
In the end, your directory structure should look like as follows:
my-project
|- CMakeLists.txt
|- source
|- my-scenario.cpp
|- dpsim (as submodule)
CMake File
Your CMakeLists could look like this:
cmake_minimum_required(VERSION 3.5)
project(my-project CXX)
add_subdirectory(dpsim)
add_executable(my-scenario source/my-scenario.cpp)
target_link_libraries(my-scenario dpsim)
Build the Project
The build process is similar to the one of DPsim:
$ cd my-project
$ mkdir build && cd build
$ cmake ..
$ make my-scenario
7 - Examples
Here you can find some examples to get started with DPsim.
The DPsim repository includes examples that can be run locally.
8 - Roadmap
Short-term planning for new features is done on the GitHub Project board.
You can also check the Issues List or the Pull Requests on GitHub.
Under Development
- Solver
- Interfaces
- Tests, Examples, CI
- Models
Ideas
9 - Reference
Low level reference docs for DPsim.
The Sphinx documentation describes the Python API.
The Doxygen documentation is automatically generated from the C++ code using Doxygen.
It is helpful to understand the general structure of the C++ DPsim core components.
10 - Contribution Guidelines
How to contribute to DPsim.
We’d love to accept your patches and contributions to this project.
Please send us a pull request or get in touch with us via mail or slack if you would like to contribute.