Update pumping station basin documentation

This commit is contained in:
Rene De Ren
2026-05-05 10:38:24 +02:00
parent ab0d4ed285
commit da50403c76
11 changed files with 78 additions and 415 deletions

View File

@@ -41,14 +41,18 @@ Every field on the pumpingStation editor maps directly to the config schema in `
| Field | Default | Meaning |
|---|---|---|
| **Basin Volume (m³)** | `1` | Total geometric volume of the empty basin (floor to rim). |
| **Basin Volume (m³)** | `1` | Total geometric storage volume from basin floor to rim. |
| **Basin Height (m)** | `1` | Physical wall height from floor to rim. |
| **Inlet Elevation (m)** | `2` | Centre of the inlet pipe, measured from the floor. |
| **Outlet Elevation (m)** | `0.2` | Centre of the pump-suction pipe, measured from the floor. |
| **Overflow Level (m)** | `2.5` | Overflow-weir crest, measured from the floor. Above this → overfill safety. |
| **Inlet Elevation (m)** | `2` | Bottom/invert of the incoming sewer pipe, measured from the basin floor. This is the level where backing up into the inlet starts to matter hydraulically. |
| **Outlet Elevation (m)** | `0.2` | Top of the pump-suction/outlet pipe, measured from the basin floor. This is the practical lower hydraulic reference for pump protection. |
| **Inlet Pipe Diameter (m)** | `0.4` | Nominal incoming sewer pipe diameter. Used with `inflowLevel` to distinguish pipe bottom, centre, and crown in future hydraulic upgrades. |
| **Outlet Pipe Diameter (m)** | `0.4` | Nominal pump-suction/outlet pipe diameter. Used with `outflowLevel` to distinguish pipe top, centre, and invert in future hydraulic upgrades. |
| **Overflow Level (m)** | `2.5` | Physical overflow-weir crest, measured from the floor. At or above this level the basin is actually spilling. |
Constant cross-section is assumed: `surfaceArea = volume / height`. All derived volumes (`minVolAtOutflow`, `minVolAtInflow`, `maxVolAtOverflow`, `maxVol`) are computed once in `initBasinProperties()` and kept on `station.basin`.
The current runtime still uses the level fields directly for its volume math. Pipe diameters are part of the basin model contract so later hydraulic logic can reason about pipe invert/crown and not silently treat every pipe elevation as a centreline.
### Hydraulics (section `hydraulics`)
| Field | Default | Meaning |
@@ -63,8 +67,8 @@ Constant cross-section is assumed: `surfaceArea = volume / height`. All derived
|---|---|---|
| **Control mode** | `levelbased` | Active control strategy. Schema enumerates seven modes; today `levelbased` is fully implemented, `manual` forwards demand via `Qd`, others are placeholders. |
| **minLevel (m)** | `1` | Below this level → unconditional MGC shutdown. |
| **startLevel (m)** | `1` | Bottom of the linear scaling range (0 % demand — ramp starts here). |
| **maxLevel (m)** | `4` | Top of the linear scaling range (100 % demand). Typically ≈ `overflowLevel`. |
| **startLevel (m)** | `1` | Mode-specific threshold. In `levelbased`, this is the bottom of the linear scaling range (0 % demand). It is not part of the generic basin model because other modes can define a different start policy. |
| **maxLevel (m)** | `4` | Upper normal operating/storage level used by the active mode. In `levelbased`, this is where demand reaches 100 %. |
| **Flow setpoint** | `0` | Flow-based target (m³/h). Placeholder until `flowbased` is wired. |
| **Deadband** | `0` | Flow-based deadband (m³/h). Placeholder. |
@@ -74,9 +78,9 @@ Constant cross-section is assumed: `surfaceArea = volume / height`. All derived
|---|---|---|
| **Time To Empty/Full (s)** | `0` | If > 0, triggers safety when predicted time-to-overflow or time-to-empty falls below this value. `0` disables time-based protection. |
| **Enable Dry-Run Protection** | `true` | If on, pumps are shut down once volume drops below the dry-run threshold while draining. |
| **Low Volume Threshold (%)** | `2` | Dry-run trigger: `triggerLowVol = minVol × (1 + pct/100)`. |
| **Enable Overfill Protection** | `true` | If on, upstream inflows are shut down once volume climbs above the overfill threshold while filling. |
| **High Volume Threshold (%)** | `98` | Overfill trigger: `triggerHighVol = maxVolAtOverflow × pct/100`. |
| **Low Volume Threshold (%)** | `2` | Safety margin above the configured minimum volume: `dryRunSafetyVol = minVol × (1 + pct/100)`. This creates `dryRunLevel`; it is derived, not a separately entered basin height. |
| **Enable Overfill Protection** | `true` | If on, upstream inflows are shut down once volume climbs above the high-volume safety point while filling. |
| **High Volume Threshold (%)** | `98` | Safety margin below physical overflow: `highVolumeSafetyVol = maxVolAtOverflow × pct/100`. Actual overflowing is still the boolean condition `level >= overflowLevel`. |
### Output formats
@@ -168,15 +172,29 @@ Line-protocol payload for the `telemetry` bucket. Tags stay low-cardinality (sta
## Basin model
The basin is modelled as a rectangular prism with constant cross-section. Everything derives from `volume = level × surfaceArea`.
The basin is modelled as a rectangular prism with constant cross-section. Everything derives from `volume = level × surfaceArea`, with every level measured upward from the basin floor.
![Basin model — physical layout with control thresholds](diagrams/basin-model.drawio.svg)
*Editable source: [`diagrams/basin-model.drawio`](diagrams/basin-model.drawio). See [`diagrams/README.md`](diagrams/README.md) for the edit-and-export workflow.*
*Editable source: [`diagrams/basin-model.drawio.svg`](diagrams/basin-model.drawio.svg) (drag into draw.io; the SVG embeds the editable source). See [`diagrams/README.md`](diagrams/README.md) for the edit-and-export workflow.*
**Typical ordering** (bottom → top): `outflowLevel ≤ minLevel < inflowLevel < startLevel < maxLevel overflowLevel`.
**Generic basin ordering** (bottom → top): `outflowLevel ≤ dryRunLevel ≤ minLevel < inflowLevel < maxLevel ≤ highVolumeSafetyLevel < overflowLevel ≤ basinHeight`.
> ⚠️ The comment block in `specificClass.js` currently says `startLevel ≤ inflowLevel` (inlet above startLevel). The physical convention is the opposite: pumps start *before* the water reaches the gravity inlet, so `inflowLevel < startLevel`. Worth fixing in the code comment next time that file is touched.
`startLevel` is deliberately not part of this generic basin diagram. It belongs to a control mode. For the current level-linear mode, see [`diagrams/modes/basin-mode-level-linear.drawio.svg`](diagrams/modes/basin-mode-level-linear.drawio.svg).
The pipe labels are intentional:
- `inflowLevel` is the bottom/invert of the incoming sewer pipe.
- `outflowLevel` is the top of the pump-suction/outlet pipe.
This avoids hiding hydraulic consequences behind ambiguous pipe-centre elevations. Pipe diameters are part of the model contract so later versions can derive pipe centre/crown/invert where needed.
`dryRunLevel` and `highVolumeSafetyLevel` are derived safety points. They provide margin before the two hard physical conditions:
- Actual dry-run risk is at or below the pumpable lower hydraulic reference.
- Actual overflowing is the boolean condition `level >= overflowLevel`.
The high-volume safety point exists so the station can still react before the basin is physically spilling. Once `overflowLevel` is reached, the model should report overflowing rather than treating that point as a controllable threshold.
**minHeightBasedOn** — which pipe defines `minVol`, the operational floor used for the initial seed, the dry-run trigger, and the 0 % point of the fill percentage:
@@ -195,6 +213,8 @@ The basin is modelled as a rectangular prism with constant cross-section. Everyt
starts at the inlet.
```
The rectangular approximation is acceptable for this node's first basin model because operational level is always in metres from the basin floor, while calculated m³ can tolerate small shape errors. A later upgrade can replace `volume = level × surfaceArea` with a level-volume curve for benching, sumps, sediment/dead zones, and irregular wet-well geometry.
## Net-flow selection
Every tick, `_selectBestNetFlow()` walks a priority ladder and returns the first net flow that clears the dead-band (`|flow| ≥ flowThreshold`):
@@ -223,7 +243,9 @@ flowPositions = { inflow: ['in', 'upstream'], outflow: ['out', 'downstream'] }
## Control logic
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.
The `pumpingStation` supports multiple control modes. Each mode is a **policy that maps basin state to demand (0-100 %)**. `levelbased` uses `minLevel`, `startLevel`, and `maxLevel`; other modes may use different thresholds or compute them dynamically.
The basin model owns the shared physical and safety references: pipe elevations, `dryRunLevel`, `highVolumeSafetyLevel`, and `overflowLevel`. `startLevel` is mode-specific and is documented with the mode diagrams, not the generic basin drawing.
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:
@@ -237,13 +259,13 @@ See [`modes/README.md`](modes/README.md) for the index and page template.
## Safety controller
`_safetyController` runs **before** `_controlLogic` every tick. Two rules, deliberately asymmetric — *dry-run protects the pumps from running themselves into air*, *overfill protects the basin from spilling*.
`_safetyController` runs **before** `_controlLogic` every tick. Two rules, deliberately asymmetric — *dry-run protects the pumps from running themselves into air*, *high-volume protection tries to preserve distance to actual overflow*.
![Safety rules — dry-run vs overfill](diagrams/safety-rules.drawio.svg)
During overfill, level-based control naturally commands 100 % on the downstream MGC because the level is above `maxLevel`.
During high-volume or overflow conditions, level-based control naturally commands >=100 % on the downstream MGC because the level is above `maxLevel`.
> ⚠️ **Known limitation — gravity-sewer context.** The "upstream STOP" action only makes sense in a **cascaded** station layout where the upstream equipment is an EVOLV-controllable pump or station. In a conventional wastewater wet-well the inflow is gravity-fed from the municipal sewer and **cannot be stopped** — attempting to would back up toilets. For that case the correct response to an overfill event is to **measure and log the spill over the weir** (for compliance reporting) and raise an alarm, while keeping downstream pumps at maximum demand. The current code fires `execSequence: shutdown` on upstream children regardless of what they are; that should be gated on "is the upstream actually controllable?" and supplemented with overflow-rate tracking. Tracked as follow-up work.
> ⚠️ **Known limitation — gravity-sewer context.** The "upstream STOP" action only makes sense in a **cascaded** station layout where the upstream equipment is an EVOLV-controllable pump or station. In a conventional wastewater wet-well the inflow is gravity-fed from the municipal sewer and **cannot be stopped** — attempting to would back up toilets. For that case the correct response at the high-volume safety point is to alarm early and keep downstream pumps at maximum demand. If `level >= overflowLevel`, the station should report actual overflowing as a boolean and, later, estimate/log spill over the weir for compliance reporting. The current code fires `execSequence: shutdown` on upstream children regardless of what they are; that should be gated on "is the upstream actually controllable?" and supplemented with overflow-rate tracking. Tracked as follow-up work.
A missing volume reading is treated as a hard fault: every direct machine is sent `execSequence: shutdown` and `safetyControllerActive` latches. Calibrate predicted volume (`calibratePredictedVolume`) or wire a level measurement to recover.
@@ -292,8 +314,8 @@ The canonical end-to-end demo lives in the EVOLV superproject at [`examples/pump
| Symptom | Likely cause | Fix |
|---|---|---|
| `fill %` exceeds 100 % or is negative | Basin geometry inconsistent — e.g. `overflowLevel > heightBasin`, or `outflowLevel > inflowLevel`. | Cross-check `0 < outflowLevel < inflowLevel < overflowLevel ≤ heightBasin` in the editor. |
| Pumps never start in `levelbased` | Level is stuck in the DEAD ZONE between `minLevel` and `startLevel`, or `startLevel == maxLevel` so the scaling range collapses. | Widen the control band: move `startLevel` above `minLevel` and set `maxLevel ≈ overflowLevel`. |
| `fill %` exceeds 100 % or is negative | Basin geometry inconsistent — e.g. `overflowLevel > basinHeight`, or `outflowLevel > inflowLevel`. | Cross-check `0 < outflowLevel < inflowLevel < overflowLevel <= basinHeight` in the editor. |
| Pumps never start in `levelbased` | Level is stuck in the DEAD ZONE between `minLevel` and `startLevel`, or `startLevel == maxLevel` so the scaling range collapses. | Widen the mode control band. In sewer-gravity cases, `startLevel` is normally below `inflowLevel` so the station starts draining before the incoming sewer pipe is hydraulically affected. |
| "No volume data available to safe guard system; shutting down all machines." in logs | No measured level, predicted volume not calibrated, and no inflow/outflow samples yet. | Issue `calibratePredictedVolume` (or `calibratePredictedLevel`) once at startup, or wire a level sensor. |
| `flowSource: null` and `direction: 'steady'` forever | Every flow / level signal falls inside the dead-band (default `1e-4 m³/s`). | Confirm flows are non-zero, or lower `config.general.flowThreshold` for a small-scale demo. |
| `Qd` ignored | Station is not in `manual` mode. | Send `{ topic: 'changemode', payload: 'manual' }` first, or fall back to level-based control. |