This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

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.

1 - Attribute Usage Guidelines

Attribute Usage Guidelines

This page gives practical rules for deciding when a model variable should be a DPsim attribute. For details on the attribute mechanism itself, see Attributes.

Rule of Thumb

Use an attribute if the value must be visible to DPsim infrastructure, for example logging, interfaces, Python access, string-based lookup, or scheduling.

Otherwise, prefer a normal C++ member variable or a local variable.

Quick Decision Checklist

Before adding a new attribute, ask:

  1. Does it need to be logged?
  2. Does it need to be imported or exported?
  3. Does it need to be accessed from Python or by name?
  4. Is it used as a scheduler dependency?
  5. Is it an externally relevant model input, output, state, or setpoint?
  6. Is a normal C++ variable insufficient?

If the answer to all questions is no, do not make it an attribute.

Use an Attribute For

Use an attribute if the value:

  • should be logged
  • should be imported or exported through an interface
  • should be accessed from Python or generic code by name
  • is read or modified by scheduled tasks
  • is an externally relevant model input, output, state, or setpoint
  • is a derived view of another attribute, for example one matrix coefficient

Typical examples are interface voltages and currents, source references, controller setpoints, and values exchanged through VILLASnode.

Do Not Use an Attribute For

Prefer a normal C++ variable if the value:

  • is only used inside one method
  • is a temporary intermediate result
  • is a cached coefficient or solver helper
  • is a fixed implementation detail
  • duplicates another existing attribute
  • never needs logging, interface access, Python access, or scheduling

Do not create attributes for every variable in the model equations.

Choose the Simplest Attribute Type

If decided that a value should be an attribute, choose the simplest suitable attribute type.

Prefer a static attribute when the value is stored directly by the component:

const Attribute<Real>::Ptr mPower;

MyComponent::MyComponent(const String& name)
    : IdentifiedObject(name),
      mPower(mAttributes->create<Real>("P", 0.0)) {}

Use dynamic, referenced, or derived attributes only when the value must depend on another attribute.

For example, use a derived attribute when exporting one coefficient of a matrix or vector attribute:

intf->exportAttribute(component->mIntfCurrent->deriveCoeff<Complex>(0, 0), 0, true);

Avoid long chains of dynamic, referenced, or derived attributes unless they are really needed.

Access Attributes in C++

In model code, prefer typed attribute members:

mPower->set(power);
const Real power = **mPower;

Use string-based lookup mainly in generic code, logging, interfaces, tests, or Python-style access:

logger->logAttribute("P", component->mPower);
intf->exportAttribute(component->attribute("P"), 0, true);

When assigning a new value, prefer set() if update tasks should be triggered or if the assignment should be explicit. Direct mutable access through **attribute can be used for simple static attributes, but it does not express this intent as clearly.

Examples

Private member variable: no need to use an attribute

This value is stored as a member because it is used by several functions of the class. It is still internal to the implementation: it does not need to be logged, imported or exported, used by the scheduler, or accessed by name.

class MyComponent : public IdentifiedObject {
private:
  Real mConductance = 0.0; // used internally by several methods
};

Externally visible output: use an attribute

This value is a model output. It may be useful for logging, plotting, interfaces, tests, or Python access, so it should be registered as an attribute.

class MyComponent : public IdentifiedObject {
public:
  const Attribute<Real>::Ptr mPower;

  explicit MyComponent(const String& name)
      : IdentifiedObject(name),
        mPower(mAttributes->create<Real>("P", 0.0)) {}

  void updatePower(Real power) {
    mPower->set(power);
  }
};

Runtime setpoint: use an attribute

This value is a model input or setpoint. It may be changed from outside the component, for example through Python, an interface, or a test setup.

class MySource : public IdentifiedObject {
public:
  const Attribute<Real>::Ptr mVoltageRef;

  explicit MySource(const String& name)
      : IdentifiedObject(name),
        mVoltageRef(mAttributes->create<Real>("V_ref", 0.0)) {}

  void setParameters(Real voltageRef) {
    mVoltageRef->set(voltageRef);
  }
};

Derived scalar value: use a derived attribute

This value is not stored separately. It is a scalar view of an existing vector or matrix attribute, which avoids duplicating data manually.

intf->exportAttribute(component->mIntfCurrent->deriveCoeff<Complex>(0, 0), 0, true);

2 - 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
                }
            ]
        }
    ]
}

3 - 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.

Pull Requests

There are no strict formal requirements besides the following:

  1. Developer Certificate of Origin (DCO)

    We require a Developer Certificate of Origin. See more here.

  2. Code Formatting with pre-commit

    We enforce code formatting automatically using pre-commit. Please run pre-commit install the first time you clone the repository to run pre-commit before each commit automatically. If you forgot to do this, you will need to use the command pre-commit run --all-files one time to format your changes.

  3. Development in Forks Only

    We accept contributions made in forks only. The main repository is not intended for contributor-specific branches.

  4. SPDX Headers

    Use SPDX headers to indicate copyright and licensing information, especially when introducing new files to the codebase. For example:

    /* Author: John Smith <John.Smith@example.com>
    * SPDX-FileCopyrightText: 2025 Example.com
    * SPDX-License-Identifier: MPL-2.0
    */
    

Creating New Releases (info for maintainers)

DPsim currently uses to Semantic 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:

  • Update setup.cfg
  • Update CMakeLists.txt
  • Update sonar-project.properties
  • Update CHANGELOG.md and include all the unreleaed changes in the list
  • Create a new tag with an increased version number (can be done during the Release in GitHub)

Python Packages

Due to the creation of a new tag, a new PyPi package will be deployed automatically.

Only Linux packages are currently available, other platforms will be supported in the future.

Container Images

To release an updated Docker image, the container workflow needs to be triggered manually.

If a Pull Request changes a container image, this is not updated automatically in the container image register.

4 - LLM Pull Request Review

Overview

DPsim ships an optional, non-blocking pull-request reviewer that runs a series of specialised passes over the diff of a pull request using a large language model and posts a single review comment. It is intended as an assistive first pass: it never requests changes and cannot block a merge, so a human review remains authoritative.

The reviewer lives under .github/llm-review/ (the prompts in prompts.py and a pure-standard-library runner in review.py) and is driven by two workflows: llm-review-collect.yml, which runs on the pull request itself, and llm-review.yml, which performs the review. The split is what makes reviewing pull requests from forks safe (see Fork pull requests). It communicates with any OpenAI-compatible chat endpoint, configured through the environment variables described below.

How it works

For each pull request the runner reads the base..head diff and sends it, in turn, to a set of focused review stages, each with its own prompt. The stages cover model equations and their derivation, MNA stamping and domain modeling, numerical correctness, task scheduling and attribute usage, real-time safety, C++ class design and reuse, naming and in-code documentation, logging discipline, the Python bindings, input parsing, the build system and dependencies, testing and component coverage, and licensing and pull-request hygiene. Each stage returns a strict JSON list of findings. A final synthesis pass deduplicates and prioritises them, and the runner posts them as one review, anchoring inline comments only to lines present in the diff.

The prompts encode DPsim’s documented conventions (see Guidelines) and the recurring points raised in past reviews, so the feedback stays specific to this project rather than generic.

Configuration

The workflow requires one repository secret:

  • RWTH_LLM_TOKEN: the bearer API key for the chat endpoint.

The following repository Actions variables are optional and override the defaults baked into the workflow:

  • LLM_BASE_URL: the OpenAI-compatible base URL.
  • LLM_MODEL: the model identifier.
  • LLM_CHAT_PATH: the chat path appended to the base URL (default /chat/completions).
  • LLM_REVIEW_RUNNER: the runner label (default ubuntu-latest; see Runner selection).

The workflow’s baked-in defaults target an OpenAI-compatible deployment; override LLM_BASE_URL and LLM_MODEL to point at a different endpoint or model.

Obtaining an API key

Obtain a bearer API key from the chosen OpenAI-compatible provider and store it as the RWTH_LLM_TOKEN repository secret. The key is only exposed to workflow runs on pull requests from the repository itself, never from forks.

Before storing the secret, a single request confirms that the key reaches the model:

curl -sS -X POST "$LLM_BASE_URL/chat/completions" \
  -H "Authorization: Bearer $RWTH_LLM_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"model\":\"$LLM_MODEL\",\"messages\":[{\"role\":\"user\",\"content\":\"ok\"}]}"

Running locally

The runner has a dry-run mode that executes the full pipeline and prints the assembled review instead of posting it. It needs no GitHub token and no Actions runner, only network access to the endpoint:

cd .github/llm-review
export LLM_BASE_URL='<openai-compatible-base-url>'
export LLM_MODEL='<model-identifier>'
export LLM_CHAT_PATH='/chat/completions'
export LLM_API_KEY="$RWTH_LLM_TOKEN"
export BASE_SHA=$(git rev-parse origin/main) HEAD_SHA=$(git rev-parse HEAD) PR_NUMBER=0
python3 review.py --dry-run

Runner selection

The workflow defaults to a GitHub-hosted ubuntu-latest runner. If the chosen endpoint is only reachable from within a particular network, set the LLM_REVIEW_RUNNER variable to a self-hosted runner label registered inside that network; no change to the workflow is required.

Fork pull requests

Reviewing pull requests from forks requires care, because a fork’s code is untrusted and must never gain access to the secret. The reviewer uses the workflow_run pattern for this, rather than pull_request_target, and splits the work into two workflows:

  • llm-review-collect.yml runs on the pull_request event, including from forks. GitHub withholds secrets from fork pull_request runs, so this job has no key. It checks out nothing and runs no code from the pull request; it only records the PR number and commit SHAs, taken from trusted GitHub context, into an artifact.
  • llm-review.yml runs on workflow_run, after the collect job completes, in the base repository context where the secret is available. It checks out the base repository’s own code, never the pull request’s, and reads the diff as data through the GitHub API. It never builds or executes anything from the pull request.

Two properties keep the key safe. First, the key is only ever sent as an Authorization header to the configured LLM endpoint, and is never placed in the model prompt, so a prompt-injection payload in the diff cannot reveal it. Second, the privileged job runs only trusted base-repository code, so untrusted pull request code never executes with the secret in scope. The PR metadata read from the collect artifact is validated (numeric PR number, hexadecimal SHAs) before use. For this to operate, both the workflows and the secret must reside on the repository the pull requests target.