English | Deutsch
grid-gym is an open-source platform for the deterministic simulation,
validation, and analysis of electrical energy systems. It models grid
connection points, PV arrays, battery storage, smart meters, and load
profiles with reproducible tick-loop execution, snapshot/replay, fault
injection, and protocol adapters for field-bus telemetry.
Simulation only — not approved for production grid control.
grid-gymis a simulation, replay, and validation environment. Protocol adapters (MQTT, Modbus, OPC-UA, DNP3, IEC-61850) are intended to drive simulated devices or test rigs, not real plants (GG-SAFE-007,GG-NONGOAL-001).
grid-gym targets developers, research institutions, and system
integrators who need a local, traceable environment for energy-management
strategies, smart-grid controls, battery-storage strategies, replay
systems, and HIL-near tests — without requiring real field devices,
cloud services, or internet access at runtime.
grid-gym is already executable as a local, Docker-based validation
environment. The current implementation includes:
- a deterministic tick loop with snapshot and replay support
- nine productive device models: battery, PV, load, grid connection, and smart meter (MVP) plus EV charger, transformer, wind turbine, and diesel generator (SOLLTE devices, M8)
- fault injection and recovery flows (device faults plus dedicated metric-quality faults: frequency drop, NaN injection, stale data)
- multi-agent scenarios with a rule-based agent
- structured logs, metrics, and traces via the observability port trio
- an OTLP adapter with a local OpenTelemetry Collector smoke test
- five protocol adapters (MQTT, Modbus, OPC-UA, DNP3, IEC-61850) with container-based integration tests
- a field-server surface that exposes the simulated devices to an external
EMS / system-under-test (HIL,
GG-TEST-004): an MQTT telemetry push side, a Modbus-TCP server a master can poll (read-serving), and an inbound-write path that turns master setpoint writes (FC06/FC16) back into deterministic, replayable commands — all opt-in and byte-identical when unconfigured (simulation-/test-only, sim-net) - an HTTP API (FastAPI, REST + WebSocket) with a server-rendered web UI (HTMX + Chart.js): live telemetry, replay controls, alarms
- time-series persistence (Postgres) and a deterministic two-run replay
with
replay_diff_statusverdict - a telemetry quality pipeline (
STALEmax-age marking, comm-failureMISSING+ alarm) coveringGG-SAFE-001..004 - a machine-readable acceptance run via
make accept(AbnahmeReportJSON)
You can run the current gates and scenarios with:
make help
make gates # 11 mandatory gates (lint, format, typecheck,
# arch-check, a-check, tests, coverage, critical-coverage,
# dep-audit, noqa-gate, spdx-check)
make test-unit # unit + property test suite (2736 tests as of
# 2026-07-14, v0.8.0)
make test-integration # Compose/testcontainers integration suite
# (171 passed + 4 skipped as of 2026-07-14; incl.
# protocol adapters (MQTT/Modbus/OPC-UA/DNP3/IEC-61850),
# OTLP, HTTP-API/UI, persistence + replay-determinism
# E2E and the GG-SAFE-001..004 quality-pipeline smokes;
# the 4 skips are IEC-61850-on-Python-3.13 only —
# covered by `make test-iec61850`)Example YAML scenarios live under
tests/integration/scenarios/.
The repository is Docker-only: the host only needs docker and
make. No local Python or uv installation is required.
make fullbuild runs the full closure pipeline including
image-audit (Trivy) and a Compose smoke test. The mandatory
development gate is make gates.
Vulnerability audit ignores are sourced from
deploy/security/vulnignore.yaml(audit source-of-truth with mandatoryid/reason/expires/scopefields) and rendered todeploy/security/.trivyignoreviamake render-trivyignore(seeADR 0044). Expired entries break the build, forcing maintenance without external reminders. There are currently no active ignore entries. (The previous entry CVE-2026-42504 — Go stdlib MIME header DoS in the OTel collector sibling image — was resolved on 2026-06-18 by bumpingotel/opentelemetry-collector-contribto 0.154.0, built against go1.26.4+; Trivy re-scan reports 0 HIGH/CRITICAL.)
Current release: v0.8.0 (2026-07-14) — see
Releases and
CHANGELOG.md.
A release is triggered by pushing a v*.*.* git tag (or via
manual workflow_dispatch in the GitHub UI). The release workflow
publishes a container image to GHCR (ghcr.io/<owner>/grid-gym:<tag>)
plus five release-asset files: SBOM (CycloneDX-JSON via Syft against
the runtime image), test reports (JUnit-XML), coverage HTML tarball,
OpenAPI specification (JSON), and the demo acceptance document.
Local SBOM generation: make sbom (writes artifacts/sbom-<version>.cdx.json;
version defaults to pyproject.toml).
CI runs six GitHub-Actions workflows: ci.yml (lint / format-check /
typecheck / arch-check / a-check; five static-analysis gates), tests.yml
(test-unit on a Python 3.13/3.14 matrix + test-integration), coverage.yml
(coverage-gate 90/85 line/branch + coverage-gate-critical 90% critical
domain), dep-audit.yml (pip-audit), fullbuild.yml (make fullbuild
on relevant paths + workflow_dispatch fallback; covers image-audit and
the Compose smoke test), and release.yml (tag-push or workflow_dispatch).
- Deterministic execution. A central tick loop drives a discrete time model; snapshot envelopes and replay samples are byte-reproducible via canonical JSON serialization.
- Enforced architecture. 20 architectural contracts run on every
make arch-check: 7 forbidden-import contracts vialint-importsplus 13 custom AST/graph checks intools/arch_check.py(includingAC-ADAPTER-LIGHTWEIGHT,AC-OTLP-ADAPTER-NO-TIME,AC-TICK-LOOP-PRIVATE-RESUME-ERRORS, andAC-IEC61850-GPL-BOUNDARY). - Eleven-stage mandatory gate.
make gatesruns lint, format-check,mypy --strict, arch-check, a-check (sprachagnostic hexagon layer/direction validator), unit tests, coverage (90 % line per module / 85 % critical), critical-coverage, dependency audit, a# noqaban, andspdx-check(GPL-3.0-only header lint for the IEC-61850 boundary) — all cache-free green without any local override. - ADR-driven decisions. Every load-bearing decision is recorded as
an Architecture Decision Record; the ADR index
(
docs/plan/adr/README.md) is the live status source — post-milestone slices land their ADRs asAcceptedat slice closure (e.g.ADR 0079, Slice 079). - CI mirrors local. GitHub Actions runs the same
lint-imports,ruff check,tools/arch_check.py, andmypy --strictgates on every pull request andmainpush (.github/workflows/ci.yml).
grid-gym and bess-ems are
sibling projects in the same energy-systems toolkit.
grid-gym is the deterministic simulation, replay, and validation
environment. It models electrical energy systems, injects faults,
records traces, and provides reproducible test scenarios.
bess-ems is a battery energy-management system for operating Battery
Energy Storage Systems (BESS) with a safety-first control loop,
market/schedule handling, and protocol adapters.
In practice, grid-gym can serve as a local test and validation
environment for a production EMS such as bess-ems: the EMS acts as
the system under test, while grid-gym provides simulated devices,
grid behavior, telemetry, replay, and fault scenarios.
The projects share architectural ideas such as hexagonal boundaries,
explicit ports/adapters, and architecture checks — but grid-gym is
not an EMS implementation and does not duplicate bess-ems control
logic.
As of 2026-07-14 — current release v0.8.0:
| Item | Status | Details |
|---|---|---|
| M1..M8 core roadmap | Done |
core platform (M7) + SOLLTE devices & grid (M8); closure in M8-results.md + M7-results.md |
| v0.8.0 (2026-07-14) | released | Minor: a-check hexagon architecture gate + port extraction (Slice 079, ADR 0079) — a-check as a third, language-agnostic architecture gate plus two new driven ports; additive and behaviour-preserving. |
Full release history (v0.2.0 → v0.8.0) → CHANGELOG.md.
Quality pipeline: make gates 11-stage cache-free green; make fullbuild (incl. accept-pin-check) green. The integration suite is green; the remaining skips are IEC-61850-on-Python-3.13 only, covered by the dedicated make test-iec61850 stage (ADR 0046).
Pointers:
- User handbook →
docs/user/anwenderhandbuch.md - Demo acceptance steps (
GG-DEMO-008) →docs/user/gg-demo-008-abnahme.md - Quality-pipeline audit (
GG-SAFE-001..004) →docs/user/safe-001-004-quality-pipeline.md - ADR index →
docs/plan/adr/README.md - AI-agent briefing →
AGENTS.md
Individual gates for fast feedback loops:
make lint # ruff check
make format-check # ruff format --check
make typecheck # mypy --strict (ADR 0005)
make arch-check # import-linter + tools/arch_check.py (ADR 0002 §A-1)
make arch-check-imports # import-linter only (7 forbidden-import contracts)
make arch-check-custom # tools/arch_check.py only (13 custom checks)
make docs-check # markdown link/anchor validator (pinned d-check container)
make fullbuild # gates + integration + runtime image + image-audit + compose smoke
make perf # GG-RT-004 + GG-RT-005 SOLLTE: pytest-benchmark vs tests/perf/baseline.json (20% Median-Drift; ADR 0041; opt-in `--extra perf`; tick-loop 100 devices x 10k ticks + telemetry-port 10k publish/s with payloads ≤256 byte)
# GG-RT-001 MUSS Backpressure-Healthcheck: GET /runs/{id}/healthcheck → JSON with tick_duration_ms_p50/p95, missed_ticks_count, backpressure_status (Welle-4b-c).
make perf-baseline-update # maintainer-only: regenerate tests/perf/baseline.jsonThe following extensions are intentionally out of the current
delivery scope and tracked as optional additions in the normative
requirements
(spec/lastenheft.md):
- additional time-series storage adapters
(TimescaleDB
GG-PERSIST-006, InfluxDBGG-PERSIST-007) - hardware-in-the-loop (HIL) integration
GG-TEST-004 - model-predictive control (MPC) agents
GG-FUTURE-001 - reinforcement-learning (RL) agents
GG-FUTURE-002
.
├── .github/workflows/ ← 6 CI workflows (ci, tests, coverage,
│ dep-audit, fullbuild, release)
├── AGENTS.md, CHANGELOG.md, CONTRIBUTING.md, README.de.md, README.md
│ ← project docs + dual-license policy + AI-agent briefing
├── Dockerfile, Makefile, alembic.ini, pyproject.toml, uv.lock, .python-version
│ ← build / gate / dependency layer (ADR 0002 §6.1)
├── LICENSE + LICENSES/ ← MIT default + GPL-3.0-only for the IEC-61850 boundary
├── deploy/
│ ├── compose.yml ← productive Compose stack + OTLP collector sibling
│ └── otel-collector-config.yaml ← collector config (gRPC :4317)
├── docs/
│ ├── archive/ ← discarded / historical drafts
│ ├── plan/
│ │ ├── adr/ ← Architecture Decision Records
│ │ └── planning/
│ │ ├── done/ ← completed slices + closure notes
│ │ ├── in-progress/ ← active roadmap + slice plans
│ │ ├── next/ ← planned but not yet active
│ │ └── open/ ← trigger watch, open follow-ups
│ └── user/ ← user-/operator-facing docs (code review etc.)
├── harness/ ← agent harness (roles, review, verification, replay)
├── spec/
│ ├── architecture.md ← architecture (GG-AR-*)
│ ├── lastenheft.md ← normative requirements (GG-*)
│ └── protocol_profiles.md ← per-protocol adapter profile index
├── src/grid_gym/
│ ├── adapters/
│ │ ├── driven/
│ │ │ ├── _protocol_otel_wrap.py ← OTel span wrapper (+ comm-failure
│ │ │ │ wrapper `_protocol_comm_failure_wrap.py`)
│ │ │ ├── alarm_stream_inmemory/ ← InMemoryAlarmStream + AlarmHistoryBuffer
│ │ │ ├── observability_null/ ← Null Log / Metrics / Trace fallback
│ │ │ ├── persistence_postgres/ ← Postgres RunRepository + alembic migrations
│ │ │ ├── protocol_dnp3/ ← nfm-dnp3 productive + dnp3-outstation dev-only
│ │ │ ├── protocol_iec61850/ ← pyiec61850-ng GPLv3-isolated optional extra
│ │ │ ├── protocol_modbus/ ← pymodbus
│ │ │ ├── protocol_mqtt/ ← paho-mqtt
│ │ │ ├── protocol_opcua/ ← asyncua + OpcuaLoopThread async bridge
│ │ │ ├── random_mt/ ← MersenneTwisterRandomPort
│ │ │ ├── telemetry_otlp/ ← OTLP gRPC adapter (log / metric / trace exporters)
│ │ │ └── telemetry_stream_inmemory/ ← InMemoryTelemetryStream + DemoTelemetryGenerator
│ │ └── driving/
│ │ ├── http_api/ ← FastAPI app + REST + WebSocket + composition roots
│ │ └── ui/ ← Jinja2 templates + vendored HTMX + Chart.js + StaticFiles
│ └── hexagon/
│ ├── core/
│ │ ├── agents/ ← Agent protocol + AgentMessageBus + RuleBasedAgent
│ │ ├── devices/ ← battery/, grid_connection/, load/, pv/, smart_meter/
│ │ ├── domain/ ← frozen dataclasses (Command, Event, Alarm, ScenarioFault, ...)
│ │ ├── faults/ ← Battery + GridFaultAdapter
│ │ ├── grid_model/ ← balance model + LoadEvent / LoadProfile
│ │ ├── replay/ ← replay sample codec
│ │ ├── scenario/ ← YAML loader + validator
│ │ ├── serialization/ ← canonical_json
│ │ └── simulation/ ← TickLoop + scheduler + alarm_mappers
│ └── ports/
│ ├── driven/ ← Clock, DeviceProtocol, Fault, Observability, Random, RunRepository
│ └── driving/ ← AlarmStream, TelemetryStream
├── tests/
│ ├── integration/ ← Compose-based integration tests (139 passed + 4 skipped)
│ ├── unit/ ← pytest unit tests (1796 as of 2026-06-12)
│ └── unit/_arch_check_* ← architecture tests (7 lint + 13 custom = 20 contracts)
└── tools/
├── accept.py ← `make accept` acceptance CLI
├── arch_check.py ← AST / graph architecture checks (ADR 0002 §A-1)
├── check_core_determinism.py ← core determinism sweep
├── check_demo_scenario_pin.py ← demo scenario-hash pin lint (`accept-pin-check`)
├── check_noqa.py ← `# noqa` ban gate
├── check_spdx.py ← SPDX header lint for the GPL-3.0-only boundary
├── _demo_replay.py ← headless demo replay helper
├── diagnose_otlp_span_export.py ← OTLP debug matrix script
├── render_trivyignore.py ← vulnignore → .trivyignore renderer (ADR 0044)
└── wait_otel_collector.py ← bounded liveness poll for distroless OTLP collector
(`make docs-check` runs the pinned `d-check` container image — no in-repo script.)
The documentation and planning structure is defined in
docs/plan/adr/0001-documentation-and-planning-structure.md.
Note: the linked ADRs, slice plans, and planning documents under
docs/ and spec/ are written in German. The English
README mirrors the structure and key facts; for deep-dive content,
consult README.de.md or the German source documents.
This project is MIT-licensed by default — see LICENSE.
Exception: GPLv3-isolated IEC-61850 adapter (M4 Wave 5b, ADR
0035 Decision I-f):
the sub-paths src/grid_gym/adapters/driven/protocol_iec61850/,
tests/unit/adapters/driven/protocol_iec61850/,
tests/integration/test_iec61850_*.py, and
tests/integration/fixtures/iec61850/ link against the GPLv3-
licensed pyiec61850-ng
/ libiec61850 library and are therefore distributed under
GPL-3.0-only. The GPLv3 text is in LICENSES/GPL-3.0.txt.
The pyiec61850-ng library itself is shipped as an optional
extra. The default install ships only MIT-licensed code; users
opt into the IEC-61850 adapter — and the GPL distribution terms
that come with it — by explicitly installing the extra:
pip install grid-gym # MIT-only
pip install 'grid-gym[iec61850]' # MIT + GPLv3 IEC-61850 adapter