Files
pumpingStation/wiki/functional-description.md
znetsixe a2189457f6 Rename basin/control thresholds to wiki naming; trim stale comments
Aligns the code with the 5-threshold convention used throughout the
wiki (basin model + per-mode transfer-function diagrams):

  heightInlet       → inflowLevel
  heightOutlet      → outflowLevel
  heightOverflow    → overflowLevel
  stopLevel         → minLevel
  maxFlowLevel      → maxLevel
  minFlowLevel      → removed (collapsed into startLevel; they were
                      always supposed to hold the same value)
  minVolIn          → minVolAtInflow
  minVolOut         → minVolAtOutflow
  maxVolOverflow    → maxVolAtOverflow
  startLevel        → unchanged

Config schema (generalFunctions/src/configs/pumpingStation.json) is
updated in a parallel commit in that submodule.

Also:
- Stripped the ~150-line ASCII basin diagram from initBasinProperties
  JSDoc; it now points at wiki/functional-description.md#basin-model.
- Trimmed the top-of-class JSDoc — the config-sections breakdown was
  drifting from the schema anyway; wiki is now the source of truth.
- Tidied inline comments in _controlLevelBased, _scaleLevelToFlowPercent.
- Editor order reshuffled to match the bottom→top basin order:
  minLevel, startLevel, maxLevel.

Breaking change for saved flows: existing pumpingStation nodes in
production flows reference the old field names and will need to be
re-entered in the editor. No compat shim — node is RnD/trial.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 16:13:59 +02:00

20 KiB
Raw Blame History

title, node, updated, status
title node updated status
pumpingStation — Functional Description pumpingStation 2026-04-22 draft

pumpingStation — Functional Description

The pumpingStation node models an S88 Process Cell: a wet-well basin with inflow and outflow, wrapped around one or more pump controllers. Every second it recomputes the basin's water balance, picks the most trustworthy net-flow source, runs its safety interlocks, and finally commands its children (individual pumps, machineGroupControl, or nested pumping stations) so the level stays inside the safe operating band.

This page is the operator-facing reference, derived from src/specificClass.js. For the 3-tier code layout see EVOLV — Node Architecture; for the atomic pump model see the rotatingMachine wiki.

Diagrams on this page are editable. Sources live in diagrams/ — open the .drawio file in draw.io, export to SVG, commit. See diagrams/README.md.

At a glance

Item Value
Node category EVOLV
S88 level Process Cell (#0c99d9, lane L5)
Inputs 1 (message-driven)
Outputs 3 — process / dbase / parent
Tick period 1 s
Basin model Rectangular prismatic — volume = level × surfaceArea
Canonical units (internal) Pa, m³/s, W, K, m, m³
Control modes implemented levelbased, manual (placeholders for flowbased, pressureBased, percentageBased, powerBased, hybrid)
Default flow dead-band 1e-4 m³/s (≈ 0.36 m³/h)

Lifecycle

  1. Construct. The node merges the user's editor config over the schema defaults, creates the measurement store, and seeds the predicted volume at the basin's operational floor (minVol).
  2. Register children. Sensors, pumps, machine groups, and nested stations register via the Port-2 handshake. The station subscribes only to the highest-level aggregator for predicted flow to avoid double-counting (MGC if present, otherwise the individual pump).
  3. Tick loop (1 s). _updatePredictedVolume → _selectBestNetFlow → _safetyController → _controlLogic → state snapshot → output.

Editor configuration

Every field on the pumpingStation editor maps directly to the config schema in generalFunctions/src/configs/pumpingStation.json.

Basin geometry (section basin)

Field Default Meaning
Basin Volume (m³) 1 Total geometric volume of the empty 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.

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.

Hydraulics (section hydraulics)

Field Default Meaning
Minimum Height Based On outlet outletminVol = outflowLevel × area (includes the buffer). inletminVol = inflowLevel × area (buffer treated as unavailable).
Reference Height NAP Vertical datum: NAP / EVRF / EGM2008. Metadata only — not used in math today.
Basin Bottom (m Refheight) 0 Absolute elevation of the basin floor, for cross-basin comparisons.

Control (section control)

Field Default Meaning
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.
Flow setpoint 0 Flow-based target (m³/h). Placeholder until flowbased is wired.
Deadband 0 Flow-based deadband (m³/h). Placeholder.

Safety (section safety)

Field Default Meaning
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.

Output formats

  • Process Output — format for Port 0 (process / json / csv).
  • Database Output — format for Port 1 (influxdb / json / csv).

Tip — always configure every field. The pumpingStation mixes geometry and control thresholds freely. Leaving overflowLevel at the schema default of 2.5 m while sizing the basin for 10 m walls produces nonsensical fill-percentages and spurious safety events. See the EVOLV flow-layout rules §9 for the completeness rule.

Input topics

All commands enter on the single input port. msg.topic selects the handler; msg.payload carries the argument.

changemode

{ "topic": "changemode", "payload": "manual" }

Switches the active control strategy. The new mode must be in config.control.allowedModes — unknown values are rejected with a warning. Typical transitions: levelbased ⇄ manual for operator override during maintenance.

calibratePredictedVolume

{ "topic": "calibratePredictedVolume", "payload": 3.4 }

Hard-reset the predicted volume time-series to the supplied value (m³). Also rewrites the predicted level (derived from the constant-area geometry) and resets the internal flow-integrator state. Use this when a trustworthy measured level becomes available.

calibratePredictedLevel

{ "topic": "calibratePredictedLevel", "payload": 1.8 }

Same as above, but caller supplies a level (m). The predicted volume is recomputed via volume = level × surfaceArea.

q_in

{ "topic": "q_in", "payload": 300, "unit": "l/s" }

Inject a manual inflow into the basin. Registered as a predicted flow under the synthetic child manual-qin at position in. Useful when no physical inflow sensor is wired but the inflow is known externally (e.g. fed from a sewer model).

Qd

{ "topic": "Qd", "payload": 75 }

Forward a manual demand to every child aggregator (MGC first, then any direct pumps). Only honoured when config.control.mode === 'manual' — in any other mode the command is logged and discarded. Mirrors how rotatingMachine gates commands behind its mode field. The interpretation of the number depends on the child's scaling (absolute = m³/h, normalized = 0100 %).

registerChild

Internal. Child nodes (measurements, rotatingMachines, machineGroupControls, nested pumpingStations) emit this on their Port 2 a few hundred ms after deploy. The station resolves the Node-RED node id back to the source object and registers it via childRegistrationUtils.

Output ports

Port 0 — process data

Delta-compressed payload (only changed fields per tick). Keys follow the standard 4-segment format <type>.<variant>.<position>.<childId> plus a handful of top-level state fields merged in by getOutput():

Key Meaning
volume.predicted.atequipment.default Running predicted volume from the flow integrator (m³).
volume.measured.atequipment.default Volume derived from a measured level sensor (m³).
level.predicted.atequipment.default Predicted level = volume / area (m).
level.measured.<position>.<childId> Raw level sensor reading (m).
volumePercent.predicted.atequipment.default (vol - minVol) / (maxVolAtOverflow - minVol) × 100 (%).
flow.predicted.in.<childId> Inflow contribution from a registered child (m³/s internally; editor unit on output).
flow.predicted.out.<childId> Outflow contribution from a registered child.
flow.measured.<position>.<childId> Flow sensor reading.
netFlowRate.<variant>.atequipment.default Net flow used for control (inflow outflow).
direction filling / draining / steady / unknown.
flowSource Which variant drove the current control cycle (measured, predicted, level:predicted, null).
timeleft Predicted seconds to overflow (while filling) or to dry-run (while draining).
volEmptyBasin, inflowLevel, overflowLevel, maxVol, maxVolAtOverflow, minVol, minVolAtInflow, minVolAtOutflow, minHeightBasedOn Echoes of the basin geometry for dashboards.
percControl Last demand (0100+ %) forwarded to the machine group during level-based control.

Consumers must cache and merge deltas — the example dashboard flows include a reusable function node that does exactly this.

Port 1 — dbase (InfluxDB)

Line-protocol payload for the telemetry bucket. Tags stay low-cardinality (station name, asset type); fields carry the numeric state. See EVOLV — InfluxDB Schema Design.

Port 2 — parent

{ topic: "registerChild", payload: <this-node-id>, positionVsParent, distance } — fired once ~100 ms after deploy so an upstream cascade can discover this station. Nested stations use this to register with an outer pumpingStation parent.

Basin model

The basin is modelled as a rectangular prism with constant cross-section. Everything derives from volume = level × surfaceArea.

Basin model — physical layout with control thresholds

Editable source: diagrams/basin-model.drawio. See diagrams/README.md for the edit-and-export workflow.

Typical ordering (bottom → top): outflowLevel ≤ minLevel < inflowLevel < startLevel < maxLevel ≤ overflowLevel.

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

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:

  outlet (default):                   inlet:

      ●  maxVolAtOverflow                   ●  maxVolAtOverflow
      │                                   │
      ●  inflowLevel                      ●  inflowLevel ─── minVol
      │                                   │
      ●  outflowLevel ──── minVol         ●  outflowLevel
      │                                   │
      ●  floor                            ●  floor

  Buffer counts as usable stock.      Buffer reserved; 0% fill
                                      starts at the inlet.

Net-flow selection

Every tick, _selectBestNetFlow() walks a priority ladder and returns the first net flow that clears the dead-band (|flow| ≥ flowThreshold):

  priority    source               note

  1   ────●  measured.flow         real sensors on inflow/outflow
          │
  2   ────●  predicted.flow        manual q_in + pump-curve outputs
          │
  3   ────●  level:measured        dL/dt × surfaceArea
          │
  4   ────●  level:predicted       dL/dt of the integrator
          │
  5   ────●  steady (fallback)     warn, return { value: 0, source: null }

Both measured and predicted variants are always computed and stored, regardless of which one drives control. The active source surfaces on Port 0 as flowSource, so operators can watch sensor drift (measured diverges from predicted), validate the volume integrator, and diagnose "which source was active when X happened?".

The inflow / outflow alias map is deliberately wide so measurements (upstream/downstream) and predicted-flow subscriptions (in/out) both feed the same aggregator:

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.

Every mode gets its own page under modes/ with a consistent layout (inputs, threshold policy, demand formula, edge cases) so they can be compared side-by-side. Currently:

Mode Status Page
levelbased implemented modes/levelbased.md
manual implemented (via Qd topic)
flowbased, pressureBased, percentageBased, powerBased, hybrid 🚧 placeholder in code

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

Safety rules — dry-run vs overfill

During overfill, 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.

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.

Registration — which children count as flow?

_registerPredictedFlowChild subscribes only to the highest-level aggregator to prevent double-counting.

  Without MGC:                        With MGC:

      [ PumpingStation ]                  [ PumpingStation ]
         │    │    │                              │
         │    │    │                          [  MGC  ]
         │    │    │                          │   │   │
         ●    ●    ●                          ●   ●   ●
        (each pump subscribed               (only MGC is subscribed;
         directly)                           MGC aggregates its pumps)

      N flow subscriptions.               1 flow subscription.
      Risk: double-count if an            Pumps' flow is already
      MGC is added later.                 inside the MGC total.

Measurement children register separately via _registerMeasurementChild and feed the measured variant — they never collide with the predicted-flow subscription. Nested pumpingStation children are always subscribed and expose their net flow at the parent's position.

Node status badge

Updated every second by _updateNodeStatus in nodeClass.js:

⬆️ 42.3% | V=4.57 / 10.80 m³ | net: 180 m³/h | t≈12 min
Symbol Direction Badge colour
⬆️ filling blue
⬇️ draining orange
⏸️ steady green
unknown / missing measurements grey

Example flow

The canonical end-to-end demo lives in the EVOLV superproject at examples/pumpingstation-3pumps-dashboard/. It wires three rotatingMachine pumps beneath an MGC beneath a pumpingStation, with the dashboard layout rule set (see the EVOLV flow-layout rules) — a useful template for any new station.

Troubleshooting

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.
"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.
Pumps keep running during overfill Intended — overfill safety only stops upstream equipment; downstream pumps must drain. To override, switch to manual and set Qd = 0, or issue an emergency-stop at the MGC.
Predicted volume drifts away from measured Flow integrator has no reference — flows might have the wrong sign, or unit is mis-declared. Call calibratePredictedVolume periodically from a measured level.

Running it locally

git clone --recurse-submodules https://gitea.wbd-rd.nl/RnD/EVOLV.git
cd EVOLV
docker compose up -d
# Node-RED: http://localhost:1880   InfluxDB: :8086   Grafana: :3000

Then in Node-RED: Import ▸ Examples ▸ EVOLV ▸ pumpingStation (or open examples/pumpingstation-3pumps-dashboard/flow.json).

Testing

cd nodes/pumpingStation
npm test

Unit tests live in test/specificClass.test.js — construction, basin derivation, measurement registration, net-flow selection, safety interlocks, and calibration.