Add modes/ section with levelbased page as the template

Introduces the pattern: basin model is the shared canvas (mode-agnostic
physics); each control mode is its own page under wiki/modes/ plus a
demand-vs-level transfer-function diagram under wiki/diagrams/modes/.

- wiki/modes/README.md — index + per-mode page template (inputs,
  threshold policy, demand formula, edge cases, related)
- wiki/modes/levelbased.md — first worked example using the new naming
  convention (dryRunLevel / minLevel / startLevel / maxLevel /
  overflowLevel). Forward-looking — the code still uses the old names
  until the pending rename refactor.
- wiki/diagrams/modes/levelbased.drawio.svg — transfer-function plot
  (zones: STOP / DEAD ZONE / RAMP / SATURATE, safety trips outside the
  plot). Round-trippable via embedded drawio XML.
- functional-description.md — replaced the inline levelbased/manual
  subsection with a table pointing at the modes/ pages. Removed the
  old control-zones ASCII diagram reference (superseded by the
  per-mode transfer function).
- wiki/README.md — added Control modes entry + diagrams/modes/ pointer.

The remaining placeholder modes (flowbased, pressureBased,
percentageBased, powerBased, hybrid, manual) can each fill in the
template independently.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
znetsixe
2026-04-22 15:45:01 +02:00
parent 61e0688f73
commit 4637448c49
6 changed files with 235 additions and 183 deletions

View File

@@ -4,12 +4,15 @@ All docs and diagrams for this node live in this folder so they version-lock wit
## Pages
- **[Functional Description](functional-description.md)** — operator-facing reference derived from `src/specificClass.js`: basin model, net-flow selection, level-based control zones, safety interlocks, registration topology.
- **[Functional Description](functional-description.md)** — operator-facing reference derived from `src/specificClass.js`: basin model, net-flow selection, safety interlocks, registration topology.
- **[Control modes](modes/README.md)** — one page per control mode (`levelbased`, `flowbased`, …) describing how the mode uses the shared basin model to compute demand.
## Diagrams
Editable draw.io sources live in [`diagrams/`](diagrams/). See [`diagrams/README.md`](diagrams/README.md) for the editing workflow — open `.drawio` files in [draw.io](https://app.diagrams.net/), export to `.drawio.svg`, commit both.
The basin model is the shared canvas ([`diagrams/basin-model.drawio.svg`](diagrams/basin-model.drawio.svg)); per-mode transfer-function diagrams live under [`diagrams/modes/`](diagrams/modes/).
## Part of
This node is a git submodule of [EVOLV](https://gitea.wbd-rd.nl/RnD/EVOLV). The EVOLV superproject has its own [`wiki/`](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/main/wiki) with platform-level docs (architecture, concepts, shared manuals).

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 566 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -224,21 +224,17 @@ flowPositions = { inflow: ['in', 'upstream'], outflow: ['out', 'downstream'] }
## Control logic
### `levelbased` mode — three zones
The `pumpingStation` supports multiple control modes. Each mode is a **policy that sets the three control thresholds (`minLevel`, `startLevel`, `maxLevel`) and produces a demand (0 100 %)** — the two safety thresholds (`dryRunLevel`, `overflowLevel`) are mode-independent and handled by the safety layer below.
![Control zones — level axis with RUN / DEAD ZONE / STOP bands](diagrams/control-zones.drawio.svg)
Every mode gets its own page under [`modes/`](modes/README.md) with a consistent layout (inputs, threshold policy, demand formula, edge cases) so they can be compared side-by-side. Currently:
- **STOP.** Below `stopLevel` every machine group receives `turnOffAllMachines()` and `percControl` is reset to `0`.
- **DEAD ZONE.** Between `stopLevel` and `startLevel` no command is issued. Pumps currently running keep their last setpoint; pumps currently off stay off. This prevents rapid on/off cycling near the threshold.
- **RUN.** Above `startLevel` the linear scaling range `[minFlowLevel … maxFlowLevel]` maps to `[0 % … 100 %]` pump demand. The station forwards the same percentage to every registered machine group via `group.handleInput('parent', percControl)`. Above `maxFlowLevel` the demand exceeds 100 %; the MGC clamps internally.
| Mode | Status | Page |
|---|---|---|
| `levelbased` | ✅ implemented | [modes/levelbased.md](modes/levelbased.md) |
| `manual` | ✅ implemented (via `Qd` topic) | — |
| `flowbased`, `pressureBased`, `percentageBased`, `powerBased`, `hybrid` | 🚧 placeholder in code | — |
### `manual` mode
`_controlLogic` is a no-op. Demand is injected externally via the `Qd` topic, which calls `forwardDemandToChildren(demand)` — MGCs get the demand unchanged; direct pumps get `demand / count` each.
### Other modes
`flowbased`, `pressureBased`, `percentageBased`, `powerBased`, `hybrid` are enumerated in the schema and selectable via `changemode`, but today fall through to a placeholder / warning. When implemented they will plug into the same `_controlLogic(direction)` switch.
See [`modes/README.md`](modes/README.md) for the index and page template.
## Safety controller

29
wiki/modes/README.md Normal file
View File

@@ -0,0 +1,29 @@
# Control modes
Each page describes one `pumpingStation` control mode and how it uses the shared [basin model](../functional-description.md#basin-model) — specifically, how it sets the three control thresholds (`minLevel`, `startLevel`, `maxLevel`) and computes the demand it sends to the MGC.
The two **safety** thresholds (`dryRunLevel` and `overflowLevel`) are mode-independent and are enforced by the safety layer outside any mode. They never appear in a mode's policy.
## Template
Every mode page follows the same structure:
1. **At a glance** — one sentence + small fact table (inputs, output, status)
2. **Diagram** — reference to `../diagrams/modes/<mode>.drawio.svg`
3. **Inputs** — what signals the mode reads
4. **Threshold policy** — how it sets/adjusts `minLevel`, `startLevel`, `maxLevel`
5. **Demand formula** — how it turns inputs into a 0-100 % demand for the MGC
6. **Edge cases** — cold start, sensor dropout, interaction with safety layer
7. **Related** — links to other modes + functional description
## Implementation status
| Mode | Status | Page |
|---|---|---|
| `levelbased` | ✅ implemented | [levelbased.md](levelbased.md) |
| `flowbased` | 🚧 placeholder in code | — |
| `pressureBased` | 🚧 placeholder in code | — |
| `percentageBased` | 🚧 placeholder in code | — |
| `powerBased` | 🚧 placeholder in code | — |
| `hybrid` | 🚧 placeholder in code | — |
| `manual` | ✅ implemented (Qd topic) | — |

84
wiki/modes/levelbased.md Normal file
View File

@@ -0,0 +1,84 @@
---
title: Level-based mode
mode: levelbased
status: implemented
updated: 2026-04-22
---
# Level-based mode
The simplest and most widely deployed control strategy. Demand is a direct, *static* piecewise-linear function of basin level — no feedback loop, no predictions beyond the level measurement itself. This page uses the [shared basin model](../functional-description.md#basin-model); see [`modes/README.md`](README.md) for the template other mode pages follow.
## At a glance
| Item | Value |
|---|---|
| Signal driving demand | basin level (measured, predicted fallback) |
| Output | demand 0100 % forwarded to every MGC child |
| Thresholds adjusted at runtime? | No — static from editor config |
| Use when | Inflow is sewer-gravity (no smart metering) and operator wants a predictable, inspectable response |
## Diagram
![Level-based mode — demand vs level transfer function](../diagrams/modes/levelbased.drawio.svg)
*Editable source: [`../diagrams/modes/levelbased.drawio.svg`](../diagrams/modes/levelbased.drawio.svg) (drag into [draw.io](https://app.diagrams.net/) — it round-trips).*
## Inputs
| Signal | Where from | Role |
|---|---|---|
| current level | `measurement` child (`measured`) → predicted from volume integrator (fallback) | X-axis of the transfer function |
| `config.control.levelbased.minLevel` | editor, static | below → pumps hard OFF |
| `config.control.levelbased.startLevel` | editor, static | where demand-ramp starts |
| `config.control.levelbased.maxLevel` | editor, static | where demand saturates at 100 % |
The three control thresholds are the **only** mode-specific configuration. Nothing here is recomputed at runtime.
## Threshold policy
| Threshold | Source | Adjustable at runtime? |
|---|---|---|
| `minLevel` | `config.control.levelbased.minLevel` | No |
| `startLevel` | `config.control.levelbased.startLevel` | No |
| `maxLevel` | `config.control.levelbased.maxLevel` | No |
That this policy is trivial (all static) is **the defining simplicity of this mode**. Modes like `powerBased` or future `weather-aware` variants will recompute these thresholds on the fly.
## Demand formula
```text
if level < minLevel:
demand = 0
MGC → turnOffAllMachines() # explicit shutdown, not just "0 %"
elif level < startLevel:
demand = <previous demand> # dead zone — hold last command (hysteresis)
elif level <= maxLevel:
demand = lerp(level, [startLevel, maxLevel], [0 %, 100 %])
else:
demand = 100 % # saturated; MGC clamps internally if overshoot
```
Where `lerp` is linear interpolation. The MGC is free to distribute the demand across its pumps however its own policy dictates (equal split, lead-lag, staging — that's the MGC's business).
## Edge cases
- **Cold start with level in the dead zone.** `demand` has no prior value; it defaults to `0`. Pumps stay OFF until the level first crosses `startLevel` upward. Once it does, normal ramp-and-hold behaviour engages.
- **Level sensor drops out mid-run.** `_selectBestNetFlow` falls back to predicted level (computed from the volume integrator) — the mode doesn't care which variant wins, it just reads the chosen level.
- **Both sensor and predictor unavailable.** The mode's preconditions fail; `_controlLogic` logs a warning and exits without issuing a command. The last-known demand is held, which is safe.
- **Level crosses `maxLevel` upward.** Demand saturates at 100 %. Level may still continue rising if inflow > station capacity — this is the scenario that trips the overflow-safety layer (see below).
- **Level crosses `dryRunLevel` downward.** The **safety layer** (not this mode) force-shuts all downstream pumps regardless of what demand the mode is commanding. The mode's demand is effectively overridden until level climbs back above `dryRunLevel + hysteresis_margin`.
- **Level crosses `overflowLevel` upward.** The safety layer logs the spill event and raises an alarm. The mode continues commanding at 100 % — which is what you want, because the pumps should keep draining as fast as physically possible. (See [functional description § Safety controller](../functional-description.md#safety-controller) for the gravity-sewer caveat.)
## Why this is worth migrating off of
Level-based is fine for steady-state sewer inflows. It has two known weaknesses:
1. **Predictable, not proactive.** It can't *pre-empty* the basin ahead of a forecasted storm or a power-price peak. Modes like `weather-aware` or `powerBased` can — by moving `startLevel` down or up at runtime.
2. **Thresholds assume pump capacity is fixed.** If you add or remove pumps, the `startLevel ↔ maxLevel` band that gave smooth 0-100 % coverage no longer matches the new capacity. Flow-based and percentage-based modes are less brittle to capacity changes because they close the loop on *what you actually measure* (outflow or fill %) rather than *what you assume the level→capacity map is*.
## Related
- [Functional description](../functional-description.md) — basin model, net-flow selection, safety layer (shared across all modes)
- [modes/README.md](README.md) — mode index + template
- Other mode pages: *to be written* (`flowbased.md`, `pressurebased.md`, `percentagebased.md`, `powerbased.md`, `hybrid.md`, `manual.md`)