🚧 Rewrite continous simulation.

This commit is contained in:
Jonas Bengtsson
2026-10-04 15:29:57 +02:00
parent 263afc9c1e
commit 66b7e12f0f
9 changed files with 856 additions and 226 deletions
+42 -37
View File
@@ -580,44 +580,49 @@ 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.14: **Plant environment abstraction** (requested 2026-10-04,
reworked same day to environment-first): `PlantEnv` is the **primary
interface** for continuous plant simulation, standalone-first — it
requires only a virtual node + datastore (no scenario), with the §8.3/8.4
machinery (degradation, invariants) as optional attachments. Per
deterministic tick (the engine or the runner advances the tick clock,
never wall-clock arithmetic): **`state = env.observe()`** returns the
state of ALL modules — DI/DO/AI/AO channels, engineering units, per-type
I/O codes, configured names, plus active aliases and parameters — →
`env.model(state, dt)` (the caller-supplied plant physics) → **`apply()`
writes the new field state**. The environment owns no policy; plant
models are supplied by subclassing (FR-8.2.17).
- FR-8.2.15: **Action semantics**: `apply()` primarily writes the **input
side (DI/AI)** — the simulated field: what the sensors show, driven by
plant physics reacting to the PLC's output commands. Writing outputs
(DO/AO) is allowed as explicit **forcing** via a `force=True` keyword or
`env.force(...)` — for headless runs without a PLC and for bench
fault setup; forced outputs go through the datastore's internal write
paths so mirrors/statistics/watchdog stay consistent (FR-9.2.6).
Exactly one decision source may own an 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.
`env.observe() -> PlantState` (all channels, aliases, parameters,
sim-time); `env.apply(values, force=False)` (keys: I/O code or alias
name; DI/AI default, DO/AO require force; unknown keys are errors);
`env.model(state, dt_s)` — override point for plant physics, returning
new field values; `env.is_done(state) -> bool` — optional end
criterion, default False (runs until manually stopped);
`env.reset()` — deterministic fresh episode; `env.tick_ms` /
`env.sim_time_ms` (read-only clock); `env.parameters` /
`env.invariants` attachments.
- FR-8.2.17: **Environment authoring & runners**: environments are easy
to create and call — a subclass of `PlantEnv` overriding `model()`
(and optionally `is_done()`) in any importable module
(`--env myenvs.pump_station.PumpStationEnv`), or a module-level
factory function. Two runners: (a) `wago-sim serve --env ...` ticks
the environment on a **wall-clock-paced** periodic task (10 ms tick
alongside the Modbus server, real-time behavior for a live PLC) and
runs **until manually stopped** (SIGINT/SIGTERM — the existing
graceful-shutdown path); (b) `wago-sim sim --node ... --env ...`
runs headless unpaced (as fast as determinism allows) until
`is_done()` or SIGINT. The scenario-continuous mode (FR-8.2.1-13)
remains a scripted layer on top of the same engine ticks.
### 8.3 Requirements — degradation models (plant logic over time)