From 4637448c497d558b78b6d3422afa243f9a4f2aa9 Mon Sep 17 00:00:00 2001 From: znetsixe Date: Wed, 22 Apr 2026 15:45:01 +0200 Subject: [PATCH] Add modes/ section with levelbased page as the template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- wiki/README.md | 5 +- wiki/diagrams/basin-model.drawio.svg | 176 +--------------------- wiki/diagrams/modes/levelbased.drawio.svg | 104 +++++++++++++ wiki/functional-description.md | 20 +-- wiki/modes/README.md | 29 ++++ wiki/modes/levelbased.md | 84 +++++++++++ 6 files changed, 235 insertions(+), 183 deletions(-) create mode 100644 wiki/diagrams/modes/levelbased.drawio.svg create mode 100644 wiki/modes/README.md create mode 100644 wiki/modes/levelbased.md diff --git a/wiki/README.md b/wiki/README.md index 2a3359d..3799dc4 100644 --- a/wiki/README.md +++ b/wiki/README.md @@ -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). diff --git a/wiki/diagrams/basin-model.drawio.svg b/wiki/diagrams/basin-model.drawio.svg index b21784b..0279225 100644 --- a/wiki/diagrams/basin-model.drawio.svg +++ b/wiki/diagrams/basin-model.drawio.svg @@ -1,170 +1,6 @@ - - - Basin model — physical layout + control thresholds - - - - - - - Basin model — physical layout + control thresholds - - - - - - - - freeboard - SCALING RANGE - (levelbased: demand ramps 0 → 100 %) - DEAD ZONE - (hysteresis — keep last cmd) - BUFFER - dead volume - - - - heightBasin - - - - heightOverflow - → spill → measure - - - - maxFlowLevel - - - - startLevel - - - - INFLOW - heightInlet - - - - stopLevel - → unconditional STOP - - - - OUTFLOW - heightOutlet - → dry-run trip - - - - floor (0) - +
5. overflowLevel
4. maxLevel
3. startLevel
Sewage + Tank buffer
2. minLevel
Spare volume before spilling
Notes: 
  • All levels should be dynamic. It could also very well be that at a certain time we want to dynamically move the startLevel up or down depending on the situation the weather or incoming flow is in.
1. dryRunLevel
  1. At this level pumps cant pump anymore they will run dry. 
  2. This is the minimum level that should be obtained to protect the pumps from dry-running. Again minLevel and dryLevel can theoretically be the same but isnt advisable seeing there are mechanical delays and other system effects which could harm the pumps.
  3. System is active and pumps flow away.
  4. Flow goes out in the basin or somewhere else in the system. This is the height where gray water will spill over to the river without being cleaned.
  5. MaxLevel is up to where we can "store water" maxLevel and OverflowLevel can theoretically be the same. Its always smart to have a minimum buffer of n %. Its important to factor in measurement deviation in repeatability and accuracy in determining this.
Tank buffer
InflowLevel
OutflowLevel
Tank buffer
Dead volume
\ No newline at end of file diff --git a/wiki/diagrams/modes/levelbased.drawio.svg b/wiki/diagrams/modes/levelbased.drawio.svg new file mode 100644 index 0000000..2cdcf25 --- /dev/null +++ b/wiki/diagrams/modes/levelbased.drawio.svg @@ -0,0 +1,104 @@ + + + Level-based mode — demand as a function of basin level + + + Level-based mode — demand as a function of basin level + Thresholds are static (from config); safety trips are handled by the mode-independent safety layer + + + + + + + + + + + + STOP + MGC shutdown + DEAD ZONE + hysteresis + RAMP + linear 0→100 % + SATURATE + clamped at 100 % + + + + 100 % + 0 % + + + demand + + + + + + + dryRunLevel + (safety) + + + minLevel + + + startLevel + + + maxLevel + + + overflowLevel + (safety) + + basin level + + + + + + + + + + + + + + + + + + + + + could be anywhere 0–100 % + (holds whatever RAMP last set) + + + + + + + + SAFETY + dry-run + (pumps forced OFF) + + + + + SAFETY + spill + (log + alarm) + + + + + Every mode page in this wiki uses the same transfer-function format. Only the curve shape (and what drives the X-axis) changes. + + diff --git a/wiki/functional-description.md b/wiki/functional-description.md index a931ac7..c691b04 100644 --- a/wiki/functional-description.md +++ b/wiki/functional-description.md @@ -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 diff --git a/wiki/modes/README.md b/wiki/modes/README.md new file mode 100644 index 0000000..2807f08 --- /dev/null +++ b/wiki/modes/README.md @@ -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/.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) | — | diff --git a/wiki/modes/levelbased.md b/wiki/modes/levelbased.md new file mode 100644 index 0000000..d4547b6 --- /dev/null +++ b/wiki/modes/levelbased.md @@ -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 0–100 % 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 = # 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`)