Pylectra architecture overview¶
Intermediate
Prerequisites: What is a plugin
At a glance¶
YAML config / Python dict
│
▼
┌───────────────────┐
│ pylectra.config │ ← parse, validate
│ ExperimentConfig │
└─────────┬─────────┘
│
mode = single / batch / cct
│
▼
┌───────────────────────────────────────┐
│ pylectra.runners │
│ SingleRunner BatchRunner CCTRunner│
└─────┬─────────────────┬────────────────┘
│ │
│ joblib parallel│
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ pylectra.engine │ │ pylectra.io │
│ (ODE + events) │ │ HDF5 / Parquet │
└─────────┬────────┘ └──────────────────┘
│
│ ABC dispatch
▼
┌─────────────────────────────────────────┐
│ pylectra.registry │
│ {category → {name → plugin class}} │
│ │
│ generator exciter governor pss │
│ ode_solver power_flow fault case │
│ scenario filter small_signal plot │
└─────────────────────────────────────────┘
▲
│ @register
│
┌─────────────────────────────────────────┐
│ Plugin implementations (sub-pkgs) │
│ models/ faults/ scenarios/ filters/ │
│ solvers/ powerflow/ plotting/ cases/ │
└─────────────────────────────────────────┘
Three layers¶
Layer 1 — Config parsing¶
pylectra.config.ExperimentConfig: turns a YAML file or a Python dict into a strongly-typed object.
Responsibilities:
- YAML schema validation (missing fields, type checks).
- Path resolution (relative → absolute).
- Default-value filling.
The schema is fixed here; specific plugins are still resolved by name at runtime via the registry.
Layer 2 — Runners¶
One class per run mode; all expose the same .run() method:
| Runner | Purpose | Output |
|---|---|---|
SingleRunner |
One deterministic simulation | SingleRunOutput (contains a SimulationResult) |
BatchRunner |
Multi-scenario dataset generation | HDF5 files + Parquet metadata |
CCTRunner |
Bisection for critical clearing time | CCTResult |
pylectra.run.run(config) is the top-level entry point — it dispatches to the right runner based on mode.
Layer 3 — Engine + Registry¶
Engine¶
pylectra/engine/
├── equilibrium.py # Power flow + multi-machine init
├── rhs.py # Assembles dy/dt = f(t, y) including network solve
├── loop.py # scipy ODE main loop with event splitting
├── torch_engine.py # torch ODE main loop (optional GPU)
└── state.py # pack/unpack state vectors
The engine is not a plugin — it's infrastructure. But each step calls plugins through ABCs:
- Power flow →
power_flowplugin (pandapower/newton) - Generator derivatives →
generatorplugin (two_axis,classical) - Exciter derivatives →
exciterplugin - ODE stepping →
ode_solverplugin - Fault on/off →
faultplugin (event injection)
Registry¶
pylectra.registry._REGISTRY = {
"generator": {"two_axis": <class>, "classical": <class>},
"exciter": {"simple_avr": <class>, ...},
...
}
Twelve whitelisted categories; lookup is a plain dict at runtime. New categories are intentionally restricted; adding new plugins inside an existing category is fully open.
Data flow for a single simulation (mode: single)¶
run("xxx.yaml")→ExperimentConfig.from_yaml(...).- Mode dispatch instantiates
SingleRunner(cfg). SingleRunner.run():- Look up
cfg.case_pfin thecaseregistry →NetworkCase. - Look up
cfg.power_flow.kind→ run PF → equilibrium voltages. - For each machine, fetch the right generator / exciter / governor / pss plugin and call
.init()→ multi-machine initial state. - Stitch every plugin's
.derivative()into a singlerhs(t, y)callable. - Look up
cfg.fault.kind→ event schedule. - Look up
cfg.solver.kind→ feedrhs+ events into the ODE solver. - The solver advances time; one ODE call per "leg" (between fault on/off events).
- Wrap the trajectory into a
SimulationResultand return.
How the three modes relate¶
SingleRunner ──────────────► one trajectory
│
▼
BatchRunner ──► loop N times ──► many trajectories ──► HDF5 / Parquet
│ │
│ └─► scenarios perturb the case → SingleRunner
│
└─► joblib parallelises N workers, each running an independent SingleRunner
CCTRunner ──► bisection loop ──► many SingleRunner calls (varying fault duration) ──► CCT value
BatchRunner and CCTRunner both treat SingleRunner as an atomic operation — which is why SingleRunner must be deterministic and pickle-safe (joblib parallelism requires it).
pylectra/_legacy/¶
A private internal sub-package holding the MATLAB-port code (PowerFlow / Models / Auxiliary / Solvers) translated from the original MatDyn. The current ODE main loop still depends on it. It is transparent to end users — you'll never see pylectra._legacy in the public API.
A future release will rewrite the loop natively on pandapower + scipy and _legacy/ will be removed. This is the project's openly tracked technical debt — see "Known limitations" in CHANGELOG.md.
Plotting subsystem¶
pylectra/plotting/
├── plugins.py # @register("plot", ...) for every built-in plot
├── time_series.py # rotor_angles / speeds / efds / voltages / overview
├── topology.py # network topology
├── batch_stats.py # histogram / violin / heatmap / acceptance
├── style.py # Nature-style rcParams
└── io.py # vector PDF / high-DPI PNG export
pylectra.plotting.render(name, data, ...) looks up name in the registry and calls the matching class's .render(). The pylectra plot ... CLI uses the same path.
Next steps¶
- How to add a new generator model — practice plugin authoring.
- pylectra.registry module — registry API details.