Skip to main content

PESLite

Time-domain simulation of power-electronic converters in Python.

  • Networks of buses, lines, grid sources and any number of converters, defined in YAML simulation files, with run-time changes described as events.
  • Grid-following (PLL, current loop, dc-voltage loop) and grid-forming control (PSC, droop, VSG, dVOC, matching), in sampled or continuous form.
  • Switching, PWM-period-averaged and ideal continuously averaged bridges, selected independently for each converter.
  • Fixed-step, adaptive (SciPy or built-in DP45) and multirate integration.
  • Closed sampled controllers with ADC/PWM timing, or controller states integrated directly with the plant for ideal averaging, plus unit-owned protection.
  • Energy accounting of the power circuit and restart from any saved state.

The power circuit works in SI units (V, A, H, F, ohm); controllers work in pu of each converter's own base. Time is in s, angles in rad.

Installation

python -m pip install -e .            # numpy, scipy, PyYAML

Python 3.10 or newer.

Quick start

# run the first bundled example (sorted by name)
peslite

# run a bundled example by name; results go to output/<name>/
peslite gfl-example
peslite gfm-psc-example --out output/psc

# run your own simulation file
peslite case.pes
peslite ./cases/my-converter.pes

# A suffix may be omitted
peslite case

# bundled examples use the -example suffix
peslite gfl-example
peslite gfm-psc-example

# override any parameter by its dotted path
peslite gfl-example --set simulation.t_end=1 --set units.vsc.ctrl.computation=2e-6
peslite gfl-example --set simulation.solver.type=adaptive --set simulation.solver.method=DP45
peslite gfm-psc-example --set units.vsc.bridge.model=switching

# override the bridge model for every converter in this run
peslite gfl-example --switching
peslite gfl-example --pwm-averaging
peslite gfl-example --averaging

# print progress with selected quantities
peslite gfl-example --progress 0.1 --watch vsc.vdc_pu --watch vsc.i_c

# continue a run from its last saved state (or from time T with --initial-time T)
peslite gfl-example --out output/a
peslite gfl-example --initial output/a/states.csv --out output/b

# inspect a configuration
peslite gfl-example --list-states     # state names (the states.csv columns)
peslite gfl-example --ph-report       # port-Hamiltonian structure of the circuit
peslite gfl-example --resolved        # complete resolved simulation file
peslite --help

From Python (in this folder, or anywhere after pip install -e .):

import peslite

p = peslite.load("case.pes", **{"simulation.t_end": 1.0})
r = peslite.Simulation(p).run()     # streams directly to output/run

r.states["vsc.dclink.u_C"]         # a state over time
r.plant["vsc.i_conv_a"]            # a plant signal in the final CSV schema
r.ctrl["vsc.id_pu"]                # controller log of unit "vsc"
r.final_states()                   # last row, usable as an initial state
r.summary                          # trips, alarms, peaks
r.files                            # files already written by run()

Simulation files and events

A simulation file contains the model (base, buses, branches, sources, units and elements), what happens during the run (events), simulation settings (simulation) and optional text notes (meta). simulation.initial and simulation.output keep the initial-state and result settings together with the solver settings. Names without a suffix are SI quantities, names ending in _pu are per-unit quantities, and switches are written as 0 or 1.

--resolved prints the complete parameter set after defaults and command-line overrides are applied. The same complete configuration is saved as simulation.pes; it can be loaded directly to repeat the run.

Events use one common top-level mapping:

events:
  connect_vsc: {type: connect, target: vsc, t: 0.2, ramp: 1.0}
  p_step: {type: set, t: 2.0, set: {units.vsc.ctrl.references.p_ref_pu: 0.8}}
  load_on: {type: connect, target: load, t: 3.0}
  line_trip: {type: disconnect, target: line, t: 4.0}

connect and disconnect operate on units, sources, branches or elements. set changes a declared run-time parameter path and validates the resulting parameter set before it is used. Every event time is an exact integration boundary: the interval ending there uses the old model, and integration after it uses the updated model. A unit connection is a host run command to its controller; while stopped, integrating loops hold and the PWM is blocked through its normal register path. A converter that trips remains disconnected.

The built-in load element is a series R-L load from a bus to ground. It can be connected, disconnected or retuned by events; a small impedance can be used to model a fault.

Controller and converter hardware

A converter controller is a closed discrete-time block. At each control interrupt it receives one SI Measurement and returns ControlOutput with duty ratios, PWM enable, start-up completion and synchronization data. It does not own or decide protection. Its only host input affecting operation is command(run, ramp).

The controller's Startup state counts its own interrupts. A connect command releases PWM with the first computed duty word, ramps the active-power setpoint, and reports when the ramp ends; the unit uses that status to arm its sampled protection. A disconnect command blocks PWM and resets that ramp. Grid-forming synchronization laws track a usable terminal voltage before starting, avoiding an artificial phase jump at connection.

All protection belongs to Unit. One protection subsystem owns both execution paths and their sole trip latch: fast over-current checks run at switching and register-load instants, while voltage, frequency, DC-voltage and ROCOF checks run from ADC/controller-rate samples. The unit arms the latter when the controller reports that its start-up ramp is complete; the controller does not make the trip decision. Any trip blocks the gates, opens the AC terminal and disconnects the DC source for the rest of the run.

Bridge models

Each converter selects its own bridge model, so all three models can share a system:

units:
  vsc:
    bridge: {model: pwm_averaging}  # default; or averaging | switching
model Behaviour
switching ideal switches at the exact carrier-comparison instants
pwm_averaging the active duty ratios are continuous bridge values until the next PWM register load; no carrier ripple
averaging an ideal controlled voltage source with continuous measurements and controller equations; no ADC or PWM timing

When bridge.model is omitted, a converter uses pwm_averaging. --switching, --pwm-averaging and --averaging are run presets: respectively switching + fixed/rk4, pwm_averaging + fixed/rk4 and averaging + adaptive/DP45. The priority is --set > run preset > simulation file.

PWM averaging supports fixed and adaptive solvers and asynchronous or synchronous PWM. It keeps the same timer, active/shadow registers, computation eligibility and single/double register-load timing as switching. With single update a duty is held for one carrier period; with double update it may load at each carrier valley and peak. An asynchronous carrier phase still shifts that unit's control interrupts and loads, although the carrier waveform itself is not evaluated.

Ideal averaging keeps the same electrical bridge boundary, but connects the controller output directly to the ideal controlled voltage source. Controller states join the plant ODE and terminal measurements are evaluated at every solver stage. It constructs no ADC, control-interrupt grid, computation wait, carrier, active/shadow register or PWM delay state. Consequently ctrl.period, ctrl.computation, loop periods, ADC sampling/window settings and PWM timing settings are retained in a loaded configuration but silently ignored in this mode; the same file can be reused unchanged with --switching or --pwm-averaging.

Instantaneous feedback through component ports can form algebraic loops—for example terminal DC voltage, controller command and bridge DC current. Model assembly detects these loops from declared port dependencies, reduces each strongly connected group to the smallest connected feedback variables it can tear, and solves it internally at each derivative evaluation. No loop-specific ordering or manual break is configured, and the controller continues to measure the actual terminal u_dc rather than a substituted storage voltage. The continuous control graph applies the same strongly-connected-component rule to direct feedback between custom control loops; acyclic loop networks retain their single-pass evaluation path.

Example configurations

File Content
gfl-example.pes Grid-following converter; every key is annotated
gfm-psc-example.pes Grid-forming, power-synchronization control
gfm-vsg-example.pes Grid-forming, virtual synchronous generator
gfm-dvoc-example.pes Grid-forming, dispatchable virtual oscillator control
gfm-matching-example.pes Grid-forming, matching control
two-converters-example.pes A grid-forming and a grid-following unit on one grid

Frequently used settings

Path Values
simulation.t_end end time, s
simulation.solver.type / .method fixed: euler | heun | rk4; adaptive: RK45 | DOP853 | Radau | BDF | LSODA | DP45
simulation.solver.dt maximum fixed step, s
simulation.solver.subsystems own steps per subsystem, e.g. {vsc.dclink: 10, pcc: 0.1}
simulation.output.period snapshot interval, s
simulation.energy_check warn | strict | off
simulation.progress {enable: 1, period: 0.1, watch: [...]}; CLI: --progress, --watch
units.<u>.bridge.model pwm_averaging (default) | averaging | switching; CLI: --pwm-averaging, --averaging, --switching
units.<u>.ctrl.type gfl | gfm | custom
units.<u>.ctrl.period / .computation sampled-mode control-interrupt period and computation time, s; ignored by averaging
units.<u>.ctrl.loops.<loop>.period sampled-mode loop period, an integer multiple of ctrl.period; ignored by averaging
units.<u>.meas.period / .average sampled-mode ADC period and window-averaged channels; ignored by averaging
units.<u>.pwm.update single (valleys) | double (valleys and peaks)
units.<u>.pwm.method / .sync spwm | svpwm; asynchronous | synchronous
simulation.output.states / .signals / .energy which files are written

Output

File Content
states.csv every state at each snapshot; any row can start a new run
plant.csv, ctrl.<unit>.csv plant signals and controller logs (simulation.output.signals: 1)
energy.csv stored energy and power balance (simulation.output.energy: 1)
summary.json run summary
simulation.pes complete resolved simulation file; loading it repeats the run

simulation.initial.t and simulation.t_end are used exactly; neither is aligned or rounded to a converter's timer grid. A saved row restores either the active/shadow PWM registers and ADC averaging-window accumulators, or the continuous controller states of an ideal averaged unit, and derives any next timer points from that row's time. An oversampled ADC's intermediate samples are not states, so its saved run must be continued from a control interrupt. With simulation.output.states: 0, no states.csv is written and only the terminal state is collected internally, so final_states() remains available without serialising every snapshot. All enabled histories are streamed directly to their final CSV files in bounded batches; a run never accumulates the complete history in RAM. Python result columns are read from those final files only when requested and are not part of the write path. With simulation.output.energy: 0, energy checks still update the summary but their full time history is not retained.

Output names use the same unit convention as input parameters: an SI value has no unit suffix, while a per-unit value ends in _pu. Runtime state paths are entity-first: physical states are <entity>.*, while converter internals are <unit>.ctrl.*, <unit>.pwm.* and <unit>.meas.*. The global solver keeps solver.*. In the summary, switches such as tripped are 0 or 1, events that did not occur are null, and alarms, port-Hamiltonian defaults and energy problems are lists.

--progress SECONDS prints simulated time and wall time at the configured interval. --watch NAME appends a value to each line and may be repeated or given comma-separated names. A watched name may be a states.csv column, a plant.csv/controller column, a state alias, or a complex state without .re/.im to print its magnitude. Converters with the same signal use their unit prefix, for example vsc_1.vdc_pu and vsc_2.vdc_pu. State names use the same entity-first paths as states.csv; there is no separate plant.* domain. Existing public result columns take precedence when a signal and a state have the same name. For example:

peslite gfl-example --progress 0.1 \
  --watch vsc.vdc_pu \
  --watch vsc.i_c,vsc.u_dc

An unknown name is reported at the first progress line together with the available names.

Custom parts

A control loop type is one class: it owns its parameter dataclass, typed ports, role in the default wiring, update and named state. Register the class before loading a configuration that names its type:

from dataclasses import dataclass
from peslite.control import SyncLaw, register_loop_type

@register_loop_type
class MyLaw(SyncLaw):
    @dataclass(frozen=True, kw_only=True)
    class Params:
        period: float
        k_p_pu: float
        type: str = "my_law"

    type = "my_law"

    def step(self, T, p_pu, q_pu, v_mag_pu, v_dc_pu,
             p_ref_pu, q_ref_pu, v_ref_pu, i_dq):
        self.omega = self.w0 + self.cfg.k_p_pu * (p_ref_pu - p_pu)
        self.theta += T * self.omega
        self.v_mag = v_ref_pu

    def flow(self, p_pu, q_pu, v_mag_pu, v_dc_pu,
             p_ref_pu, q_ref_pu, v_ref_pu, i_dq):
        self.omega = self.w0 + self.cfg.k_p_pu * (p_ref_pu - p_pu)
        self.v_mag = v_ref_pu
        return {"theta": self.omega}

The registered name can then be used at units.<u>.ctrl.loops.<loop>.type. Circuit element types can likewise be registered with register_element_type, and event types with register_event_type; each type owns a frozen parameter dataclass, so its file parameters use the same parsing and validation as built-in types. A custom loop used by ideal averaging implements its continuous outputs and state derivatives (flow() for a SyncLaw); a sampled-only loop still works with switching and pwm_averaging. A user solver may implement the normal solver call alone; event-aware solvers may additionally provide settle() and parameters_changed() hooks.

A custom controller output stage can also be built with UniteType(cfg, pwm_method=..., limiter=...). Other replaceable parts are <unit>.modulator and solver. Executable examples of a custom loop, element, event and user solver live in tests/test_custom_parts.py; the root examples/ directory remains simulation-data-only.

Layout

pyproject.toml
src/peslite/            the package: __init__.py and four code parts
  components/           what the system is made of
    network.py            three-phase source, R-L branch, bus (R-C node), element types
    converter.py          switching/PWM-averaged bridges, ideal continuous bridge and dc link
    adc.py                sampling of a converter's measurements, averaging window, oversampling
    pwm.py                PWM timer, duty registers, carrier and modulators used inside a bridge
  control/              the converter's controller
    loops.py              what each loop type computes: its parameters, ports and update
    controller.py         controller interface, Startup, loop network, GFL/GFM wiring, UniteType
    modulation.py         output stage: voltage command to duty ratios, limiter, anti-windup
    blocks.py             transforms, filters and timers
  assembly/             a system built from a simulation file
    params.py             parameter classes, pu bases, runtime changes, file reading and writing
    validate.py           checks across the file's sections
    events.py             event types, connection state/ramp metadata and source scenarios
    protection.py         fast and sampled criteria, timers, alarms and the unit's trip latch
    unit.py               power stage, ADC/PWM peripherals, controller, protection and trip actions
    system.py             the network, units and elements as one model; applies events
  solver/               the numerical kernel and the run
    model.py              subsystems, connections, automatic algebraic-loop solving and named states
    energy.py             energy declarations, power balance, port-Hamiltonian report
    integrators.py        solver interface and single-rate integrators
    multirate.py          multirate integration and make_solver
    splitbound.py         error estimate of a multirate split
    simulation.py         run, records, result files and command line
tests/                  development test suite (python -m pytest); not included in the wheel
examples/               bundled *-example.pes files (YAML); no Python files

The controller depends only on the solver kernel; components depend on the controller interface; assembly builds a system from both; solver.simulation runs the assembled system. The YAML files under the root examples/ directory use the .pes extension and are wheel data, so the named *-example cases remain available after installing only the wheel.

License

GNU Affero General Public License v3.0 only (AGPL-3.0-only). See LICENSE.

Metadata

Release files for peslite 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for peslite 0.2.0
File Size Uploaded
peslite-0.2.0.tar.gz 189.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for peslite 0.2.0
File Interpreter ABI Platform
peslite-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 358.8 kB

Release files / peslite-0.2.0.tar.gz

Download URL peslite-0.2.0.tar.gz
Size 189.8 kB
Tags Source
SHA-256 checksum
How to use checksums
ca320e53cb32b14b3cf321c0237b1f4806a414400878e4aee32548ba7babadb7
BLAKE2b-256 checksum
How to use checksums
9eff37532921afe0835d9901b40991d0e79fe791aaf3681451109092f0d51881
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.

Transparency log

Release files / peslite-0.2.0-py3-none-any.whl

Download URL peslite-0.2.0-py3-none-any.whl
Size 169.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c47ec579e8956307a48e647103ec484fb71999df39beb41b597a5998e01c6c47
BLAKE2b-256 checksum
How to use checksums
a5a608db8e30e6c31543b90bf4a1681a4f265ee361f55c9c81c9832ae4536bb1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.

Transparency log

Release history Release notifications | RSS feed

0.3.0

2 release files

0.2.2

2 release files

0.2.1

2 release files

This release

0.2.0 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page