From c30b444ef16995e9632eb2cad9fa4dd7f427a85a Mon Sep 17 00:00:00 2001 From: Jonas Bengtsson Date: Sun, 4 Oct 2026 14:15:26 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=9D=20Update=20spec=20and=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 13 +++++- docs/spec/software-spec.md | 81 +++++++++++++++++++++++++++++++++----- 2 files changed, 84 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 077b8b1..71d53a4 100644 --- a/README.md +++ b/README.md @@ -325,7 +325,7 @@ requirement ID for each item in parentheses. statistics table (0x1029 words) with reset, LED-error (0x1020/0x1021) and module-diagnosis (0x1050) fault injection and clear; SF LED on the coupler reflects injected LED errors. -- [x] M5: configuration — coupler settings forms (watchdog time/masks/type/close-on-timeout via 0x1000–0x100B, response delay 0x1037, idle timeout 0x1030) with the device's own validation surfaced (writes refused while the watchdog runs → field error); module list editor over the 13-article catalog with SAVE CONFIG → live process-image rebuild (`rebuild_node`: datastore survives, values clear like a KBUS reset, 0x1022–0x1025 and 0x2030+ updated) + persistence to the node-config YAML via a comment-preserving custom emitter. +- [x] M5: configuration — coupler settings forms (watchdog time/masks/type/close-on-timeout via 0x1000–0x100B, response delay 0x1037, idle timeout 0x1030) with the device's own validation surfaced (writes refused while the watchdog runs → field error); module list editor over the 13-article catalog with SAVE CONFIG → live process-image rebuild (`rebuild_node`: datastore survives, values clear like a KBUS reset, 0x1022–0x1025 and 0x2030+ updated) + persistence to the node-config YAML via a comment-preserving custom emitter. Modules re-orderable by drag-and-drop or ▲/▼ arrows (FR-11.2.6); with per-type I/O codes (FR-4.3.11 v1.5) cross-kind moves don't renumber other kinds' channels. - [x] M5a: channel names for every module channel (FR-10.2.11) — node-config persisted (`channel_names:` mapping, I/O code → name), survive module saves and rebuilds, shown in the overview @@ -400,6 +400,17 @@ not yet implemented. Datasets are **optional**: signal logic (plugins, scheduled steps, degradation) runs in parallel with CSV streaming, or entirely without it. +- [ ] Plant environment `scenario/plantenv.py` (FR-8.2.14–16, requested + 2026-10-04): gym-style `PlantEnv` — deterministic engine-owned + tick clock (never wall-clock); per tick: + `observe()` returns the WAGO input image in engineering units + (channel values, names, per-type I/O codes, ranges, sim-time); + the decision source decides; `apply()` writes the decided + outputs through the datastore's internal paths (mirrors/ + statistics/watchdog stay consistent). Existing `plugins:` + become thin adapters; external (non-Python) decision sources + step the env over a socket; one decision source per output + alias per tick (ownership conflicts are errors). - [ ] `scenario/datasets.py` — streaming CSV playback for designated inputs (OPTIONAL driver): header-driven time column, `interpolation: linear|hold`, `repeat` with seamless seams, diff --git a/docs/spec/software-spec.md b/docs/spec/software-spec.md index a2ca7ab..df96a5e 100644 --- a/docs/spec/software-spec.md +++ b/docs/spec/software-spec.md @@ -567,6 +567,12 @@ unchanged whether or not CSV streaming is used. (6) scheduled steps due at this tick execute, (7) snapshots are taken if due. This order is part of the specification and shall be covered by tests. + **With the plant environment (FR-8.2.14)**, the decision source sees this + pipeline as one atomic `env` tick: `observe()` reads the state as of the + start of the tick, `apply()` queues the decided outputs, and the engine + then advances the pipeline once — outputs written by `apply()` land in + the output image before the next tick's `observe()`, so every state the + decision source sees is pipeline-consistent. - FR-8.2.13: **Logic-only continuous runs**: with no `datasets:` block at all, continuous mode shall work unchanged — demand profiles and other exogenous signals may be generated *inside* plant-model plugins (e.g., a @@ -574,6 +580,44 @@ unchanged whether or not CSV streaming is used. top-level `until:` bounding the run. All §8.3–8.4 machinery (degradation, invariants, rate limits, snapshots) applies identically, whether or not any CSV streaming is configured. +- FR-8.2.14: **Plant environment abstraction** (requested 2026-10-04): the + continuous engine exposes a gymnasium-style **environment object** — + `PlantEnv` — as the single programmatic interface to the simulated plant. + Per deterministic tick (the engine's fixed tick clock, FR-4.3.9 — never + wall-clock), the cycle is: + **`state = env.observe()`** → the caller (plant logic, an external + agent, or the scenario) decides → **`env.apply(actions)`** writes the + decided output values → the engine advances exactly one tick + (edges, datasets, degradation, invariants — FR-8.2.12 order) → repeat. + `observe()` returns the state of the **generic inputs** — the WAGO + input process image in engineering units (all DI/DO/AI/AO channel + values, per-type module/channel addressing, configurable channel + names, plus the active aliases) — and `apply(actions)` maps decided + values onto the **output process image** through the datastore's + internal Modbus write paths, so mirrors, statistics, and watchdog + behavior stay consistent (FR-9.2.6 semantics). The environment owns + no policy: the plant model (physics, closed-loop control) is supplied + by the caller, exactly like `gym.Env` vs. its agents. +- FR-8.2.15: **Environment integration**: `PlantEnv` is the interface the + per-tick plugins (FR-4.3.8) are migrated onto: a plugin becomes + `env.observe()` → decide → `env.apply()` per tick, and a scenario may + alternatively register an external `step(env)` agent — including a + non-Python one (e.g., a trained RL policy) communicating over a + socket/pipe — without touching the engine loop. The existing + `plugins:` mechanism keeps working unchanged (thin adapter); the + engine always owns the tick clock, and exactly one decision source + may drive an output alias per tick (ownership conflicts are validation + errors, extending FR-8.2.2). +- FR-8.2.16: **Environment API surface** (concrete): + `env.observe() -> PlantState` (channel values, names, I/O codes, + ranges, sim-time, wear/parameters); `env.apply(actions) -> None` + (actions keyed by I/O code or alias name; unknown keys are errors); + `env.reset(config) -> PlantState` (fresh episode, deterministic + seed); `env.tick_ms`, `env.sim_time_ms` (read-only clock); and the + full §8.3/§8.4 machinery (parameters, degradation, invariants, + snapshots) reachable as `env.parameters`, `env.invariants`. The same + object is served over the scenario-run HTTP API in server mode (§9), + so external agents can run against a live PLC-connected bench. ### 8.3 Requirements — degradation models (plant logic over time) @@ -744,15 +788,20 @@ src/wago_sim/scenario/ │ # time_factor — OPTIONAL driver in continuous mode ├── degradation.py # WearCounters, DegradationModel registry, maintenance actions ├── invariants.py # invariant & rate-limit evaluation, episode aggregation +├── plantenv.py # PlantEnv: gym-style observe/apply/reset cycle over the +│ # process images — the decision-source interface +│ # (FR-8.2.14–16); plugins become thin adapters └── engine.py # mode: continuous, parameter() accessor, snapshot # scheduler; fixed tick pipeline per FR-8.2.12: - # datasets -> edges -> plugins -> degradation -> + # datasets -> edges -> plugins/env -> degradation -> # invariants -> scheduled steps -> snapshots ``` ```mermaid erDiagram ContinuousScenario ||--o{ Dataset : "datasets (optional)" + ContinuousScenario ||--|| PlantEnv : "environment (FR-8.2.14)" + PlantEnv ||--o{ DecisionSource : "plugins / external agents" Dataset ||--|{ ColumnMapping : "alias columns" ContinuousScenario ||--|{ PlantPlugin : "signal logic (always runs)" ContinuousScenario ||--|{ DegradationModel : degradation @@ -764,6 +813,7 @@ erDiagram ContinuousScenario ||--o{ Snapshot : "periodic dumps" PlantPlugin }o--|| DegradationModel : "reads parameters" PlantPlugin }o--o{ Alias : "reads dataset-driven + computes logic-driven" + PlantEnv }o--|| ProcessImage : "observe/apply via datastore" ``` ### 8.7 Non-functional requirements (long-duration) @@ -809,6 +859,18 @@ erDiagram tick-order test. 11. A continuous scenario with no datasets, no scheduled steps, and no `until:` is rejected at validation time (FR-8.2.1). +12. **PlantEnv cycle** (FR-8.2.14): `observe()` returns every input channel + (values, names, codes, ranges, sim-time); `apply()` lands the decided + outputs in the output image before the next `observe()` — verified by a + deterministic two-tick test where the decision source copies an input + to an output and the second observation contains it. +13. **Plugin parity** (FR-8.2.15): the same plant logic runs unchanged as + a classic `plugins:` entry and as an `env.observe/apply` decision + source — identical outputs over a 1000-tick headless run (regression + gate for the adapter). +14. **External agent**: a decision source stepping the env over a + socket (non-Python agent) produces identical results to the in-process + variant on the same scenario (FR-8.2.15). --- @@ -1135,14 +1197,15 @@ This feature request closes that gap. - FR-11.2.5: A reconfiguration event is logged (sim-time, change description, new image sizes) and visible in the web UI's I/O Config page, mirroring the WBM SAVE CONFIG flow. -- FR-11.2.6: **Module reordering** (requested 2026-10-04): the web UI's - I/O configuration pane supports rearranging modules — move up/down - controls per module slot. Reordering triggers the same process-image - rebuild as any module change (FR-11.2.1–11.2.3) and persists the new - order. Because I/O codes number modules per type (FR-4.3.11 v1.5), - moving a module past a module of a different kind does not renumber - the other kind's channels — only the relative order within the same - type affects addressing. +- FR-11.2.6: **Module reordering** (requested 2026-10-04, implemented + same day): the web UI's I/O configuration pane supports rearranging + modules two ways — drag-and-drop of module rows (HTML5 DnD) and + move up/down arrow controls per slot; both are one gesture each and + trigger the same process-image rebuild + persistence as any module + change (FR-11.2.1–11.2.3). Because I/O codes number modules per type + (FR-4.3.11 v1.5), moving a module past a module of a different kind + does not renumber the other kind's channels — only the relative + order within the same type affects addressing. ### 11.3 Requirements — address-selection DIP switch emulation