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

@@ -1 +1,9 @@
# rotating machine
# pumpingStation
Wet-well basin model and pump orchestration node for EVOLV.
The detailed documentation lives in [`wiki/`](wiki/):
- [`wiki/functional-description.md`](wiki/functional-description.md) defines the shared basin model, pipe reference semantics, safety points, net-flow selection, and child registration behaviour.
- [`wiki/modes/`](wiki/modes/) documents control-mode-specific behaviour such as the level-linear `startLevel` demand ramp.
- [`wiki/diagrams/basin-model.drawio.svg`](wiki/diagrams/basin-model.drawio.svg) is the current source of truth for the generic basin model.

View File

@@ -9,9 +9,9 @@ All docs and diagrams for this node live in this folder so they version-lock wit
## 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.
Editable draw.io SVGs live in [`diagrams/`](diagrams/). See [`diagrams/README.md`](diagrams/README.md) for the editing workflow — open the `.drawio.svg` in [draw.io](https://app.diagrams.net/), edit it, then export back to SVG with the source embedded.
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/).
The basin model is the shared physical canvas ([`diagrams/basin-model.drawio.svg`](diagrams/basin-model.drawio.svg)); per-mode transfer-function diagrams live under [`diagrams/modes/`](diagrams/modes/). Mode-specific thresholds such as `startLevel` belong in those mode diagrams, not in the generic basin model.
## Part of

View File

@@ -1,15 +1,15 @@
# Diagrams
Editable source diagrams for the pumpingStation wiki. Each diagram is a **`.drawio` + `.drawio.svg` pair**, so anyone can edit the source in [draw.io](https://app.diagrams.net/) without touching any Markdown.
Editable source diagrams for the pumpingStation wiki. The current diagrams are **`.drawio.svg` files with the draw.io source embedded**, so anyone can edit the SVG directly in [draw.io](https://app.diagrams.net/) without touching any Markdown.
## Why two files?
## File roles
| File | Role |
|---|---|
| `<name>.drawio` | Native draw.io XML. The canonical source. |
| `<name>.drawio` | Optional native draw.io XML source, if a diagram also keeps a standalone source file. |
| `<name>.drawio.svg` | SVG export of the same diagram (with source embedded). What the wiki actually renders, and what round-trips back into draw.io. |
Checking both in means the wiki renders for everyone, and the next editor picks up from exactly where the last one left off.
An optional standalone `.drawio` file can be committed beside the SVG, but the embedded-source SVG is enough for the wiki to render and for the next editor to pick up from exactly where the last one left off.
## Editing workflow
@@ -18,7 +18,7 @@ Checking both in means the wiki renders for everyone, and the next editor picks
git clone https://gitea.wbd-rd.nl/RnD/pumpingStation.git
cd pumpingStation/wiki/diagrams
```
2. **Open** the `.drawio` file in draw.io:
2. **Open** the `.drawio.svg` file in draw.io:
- Web: [app.diagrams.net](https://app.diagrams.net/) → *Open Existing Diagram*, or drag-and-drop.
- Desktop: [drawio-desktop](https://github.com/jgraph/drawio-desktop/releases).
3. **Edit** — move shapes, change labels, adjust layout.
@@ -26,9 +26,9 @@ Checking both in means the wiki renders for everyone, and the next editor picks
- `File → Export as → SVG…`
- Check **Include a copy of my diagram** ← this is what lets future edits round-trip through the SVG.
- Save next to the source as `<name>.drawio.svg` (overwrite).
5. **Commit & push** both files:
5. **Commit & push** the edited SVG, plus the `.drawio` file if one exists:
```bash
git add wiki/diagrams/<name>.drawio wiki/diagrams/<name>.drawio.svg
git add wiki/diagrams/<name>.drawio.svg
git commit -m "Update <name>: <what changed>"
git push
```
@@ -50,22 +50,22 @@ Use a descriptive `alt` text; it's the fallback if the SVG fails and it shows up
| Diagram | Shows |
|---|---|
| `basin-model` | Physical basin cross-section — walls, pipes at their real heights, control thresholds cutting across, zone labels |
| `control-zones` | Vertical level axis ("thermometer") for `levelbased` mode — STOP / DEAD ZONE / RUN with demand ramp |
| `basin-model` | Shared physical basin cross-section — walls, pipe reference heights, derived safety zones, storage/dead volumes |
| `modes/basin-mode-level-linear` | Level-linear control mode — `startLevel`, demand ramp, threshold-shift behaviour |
| `control-zones` | Legacy vertical level axis ("thermometer") for `levelbased` mode — STOP / DEAD ZONE / RUN with demand ramp |
| `safety-rules` | Dry-run vs overfill rule asymmetry — which children stop, which keep running |
## Making a brand-new diagram
1. Open draw.io, start blank.
2. Draw it.
3. `File → Save As…` → `wiki/diagrams/<name>.drawio`.
4. `File → Export as → SVG…` with **Include a copy of my diagram** checked → save as `wiki/diagrams/<name>.drawio.svg`.
5. Reference from the wiki page with `![alt](diagrams/<name>.drawio.svg)`.
6. Add an entry to the table above.
7. Commit all three files together (`.drawio`, `.drawio.svg`, updated `.md`).
3. `File → Export as → SVG…` with **Include a copy of my diagram** checked → save as `wiki/diagrams/<name>.drawio.svg`.
4. Reference from the wiki page with `![alt](diagrams/<name>.drawio.svg)`.
5. Add an entry to the table above.
6. Commit the new `.drawio.svg` and updated `.md` together.
## These starters are rough
The `.drawio` files and their matching `.drawio.svg` exports committed here are **placeholders** — layout is approximate, colors and fonts are defaults, no fine alignment. They're meant to be a starting point; open them in draw.io and refine.
Some diagrams are still rough — layout is approximate, colors and fonts may be defaults, and alignment may need refinement. They're meant to be improved in draw.io as the model settles.
Both formats are round-trippable: open either the `.drawio` or the `.drawio.svg` in draw.io and it will load the editable model. (The SVG has the drawio XML embedded in a `content="…"` attribute on the root `<svg>` element — that's what lets draw.io re-open its own SVG exports.)
Open the `.drawio.svg` in draw.io and it will load the editable model. The SVG has the draw.io XML embedded in a `content="…"` attribute on the root `<svg>` element — that's what lets draw.io re-open its own SVG exports.

View File

@@ -1,109 +0,0 @@
<mxfile host="app.diagrams.net" modified="2026-04-22T12:00:00.000Z" agent="Claude Code placeholder" etag="initial" version="22.0.0" type="device">
<diagram name="basin-model" id="basinModel">
<mxGraphModel dx="1200" dy="900" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="900" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="title" value="Basin model — physical layout + control thresholds" style="text;html=1;fontSize=16;fontStyle=1;align=center;" vertex="1" parent="1">
<mxGeometry x="200" y="20" width="500" height="30" as="geometry" />
</mxCell>
<mxCell id="tank" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#E6F2FF;strokeColor=#000000;strokeWidth=2;" vertex="1" parent="1">
<mxGeometry x="300" y="80" width="260" height="520" as="geometry" />
</mxCell>
<mxCell id="deadvol" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#9FC5E8;strokeColor=none;" vertex="1" parent="1">
<mxGeometry x="302" y="550" width="256" height="48" as="geometry" />
</mxCell>
<mxCell id="freeboard_label" value="freeboard" style="text;html=1;fontSize=11;fontStyle=2;align=center;" vertex="1" parent="1">
<mxGeometry x="310" y="90" width="240" height="20" as="geometry" />
</mxCell>
<mxCell id="overflow_line" value="" style="endArrow=none;html=1;strokeColor=#B22222;dashed=1;strokeWidth=2;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="250" y="145" as="sourcePoint" />
<mxPoint x="620" y="145" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="overflow_label_l" value="heightOverflow" style="text;html=1;fontSize=12;align=right;fontColor=#B22222;" vertex="1" parent="1">
<mxGeometry x="140" y="130" width="100" height="20" as="geometry" />
</mxCell>
<mxCell id="overflow_label_r" value="spill → measure" style="text;html=1;fontSize=12;align=left;fontColor=#B22222;" vertex="1" parent="1">
<mxGeometry x="630" y="130" width="140" height="20" as="geometry" />
</mxCell>
<mxCell id="maxflow_line" value="" style="endArrow=none;html=1;strokeColor=#D68910;dashed=1;strokeWidth=2;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="250" y="200" as="sourcePoint" />
<mxPoint x="620" y="200" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="maxflow_label_l" value="maxFlowLevel" style="text;html=1;fontSize=12;align=right;fontColor=#D68910;" vertex="1" parent="1">
<mxGeometry x="140" y="185" width="100" height="20" as="geometry" />
</mxCell>
<mxCell id="scaling_label" value="SCALING RANGE&#10;(levelbased: demand ramps 0→100%)" style="text;html=1;fontSize=11;fontStyle=2;align=center;" vertex="1" parent="1">
<mxGeometry x="310" y="255" width="240" height="40" as="geometry" />
</mxCell>
<mxCell id="startlevel_line" value="" style="endArrow=none;html=1;strokeColor=#1E8449;dashed=1;strokeWidth=2;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="250" y="345" as="sourcePoint" />
<mxPoint x="620" y="345" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="startlevel_label_l" value="startLevel" style="text;html=1;fontSize=12;align=right;fontColor=#1E8449;" vertex="1" parent="1">
<mxGeometry x="140" y="330" width="100" height="20" as="geometry" />
</mxCell>
<mxCell id="deadzone_label" value="DEAD ZONE&#10;(hysteresis — keep last cmd)" style="text;html=1;fontSize=11;fontStyle=2;align=center;" vertex="1" parent="1">
<mxGeometry x="310" y="360" width="240" height="40" as="geometry" />
</mxCell>
<mxCell id="inflow_arrow" value="" style="endArrow=classic;html=1;strokeColor=#1F4E79;strokeWidth=3;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="150" y="410" as="sourcePoint" />
<mxPoint x="300" y="410" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="inflow_label" value="INFLOW" style="text;html=1;fontSize=13;fontStyle=1;align=left;fontColor=#1F4E79;" vertex="1" parent="1">
<mxGeometry x="90" y="395" width="70" height="20" as="geometry" />
</mxCell>
<mxCell id="inlet_label" value="heightInlet" style="text;html=1;fontSize=12;align=left;fontColor=#1F4E79;" vertex="1" parent="1">
<mxGeometry x="570" y="400" width="90" height="20" as="geometry" />
</mxCell>
<mxCell id="stoplevel_line" value="" style="endArrow=none;html=1;strokeColor=#6C3483;dashed=1;strokeWidth=2;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="250" y="465" as="sourcePoint" />
<mxPoint x="620" y="465" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="stoplevel_label_l" value="stopLevel" style="text;html=1;fontSize=12;align=right;fontColor=#6C3483;" vertex="1" parent="1">
<mxGeometry x="140" y="450" width="100" height="20" as="geometry" />
</mxCell>
<mxCell id="stoplevel_label_r" value="unconditional STOP" style="text;html=1;fontSize=12;align=left;fontColor=#6C3483;" vertex="1" parent="1">
<mxGeometry x="630" y="450" width="160" height="20" as="geometry" />
</mxCell>
<mxCell id="buffer_label" value="BUFFER" style="text;html=1;fontSize=11;fontStyle=2;align=center;" vertex="1" parent="1">
<mxGeometry x="310" y="490" width="240" height="20" as="geometry" />
</mxCell>
<mxCell id="outflow_arrow" value="" style="endArrow=classic;html=1;strokeColor=#1F4E79;strokeWidth=3;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="560" y="540" as="sourcePoint" />
<mxPoint x="720" y="540" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="outflow_label" value="OUTFLOW" style="text;html=1;fontSize=13;fontStyle=1;align=left;fontColor=#1F4E79;" vertex="1" parent="1">
<mxGeometry x="730" y="525" width="80" height="20" as="geometry" />
</mxCell>
<mxCell id="outlet_label_l" value="heightOutlet" style="text;html=1;fontSize=12;align=right;fontColor=#B22222;" vertex="1" parent="1">
<mxGeometry x="140" y="525" width="100" height="20" as="geometry" />
</mxCell>
<mxCell id="outlet_label_r" value="dry-run trip" style="text;html=1;fontSize=12;align=left;fontColor=#B22222;" vertex="1" parent="1">
<mxGeometry x="730" y="550" width="120" height="20" as="geometry" />
</mxCell>
<mxCell id="deadvol_label" value="dead volume" style="text;html=1;fontSize=11;fontStyle=2;align=center;" vertex="1" parent="1">
<mxGeometry x="310" y="560" width="240" height="20" as="geometry" />
</mxCell>
<mxCell id="floor_label" value="floor (0)" style="text;html=1;fontSize=11;align=right;" vertex="1" parent="1">
<mxGeometry x="190" y="590" width="50" height="20" as="geometry" />
</mxCell>
<mxCell id="basin_label" value="heightBasin" style="text;html=1;fontSize=11;align=right;" vertex="1" parent="1">
<mxGeometry x="180" y="70" width="60" height="20" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 661 KiB

After

Width:  |  Height:  |  Size: 686 KiB

View File

@@ -1,102 +0,0 @@
<mxfile host="app.diagrams.net" modified="2026-04-22T12:00:00.000Z" agent="Claude Code placeholder" etag="initial" version="22.0.0" type="device">
<diagram name="control-zones" id="controlZones">
<mxGraphModel dx="1000" dy="800" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="700" pageHeight="800" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="title" value="levelbased mode — three zones" style="text;html=1;fontSize=16;fontStyle=1;align=center;" vertex="1" parent="1">
<mxGeometry x="100" y="20" width="500" height="30" as="geometry" />
</mxCell>
<mxCell id="axis" value="" style="endArrow=classic;html=1;strokeColor=#000;strokeWidth=2;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="280" y="600" as="sourcePoint" />
<mxPoint x="280" y="80" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="axis_label" value="level" style="text;html=1;fontSize=13;fontStyle=1;align=left;" vertex="1" parent="1">
<mxGeometry x="240" y="60" width="50" height="20" as="geometry" />
</mxCell>
<mxCell id="overflow" value="heightOverflow — weir crest (spill → measure)" style="text;html=1;fontSize=12;align=left;fontColor=#B22222;" vertex="1" parent="1">
<mxGeometry x="300" y="130" width="380" height="20" as="geometry" />
</mxCell>
<mxCell id="overflow_tick" value="" style="endArrow=none;html=1;strokeColor=#B22222;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="270" y="140" as="sourcePoint" />
<mxPoint x="290" y="140" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="run_band" value="RUN — linear 0 → 100 %" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#E8F5E9;strokeColor=#1E8449;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="300" y="160" width="220" height="110" as="geometry" />
</mxCell>
<mxCell id="maxflow" value="maxFlowLevel — 100 % demand" style="text;html=1;fontSize=12;align=left;fontColor=#D68910;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="300" y="265" width="300" height="20" as="geometry" />
</mxCell>
<mxCell id="maxflow_tick" value="" style="endArrow=none;html=1;strokeColor=#D68910;strokeWidth=2;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="265" y="275" as="sourcePoint" />
<mxPoint x="295" y="275" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="ramp_label" value="(ramp — demand scales linearly with level)" style="text;html=1;fontSize=11;align=left;fontStyle=2;" vertex="1" parent="1">
<mxGeometry x="300" y="300" width="320" height="20" as="geometry" />
</mxCell>
<mxCell id="startlevel" value="startLevel — 0 % demand (ramp starts)" style="text;html=1;fontSize=12;align=left;fontColor=#1E8449;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="300" y="335" width="340" height="20" as="geometry" />
</mxCell>
<mxCell id="start_tick" value="" style="endArrow=none;html=1;strokeColor=#1E8449;strokeWidth=2;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="265" y="345" as="sourcePoint" />
<mxPoint x="295" y="345" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="dead_band" value="DEAD ZONE — hysteresis, keep last cmd" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#FFF8E1;strokeColor=#F57C00;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="300" y="360" width="220" height="80" as="geometry" />
</mxCell>
<mxCell id="inlet" value="heightInlet — inflow pipe" style="text;html=1;fontSize=12;align=left;fontColor=#1F4E79;" vertex="1" parent="1">
<mxGeometry x="300" y="395" width="300" height="20" as="geometry" />
</mxCell>
<mxCell id="inlet_tick" value="" style="endArrow=none;html=1;strokeColor=#1F4E79;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="270" y="405" as="sourcePoint" />
<mxPoint x="290" y="405" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="stoplevel" value="stopLevel — unconditional STOP" style="text;html=1;fontSize=12;align=left;fontColor=#6C3483;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="300" y="440" width="300" height="20" as="geometry" />
</mxCell>
<mxCell id="stop_tick" value="" style="endArrow=none;html=1;strokeColor=#6C3483;strokeWidth=2;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="265" y="450" as="sourcePoint" />
<mxPoint x="295" y="450" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="stop_band" value="pumps OFF (MGC shutdown)" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#F4ECF7;strokeColor=#6C3483;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="300" y="465" width="220" height="80" as="geometry" />
</mxCell>
<mxCell id="outlet" value="heightOutlet — outflow pipe (dry-run trip here)" style="text;html=1;fontSize=12;align=left;fontColor=#B22222;" vertex="1" parent="1">
<mxGeometry x="300" y="510" width="360" height="20" as="geometry" />
</mxCell>
<mxCell id="outlet_tick" value="" style="endArrow=none;html=1;strokeColor=#B22222;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="270" y="520" as="sourcePoint" />
<mxPoint x="290" y="520" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="floor" value="0 (floor)" style="text;html=1;fontSize=11;align=left;" vertex="1" parent="1">
<mxGeometry x="300" y="580" width="60" height="20" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 271 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1,58 +0,0 @@
<mxfile host="app.diagrams.net" modified="2026-04-22T12:00:00.000Z" agent="Claude Code placeholder" etag="initial" version="22.0.0" type="device">
<diagram name="safety-rules" id="safetyRules">
<mxGraphModel dx="1200" dy="700" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="900" pageHeight="700" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="title" value="Safety rules — asymmetric by direction" style="text;html=1;fontSize=16;fontStyle=1;align=center;" vertex="1" parent="1">
<mxGeometry x="150" y="20" width="600" height="30" as="geometry" />
</mxCell>
<mxCell id="dryrun_box" value="DRY-RUN&#10;(direction = draining)" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#FFF3E0;strokeColor=#E65100;strokeWidth=2;fontSize=14;fontStyle=1;verticalAlign=top;" vertex="1" parent="1">
<mxGeometry x="80" y="80" width="340" height="340" as="geometry" />
</mxCell>
<mxCell id="dr_upstream" value="upstream children — KEEP" style="text;html=1;fontSize=13;align=left;" vertex="1" parent="1">
<mxGeometry x="100" y="140" width="300" height="24" as="geometry" />
</mxCell>
<mxCell id="dr_downstream" value="downstream children — STOP" style="text;html=1;fontSize=13;align=left;fontStyle=1;fontColor=#E65100;" vertex="1" parent="1">
<mxGeometry x="100" y="170" width="300" height="24" as="geometry" />
</mxCell>
<mxCell id="dr_machinegroups" value="machineGroups — STOP" style="text;html=1;fontSize=13;align=left;fontStyle=1;fontColor=#E65100;" vertex="1" parent="1">
<mxGeometry x="100" y="200" width="300" height="24" as="geometry" />
</mxCell>
<mxCell id="dr_control" value="control loop — BLOCKED" style="text;html=1;fontSize=13;align=left;fontStyle=1;fontColor=#E65100;" vertex="1" parent="1">
<mxGeometry x="100" y="230" width="300" height="24" as="geometry" />
</mxCell>
<mxCell id="dr_note" value="safetyControllerActive = true&#10;&#10;Pumps must stop before sucking air." style="text;html=1;fontSize=12;align=left;fontStyle=2;" vertex="1" parent="1">
<mxGeometry x="100" y="290" width="300" height="80" as="geometry" />
</mxCell>
<mxCell id="overfill_box" value="OVERFILL&#10;(direction = filling)" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#FFEBEE;strokeColor=#C62828;strokeWidth=2;fontSize=14;fontStyle=1;verticalAlign=top;" vertex="1" parent="1">
<mxGeometry x="480" y="80" width="340" height="340" as="geometry" />
</mxCell>
<mxCell id="of_upstream" value="upstream children — STOP ⚠" style="text;html=1;fontSize=13;align=left;fontStyle=1;fontColor=#C62828;" vertex="1" parent="1">
<mxGeometry x="500" y="140" width="300" height="24" as="geometry" />
</mxCell>
<mxCell id="of_downstream" value="downstream children — KEEP" style="text;html=1;fontSize=13;align=left;" vertex="1" parent="1">
<mxGeometry x="500" y="170" width="300" height="24" as="geometry" />
</mxCell>
<mxCell id="of_machinegroups" value="machineGroups — KEEP" style="text;html=1;fontSize=13;align=left;" vertex="1" parent="1">
<mxGeometry x="500" y="200" width="300" height="24" as="geometry" />
</mxCell>
<mxCell id="of_control" value="control loop — ACTIVE" style="text;html=1;fontSize=13;align=left;" vertex="1" parent="1">
<mxGeometry x="500" y="230" width="300" height="24" as="geometry" />
</mxCell>
<mxCell id="of_note" value="Level control keeps commanding downstream MGC.&#10;&#10;⚠ &quot;upstream STOP&quot; is only correct in a cascaded layout. In a gravity-sewer station the inflow can&apos;t be stopped — log the spill instead." style="text;html=1;fontSize=12;align=left;fontStyle=2;" vertex="1" parent="1">
<mxGeometry x="500" y="290" width="300" height="120" as="geometry" />
</mxCell>
<mxCell id="trigger_title" value="Triggers (either condition fires the rule):" style="text;html=1;fontSize=13;fontStyle=1;align=left;" vertex="1" parent="1">
<mxGeometry x="80" y="450" width="740" height="20" as="geometry" />
</mxCell>
<mxCell id="trigger_list" value="• vol &lt; triggerLowVol (triggerLowVol = minVol × (1 + pct/100))&#10;• vol &gt; triggerHighVol (triggerHighVol = maxVolOverflow × pct/100)&#10;• remainingTime &lt; timeleftToFullOrEmptyThresholdSeconds (if enabled)" style="text;html=1;fontSize=12;align=left;" vertex="1" parent="1">
<mxGeometry x="80" y="480" width="740" height="80" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

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. |

View File

@@ -20,9 +20,9 @@ The simplest and most widely deployed control strategy. Demand is a direct, *sta
## Diagram
![Level-based mode — demand vs level transfer function](../diagrams/modes/levelbased.drawio.svg)
![Level-linear basin mode — demand vs level transfer function](../diagrams/modes/basin-mode-level-linear.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).*
*Editable source: [`../diagrams/modes/basin-mode-level-linear.drawio.svg`](../diagrams/modes/basin-mode-level-linear.drawio.svg) (drag into [draw.io](https://app.diagrams.net/) — it round-trips).*
## Inputs