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

Return to the regular view of this page.

Contributing

How to get a change into DPsim.

Contributions of all kinds are welcome, including code, documentation, examples, models, bug reports and reviews. Open a pull request or get in touch through GitHub Discussions.

These pages cover the process. For how the code is organised and the conventions it follows, see the developer guide.

Quick start

  1. Fork the repository and clone your fork.

  2. Run pre-commit install to activate the automated checks (formatting, notebook output stripping).

  3. Create a branch with a descriptive prefix (feature/, fix/, docs/).

  4. Commit with a sign-off, using the conventional commit style:

    git commit -s -m "fix: correct node voltage initialization in DiakopticsSolver"
    
  5. Keep your branch up to date by rebasing; do not merge the target branch into your branch:

    git fetch upstream
    git rebase upstream/master
    git push --force-with-lease
    
  6. Open a pull request from your fork against sogno-platform/dpsim:master.

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
     */
    

    Keep the SPDX tags on adjacent lines with no blank line between them, as the tooling reads them as a block.

  5. Linear History (no merge commits)

    DPsim maintains a linear git history. Never merge the target branch into your feature branch; rebase instead:

    git rebase upstream/master
    git push --force-with-lease
    
  6. No Saved Notebook Outputs

    Jupyter notebooks must be committed without saved cell outputs (images, plots, printed text). Saved outputs bloat diffs and can break the notebook test collector, which re-executes notebooks and re-extracts their outputs. Strip outputs before committing:

    jupyter nbconvert --ClearOutputPreprocessor.enabled=True --inplace <notebook.ipynb>
    

    A pre-commit hook does this automatically once you have run pre-commit install; CI also rejects a pull request that changes a notebook still carrying saved outputs.

Creating New Releases (info for maintainers)

DPsim currently uses 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 version in pyproject.toml
  • Update VERSION in the top level CMakeLists.txt
  • Update sonar.projectVersion in sonar-project.properties
  • Update version in CITATION.cff and the version noted on the citation page
  • Update CHANGELOG.md and include all the unreleased changes in the list
  • Update CONTRIBUTORS.md with the contributors added since the last release, as listed by git shortlog -sne
  • Create a new tag with an increased version number, which 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.

Linux and Windows wheels 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.

Planning

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.

Provenance and responsibility

Parts of this documentation were drafted with the help of large language models. The maintainers edit and curate that output; they do not vouch for every line as if it were written from first-hand knowledge, and the documentation carries no warranty.

That is a statement about how it is produced, not an excuse. The working rules exist precisely because generated prose is confidently wrong in ways that read well:

  • A page is written after running its code, never from reading the API. Every tutorial has a script in the repository and quotes it; if the two disagree, the page is wrong.
  • A claim about the code is traced to where the value is used before it is written down. Two hazard notes here described correct code as broken because a line was read in isolation; both were corrected once the value was followed through.
  • Anything measurable is measured by a script that reads the source, not asserted from having read a lot of it. That is what generate_model_availability.py, check_docs_pairing.py and check_docs_hazards.py are for.
  • No reference is cited that the repository does not already contain. Invented citations look exactly like real ones.

A notice in the site footer says the same to readers. If you find something wrong, open an issue. That is more useful than assuming a page is authoritative because it is detailed.

How the documentation is organised

Six sections, split by what the reader is trying to do rather than by topic. Putting a page in the right one is most of the work; the rest is deciding which half of the subject it covers.

SectionFor someone who wants toNames C++ classes
User Guideinstall DPsim and run a simulationno
Tutorialslearn by working through one idea at a timeonly Python API calls
Developer Guidechange DPsim, or understand how it works insideyes
Conceptsknow the mathematics behind a model or methodnever
Referencelook up what exists and wheregenerated
Contributingcontribute to the projectn/a

Every subject is written twice

Concepts carries the mathematics: what the model represents, the equations, and what they assume. It names no class, no file and no example, so a reader could follow it while implementing the model in something else entirely.

Developer Guide carries the arrangement in code: the class hierarchy, how the component interfaces with the solver, its attributes and state layout, the traps in configuring it, and links to the source and examples.

The two link to each other. scripts/docs/check_docs_pairing.py reports any Concepts page without a counterpart and exits non-zero, with an exemption list for method pages that have no single implementation behind them.

What is generated rather than written

Do not hand-edit these.

  • The model availability matrix comes from the component headers via scripts/docs/generate_model_availability.py. A model class it does not recognise makes it fail rather than silently omit the model.
  • The Python and C++ API references are generated by Sphinx and Doxygen from the source, so they are improved by writing better doc comments, not by editing the site.
  • Tutorial figures come from scripts/docs/generate_tutorial_figures.py. The architecture diagrams do not: they are editable draw.io SVGs and must be edited in draw.io.

Conventions

Every page carries a description, which appears under its title in section listings, and a weight, which sets its order in the sidebar. A page without a weight falls to alphabetical, which is how sections drift into arbitrary order.

Terminology follows standard electrical engineering usage. Avoid words that collide with class names: “signal” reads as SimSignalComp rather than as a recorded quantity, so prefer “attribute”.

Highlighting what a reader must not miss

Documentation prose is read in order, so anything that will silently cost someone an afternoon has to break out of the prose. Use the Docsy alert shortcode, with one of four titles, and keep the vocabulary small so the colours stay meaningful.

{{% alert title="Requires a build with VILLASnode" color="info" %}}
Something the reader must have before this page works at all.
{{% /alert %}}
ColourUse it forTitle starts with
infoa prerequisite: a build flag, an optional dependency, a package that is not installed by defaultRequires
warninga trap: correct code that silently does the wrong thing, a parameter that means two things, an ordering that mattersWatch out:
dangera known defect in DPsim itself, not something the reader can avoid by being carefulDefect: or Suspected defect:
primarya genuine aside that is neither a prerequisite nor a hazardNote

Use exactly these four prefixes. Earlier pages mixed Trap:, Watch out: and Gap: for the same colour, which made the severity unreadable: if two words mean one thing, neither means anything.

Three rules keep this useful. Mark a hazard where the reader meets it, not only in a reference page they may never open, so the same trap can appear on a tutorial and on an implementation page. Reserve danger for defects: if a reader can avoid the problem by knowing about it, it is a warning, and a page full of red stops being read. And a missing capability is a Note, not a Watch out:, because there is no hazard to avoid, only something that cannot be done.

Do not use blockquotes for this. They carry no colour and no title, so they read as ordinary text.

1 - LLM Pull Request Review

How the automated pull request review works and what it checks.

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.

What gets posted

Not every finding the passes raise reaches the pull request.

Tentative findings are not posted. A finding that verification could not confirm against the source, or that carries a confidence below 50 %, is written to the workflow log only. Over the review history of a merged pull request this was the large majority of everything raised, and effectively none of it was actionable; posting it buried the findings that were.

A run with nothing to say does not post a second review. The runner reviews again on every push. When a run has nothing left after the rule above, it edits its previous review instead, appending a line recording that the head commit was re-checked and nothing new was found. Editing a review body sends no notification, so those runs are visible on the pull request but silent in your inbox; only a run with something to say posts, and only that one mails you.

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.

2 - Continuous Integration

How the GitHub Actions workflows fit together and which of them are load bearing.

Entry point

Almost everything runs from a single workflow, .github/workflows/ci.yaml. It triggers on pushes to master, on tags, on pull requests and on manual dispatch, and it calls the reusable workflows that hold the actual work:

Reusable workflowContents
prepare-images.yamlbuilds the container images from their Dockerfiles and pushes them to the GitHub container registry
build-linux.yamlthe five Linux compilations, Fedora and Rocky, with ccache
build-windows.yamlthe two Windows compilations
checks.yamlpre-commit, the notebook output rule, the pyproject.toml extras resolution, and cppcheck
test-linux.yamlthe notebook test suite with coverage, the notebook result comparison, and the compiled WSCC 9 bus examples
run_villas_example.yamlone VILLASnode example per matrix entry
build-nix.yamlthe Nix build
packaging-python.yamlsource distribution, wheels, and the PyPI uploads
documentation.yamlthe reference documentation and both deployment targets

ci.yaml itself holds no build steps, only the triggers, the setup job and the ordering.

One consequence is worth knowing before renaming anything. A required status check is identified by its name, and a job called through a reusable workflow reports as caller job / inner job, so the branch protection rule has to list Build Linux / Rocky Linux rather than Rocky Linux. Matrix jobs whose name interpolates a matrix value, as the build jobs do, are reported under exactly that rendered name; a matrix job with a static name would instead get a (matrix, values) suffix appended. Renaming a job, or moving one between ci.yaml and a reusable workflow, therefore invalidates its protection entry and has to be done together with an update to the branch protection settings.

How a run picks its container images

The setup job decides this per image, not for all of them at once. Each image has its own path filter, so editing Dockerfile.dev-rocky rebuilds the Rocky image and leaves the other five alone. The two images that are built FROM the development image, the release image and the Binder image, track the development image’s paths as well, so a change to Dockerfile.dev rebuilds all three. Everything under packaging/Shell/ counts towards every image.

An image whose definition did not change is used as published on Docker Hub. One whose definition did change is rebuilt by prepare-images.yaml and pushed to ghcr.io/<owner>/dpsim/<image> under two tags, the commit SHA and the slugified ref name, and the run then builds against the SHA tag.

Manual dispatch takes a rebuild_images input that forces every image to be rebuilt.

Pull requests from forks

Rebuilding an image needs a token that may write packages, and a fork pull request gets a read-only one. CI therefore listens on two events and lets exactly one of them through: a pull request from a branch of this repository runs on pull_request, and one from a fork runs on pull_request_target. The setup job carries the condition, every other job depends on it, so the run that does not apply skips in its entirety.

On the pull_request_target path setup also carries the fork-pr environment. That environment has required reviewers, and because the whole pipeline hangs off setup, the approval is the single gate in front of a privileged run of code that came from a fork. It should not be granted without reading the change. Approving means the fork’s code builds and runs with a token that may write packages, and with the repository secrets that the test and documentation jobs inherit.

Both paths build their images inside the same run that consumes them, so a pull request is always tested against the images its own diff produces, and there is nothing to re-run by hand.

Because GITHUB_SHA and GITHUB_REF_NAME point at the base branch under pull_request_target, setup resolves the revision under test explicitly and passes it to every job, each of which checks out that revision rather than the default one.

The required check is the all barrier job, which fails if any job it depends on failed or was cancelled. It renames itself on the path that does not apply, so only the real run claims the protected name.

Publishing to Docker Hub

publish-images.yaml is what refreshes the sogno/dpsim images that the documentation and the outside world consume. It runs on the self-hosted runner, needs the Docker Hub credentials, and triggers on pushes to master, where it applies the same per-image path filters and republishes only what changed. Its manual dispatch takes an image input for republishing a single image on demand.

Workflows outside CI

  • sonar_cloud.yaml and sonar_cloud_scan.yaml are a deliberately separate pair. The scan holds SONAR_TOKEN and is triggered by the completion of the unprivileged build workflow; folding the build into CI would mean that any unrelated failing job suppresses the analysis.
  • llm-review.yml and llm-review-collect.yml are described under LLM Pull Request Review.
  • build_test_linux_rocky_profiling.yaml is dispatch only.
  • documentation.yaml is called by CI on master, and can also be dispatched on its own with an image input.