🚧 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
+62 -11
View File
@@ -185,6 +185,56 @@ Configuration options: `--no-web`, `--web-port <n>` or env
benches (all mutating endpoints answer 403). Plain HTTP — keep the
port inside the bench network.
## Plant environments (continuous simulation)
A **plant environment** simulates the physical world around the
distributed I/O: per deterministic tick it reads what the PLC
commanded (DO/AO), runs your plant physics, and writes what the field
now shows (DI/AI) — the sensors the PLC reads back. Environments run
**until manually stopped** (Ctrl+C / `docker stop`), optionally ending
sooner via an `is_done()` criterion.
Attach one to a live Modbus server (wall-clock-paced 10 ms ticks):
```bash
wago-sim serve --node config/pump_node.yaml --port 5020 \
--env envs.pump_station_env:PumpStationEnv
```
or run headless (unpaced, for plant-model development):
```bash
wago-sim sim --node config/pump_node.yaml \
--env envs.pump_station_env:PumpStationEnv
```
**Authoring one** is a small subclass — override `model()`:
```python
# myenvs/tank.py
from wago_sim.scenario.plantenv import PlantEnv
class TankEnv(PlantEnv):
def model(self, state, dt_s):
pump_on = bool(state.by_code("DO0.0")) # PLC command
level = float(state.by_code("AI0.0")) # current field
return {"AI0.0": level + (-5.0 if pump_on else 1.0) * dt_s}
Env = TankEnv # `--env myenvs.tank` picks this up
```
API surface (spec FR-8.2.14–17): `observe()` returns every
DI/DO/AI/AO channel (I/O codes, names, values) plus aliases and
degradation parameters; `apply()` writes DI/AI (the field);
`force()` writes DO/AO master-side (headless runs, fault setup);
`is_done()` optional end; `reset()` deterministic replay. Reference
forms: `pkg.mod:ClassName`, `pkg.mod:factory`, or `pkg.mod` with
`Env`/`make_env`. The shipped example is
`envs/pump_station_env.py` (tank + pump physics with a low-level
switch). Scenario-continuous mode (§8 YAML: datasets, degradation,
invariants) remains available for scripted soak runs on the same
engine ticks.
## Documentation
- `docs/spec/software-spec.md` — the software specification
@@ -404,17 +454,18 @@ reporting, snapshots, and the gym-style `PlantEnv`. 30 new tests
Datasets are **optional**: signal logic runs in parallel with CSV
streaming, or entirely without it.
- [x] 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).
- [x] Plant environment `scenario/plantenv.py` (FR-8.2.14–17, reworked
2026-10-04 to environment-first): standalone `PlantEnv` —
`observe()` returns ALL modules (DI/DO/AI/AO channels, per-type
codes, names, parameters); `apply()` writes the FIELD side
(DI/AI) by default; `force()` writes DO/AO master-side (mirrors/
statistics/watchdog stay consistent); `model()`/`is_done()`
override hooks (subclass authoring, FR-8.2.17); `reset()`
deterministic. Runners: `serve --env ...` wall-clock-paced
alongside Modbus, runs until stopped; `sim --env ...` headless
unpaced until is_done()/Ctrl+C. Example: `envs/pump_station_env.py`.
Verified live: PLC coil write → env physics → level registers
drain/refill over Modbus.
- [x] `scenario/datasets.py` — streaming CSV playback for designated
inputs (OPTIONAL driver): header-driven time column,
`interpolation: linear|hold`, `repeat` with seamless seams,