Files
pumpingStation/wiki/functional-description.md
2026-05-05 10:38:24 +02:00

23 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 storage volume from basin floor to rim.
Basin Height (m) 1 Physical wall height from floor to rim.
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
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 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.

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

  • 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, with every level measured upward from the basin floor.

Basin model — physical layout with control thresholds

Editable source: diagrams/basin-model.drawio.svg (drag into draw.io; the SVG embeds the editable source). See diagrams/README.md for the edit-and-export workflow.

Generic basin ordering (bottom → top): outflowLevel ≤ dryRunLevel ≤ minLevel < inflowLevel < maxLevel ≤ highVolumeSafetyLevel < overflowLevel ≤ basinHeight.

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.

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:

  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.

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):

  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 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/ 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, high-volume protection tries to preserve distance to actual overflow.

Safety rules — dry-run vs overfill

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

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