Waterschap Brabantse Delta · R&D-lab · uitleg
pumpingStation is de S88 Process Cell-orkestrator van het EVOLV-platform:
een natte-kelder-model (wet well) plus pomporkestratie. Elke seconde ontvangt de node
metingen en voorspelde flows van geregistreerde children, draait een kelderbalans
(instroom − uitstroom over de tijd), evalueert dry-run- en overvul-beveiliging en stuurt
een vraag (demand) naar machinegroepen. Anders dan rotatingMachine — dat
event-driven is en een S88-toestandsmachine draait — is dit node tick-driven
(1000 ms) en heeft het géén klassieke FSM: de toestand is samengesteld uit richting,
mode en veiligheidsstatus. Deze pagina volgt letterlijk wat de code doet, aan de hand van
één kelder-tick en een paar interactieve figuren die de kernmodellen getrouw naspelen.
RnD/EVOLV, submodule
pumpingStation (gitlink-pin 9b090d3012c8; de werkkopie draagt
lokale wijzigingen en komt inhoudelijk overeen met origin/main
@ 5719459 — zie verantwoording).pumpingStation beschrijft zichzelf als "S88 Process Cell orchestrator.
Wires the basin / measurement / control / safety modules in configure() and runs
them in tick()" (src/specificClass.js:1-6). S88-niveau Process
Cell, kleur #0c99d9, lane L5 (CLAUDE.md, tabel "S88
classification"). Domein: natte-kelder-model + pomporkestratie (README.md:3).
De node is parent van vier child-softwaretypes: measurement,
machine (rotatingMachine), machinegroup (machineGroupControl) en
pumpingstation (geneste child-stations) — de vier
router.onRegister(...)-handlers in src/specificClass.js:118-128.
valve is uitdrukkelijk géén geregistreerd child-type: er is geen
onRegister('valve', …) en geen enkele verwijzing naar valve in src/
(§1 van het feitendossier); klep-interactie loopt in het platform via andere nodes. Zelf
registreert de station-node zich bij een eventuele bovenliggende parent via poort 2
(BaseNodeAdapter.js:117-131). Het configveld
supportedChildSoftwareTypes (default ["measurement"]) is
documentatief — de registratie filtert er niet op
(childRegistrationUtils.js:22-67).
De node is tick-driven — static tickInterval = 1000: elke seconde
draait tick() de keten flowAggregator.tick() →
safety.evaluate() → (indien niet geblokkeerd) control.dispatch() →
state → notifyOutputChanged() (src/specificClass.js:164-182;
interval in BaseNodeAdapter.js:134-145). Output verschijnt dus elke seconde,
delta-gecomprimeerd — geen event-driven hercompute zoals bij rotatingMachine.
Het per-seconde tikken is nodig omdat de volume-integrator een Δt per seconde
nodig heeft (src/nodeClass.js:5-10).
Rode draad door de hele pagina: één voorbeeldgemaal (expliciet
zo gelabeld; uit de schema/editor-defaults). Lege kelder V = 50 m³, hoogte
4 m → oppervlak A = 12,5 m²; outflowLevel 0,2 m, inflowLevel
1,5 m, overflowLevel 3,8 m; minLevel 0,3 m, startLevel 1,0 m, stopLevel
0,5 m, maxLevel 3,8 m; curveType linear. Scenario: instroom
Qin = 120 m³/h, pompuitstroom
Quit = 90 m³/h → netto +30 m³/h vullend. Alle
getallen zijn met node tegen de echte modules (BasinGeometry, flowAggregator,
levelBased._scaleLevelToFlowPercent) doorgerekend; testgevallen staan als
commentaar in de pagina-JS.
Het I/O-contract in één oogopslag: kies een verb, stel de velden in,
kopieer het exacte injecteerbare msg, en klik Injecteer ▸ om te zien wat
er uit de drie poorten komt — met live doorgerekende waarden op het voorbeeldgemaal.
Elke bewering draagt een pad:regel-chip. Poortlabels
["process","dbase","parent"]. Let op de null-conventie: nog niet
beschikbare keys worden als expliciete null geëmit — anders dan
rotatingMachine/machineGroupControl, waar een ontbrekende key
gewoon afwezig is.
["process","dbase","parent"],
pumpingStation.html:106-108). Conventie: een nog niet beschikbare key wordt
als expliciet null geëmit, niet weggelaten
(test/_output-manifest.md:10-13). Commando-envelope in, plus impliciet
query.units op de reply-poort (BaseNodeAdapter.js:25-49,88-96).| Poort | Label | Belangrijkste keys |
|---|---|---|
| 0 | process |
Delta-gecomprimeerde snapshot (getOutput(),
src/specificClass.js:235-262): platgeslagen metingen
"<type>.<variant>.<positie>[.<childId>]" (o.a.
volume/level/volumePercent/netFlowRate.predicted.atequipment);
keldergeometrie-snapshot (volEmptyBasin, heightBasin, inflowLevel,
outflowLevel, overflowLevel, inletPipeDiameter, outletPipeDiameter, surfaceArea,
maxVol, maxVolAtOverflow, minVolAtInflow, minVolAtOutflow, minVol,
minHeightBasedOn; BasinGeometry.snapshot()); toestand
(direction, flowSource, timeleft, percControl, mode, manualDemand);
afgeleide safety-drempels (dryRunLevel, dryRunSafetyVol,
highVolumeSafetyLevel, highVolumeSafetyVol); spill/underflow
(predictedOverflowVolume, predictedOverflowRate,
predictedUnderflowVolume). |
| 1 | dbase |
Zelfde key-set via de influxdb-formatter (default);
config.general.name is de measurement-tag. Omzetbaar naar
frost / json / csv via output.dbase
(configs/pumpingStation.json:160-184;
BaseNodeAdapter.js:160-162). |
| 2 | parent |
Eenmalig bij startup { topic:'child.register', payload:node.id,
positionVsParent, distance } naar de eigen parent
(BaseNodeAdapter.js:120-131). |
Daarnaast child-facing events zonder poort via
measurements.emitter, patroon
<type>.<variant>.<positie> (o.a.
volume.predicted.atequipment, flow.predicted.in,
overflowVolume/underflowVolume; CONTRACT.md:52-68). De
editor-statusbadge (1 s) toont richting (⬆️ filling / ⬇️ draining / ⏸️ steady),
volume-%, netto-flow in m³/h en in manual Qd=… m³/h
(src/specificClass.js:264-281).
Een optionele parent staat boven (via poort 2),
de vier child-typen onder. Registratie en telemetrie lopen via draden;
kind-metingen en voorspelde child-flows komen als in-process events binnen. De
demand gaat OMLAAG naar machineGroups (in-process group.setDemand) —
losse machines worden overgeslagen zodra er een machineGroup is
(src/specificClass.js:120-126).
setDemand
in-process events · measurement/predicted-flow
measurement-kinderen binden op
"<assetType>.measured.<positie>" met replay-on-subscribe
(een laat registrerende parent mist een eerder sample niet;
src/specificClass.js:290-321). Voorspelde pompuitstroom komt als
flow.predicted.downstream/upstream terug de balans in; downstream
wordt geprefereerd boven atequipment — zelfde totaal, anders dubbeltellen
(src/specificClass.js:323-348).
Klik een stap in de lijst (of gebruik ◀ ▶). Boven zie je waar in de tick-keten je bent;
rechts wat er gebeurt, de waarde van het rode-draadvoorbeeld op dat punt (met
node tegen de echte modules doorgerekend), en het codepad. Dit is een
per-seconde tick-lus — géén S88-toestandsmachine.
Dit figuur speelt de flowAggregator-balans
(ΔV = (Qin − Quit)·Δt) plus de
levelBased up-curve compact na, per seconde vooruit geïntegreerd. Kies of de
regeling Quit bepaalt (peilsturing: up-curve → demand% → pompgroep)
of dat je zelf een vaste Quit instelt. Het bovenpaneel toont
level(t) met de drempellijnen; het onderpaneel de demand%(t) uit de up-curve.
Markers: engagement (peil kruist startLevel) en overstort. De demand→flow-map
gebruikt een voorbeeldgroep (flow.min 40 … flow.max 200 m³/h) — die
interpolatie doet machineGroupControl, niet dit node.
De up-curve in _scaleLevelToFlowPercent mapt het peil, genormaliseerd op
[rampFoot, maxLevel], naar 0–100 % demand: linear
y = x of log
y = ln(1+f·x)/ln(1+f) met f = logCurveFactor
(default 9; src/control/levelBased.js:26-46). rampFoot = max(startLevel,
holdLevel). Schuif de drempels en de factor; de werkpuntmarker staat op het huidige
peil. Log slaat zachter aan (hogere demand bij lager peil).
draining, vol < minVol·(1+dryRunPct/100)):
downstream + groepen uit én blocked = true — de regeling
stopt. Overfill (alleen bij filling,
vol > maxVolAtOverflow·pct/100): alléén upstream uit,
machineGroups blijven expliciet draaien en blocked blijft
false — "they must keep draining". De asymmetrie (droog ⇒ blokkeer,
vol ⇒ blokkeer NIET) is het belangrijkste veiligheidsinzicht
(src/safety/safetyController.js:59-153).auto (default): flow-balans eerst (measured→predicted),
level-rate als fallback. level: het gemeten peilverloop is
grondwaarheid (dV/dt = A·dL/dt) — eerlijk voor een echt gevoed
gemaal met een peilmeter maar zónder instroomsensor. flow: nooit
level-rate. Dead-band ±flowThreshold (1e-4 m³/s ≈ 0,36 m³/h)
bepaalt filling/draining/steady (src/measurement/flowAggregator.js:189-298).null — géén weggelaten key. Dat is anders dan
rotatingMachine/machineGroupControl, waar een ontbrekende
meting simpelweg afwezig is. Eén conventie per node, hier vastgelegd in
test/_output-manifest.md:10-13.hydraulics (staticHead,
maxDischargeHead, pipelineLength, buisdiameters, …),
alarms en simulation staan volledig in het schema maar worden
in src/ nooit in een berekening gebruikt — alleen opgeslagen/
doorgegeven (src/nodeClass.js:20-32,
src/basin/BasinGeometry.js:19-20,38-39). Dit node berekent géén
systeemkromme, opvoerhoogte of werkpunt — het stuurt enkel een demand-percentage; de
hydraulische vertaling doet machineGroupControl.Δt komt uit Date.now()
(flowAggregator.js:97,112-113): klokverschuiving of pauzes vertalen zich
direct in volume-integratiefouten. Negatieve Δt wordt op 0 geklemd.
De integrator seedt bij configure() op basin.minVol
(src/specificClass.js:101-104).0 % demand is de engaged "min flow"-vloer
(MGC's setDemand interpoleert 0 → flow.min), NIET
uitschakelen — dat is altijd een expliciet commando
(src/control/levelBased.js:48-64,221-228). De engagement-gate zit dáárom
buiten het percentage-pad.De kelder is een rechthoekig prisma:
A = Vleeg/hkelder,
V(L) = max(L,0)·A en
L(V) = max(V,0)/A
(src/basin/BasinGeometry.js:23-31,64-72). Voor het voorbeeldgemaal:
A = 50/4 = 12,5 m². Hart van de node is de
predicted-volume-integrator (flowAggregator.js:95-187), per zijde de beste
variant (measured vóór predicted):
| Symbool | Betekenis | Eenheid | Herkomst |
|---|---|---|---|
| ΔV | volumeverandering van de kelder per tick (rode draad: +0,00833 m³ bij +30 m³/h over 1 s) | m³ | flowAggregator.js:112-114 |
| Qin | beste beschikbare instroom (measured vóór predicted, per zijde onafhankelijk) | m³/s | flowAggregator.js:69-93,105-108 |
| Quit | beste beschikbare uitstroom (idem); spill op positie overflow telt hier niet mee | m³/s | flowAggregator.js:99-108,148-155 |
| Δt | verstreken tijd sinds vorige tick, uit wall-clock (Date.now()), negatief geklemd op 0 | s | flowAggregator.js:97,112-113 |
Het volume wordt geklemd: hard fysiek op maxVolAtOverflow
(= overflowLevel·A = 47,5 m³) en 0, operationeel op
dryRunSafetyVol (alleen bij transitie van bovenaf). Excedent boekt cumulatief
als overflowVolume/underflowVolume; tijdens overstort staat een
synthetische spill-flow op positie overflow die in de netto-flow-balans wél
meetelt maar niet in de operationele uitstroomsom
(flowAggregator.js:120-170,207-210). volumePercent is lineaire
interpolatie van V over [minVol → 0 %,
maxVolAtOverflow → 100 %] — bij het voorbeeldgemaal op peil
2,0 m (V = 25 m³) is dat exact 50 %
(flowAggregator.js:180-184). De resterende tijd (ETA) is
restanthoogte × A / |Qnet|
(flowAggregator.js:264-292) — vullend op 2,0 m tot overstort:
1,8·12,5/0,00833 ≈ 2700 s (45 min).
De peilsturing (levelbased) mapt het peil naar 0–100 % demand,
genormaliseerd op [rampFoot, maxLevel]:
| Symbool | Betekenis | Eenheid | Herkomst |
|---|---|---|---|
| y | demand-fractie, uitgestuurd als 0–100 % (linear: y = x) | – | src/control/levelBased.js:26-46 |
| x | peil genormaliseerd op [rampFoot = max(startLevel, holdLevel), maxLevel], geklemd op [0,1] | – | levelBased.js:39-45,165-175 |
| f | krommingsfactor logCurveFactor (default 9; alleen bij curveType='log') | – | levelBased.js:27-32 |
Voor het voorbeeldgemaal op peil 2,0 m (rampFoot 1,0; maxLevel 3,8):
x = (2,0−1,0)/(3,8−1,0) = 0,357 → linear demand 35,7 %
(log met f = 9: 62,5 %). Beide met node tegen
_scaleLevelToFlowPercent geverifieerd. Binnenkomende drukmetingen gaan
door de MeasurementRouter naar peil: L = p/(ρ·g) met
g = 9,80665 m/s² en ρ via CoolProp (bij ontbrekende temperatuur
15 °C, variant assumed; src/measurement/measurementRouter.js:7-9,55-78).
Er is géén klassieke S88-toestandsmachine. De stationstoestand is samengesteld uit
direction (filling|draining|steady, dead-band
flowThreshold), mode (regelstrategie, gevalideerd tegen
control.allowedModes; bij verlaten van manual wordt
_manualDemand gewist) en safetyControllerActive (bij
blocked wordt de control-dispatch die tick overgeslagen;
src/specificClass.js:164-193). De dispatcher kent exact levelbased,
flowbased en manual (src/control/index.js:5-18);
flowbased is een lege placeholder, manual is event-driven via het
demand-commando. Commando's volgen het command-envelope
msg.command = { <verb>: {params}, meta:{origin,at} }.
src/commands/index.js:13-70,
src/commands/handlers.js; CONTRACT.md:8-28). Onbekende verbs
worden luid gedropt; demand buiten manual mode wordt gedropt met debug-log
(handlers.js:90-95).| Verb | Kind | Params | Effect |
|---|---|---|---|
mode | set | {value} ∈ manual/levelbased/flowbased/none | strategie wisselen → changeMode (valideert tegen allowedModes) |
calibrate | cmd | {target ∈ volume|level, value, unit?} | predicted-integrator resetten/ankeren; eenheid target-afhankelijk (m³ resp. m) |
inflow | data | {value, unit?} → m³/h | instroom in de balans → setManualInflow |
outflow | data | {value, unit?} → m³/h | uitstroom in de balans → setManualOutflow |
demand | set | {value, unit?} → m³/h | operator-uitstroomsetpoint; alléén in manual mode |
child.register | topology | legacy msg.topic-pad | child-handshake; expliciet TRANSITIONAL |
Belangrijkste configuratie-subtrees (defaults/schema:
nodes/generalFunctions/src/configs/pumpingStation.json; editor-defaults
pumpingStation.html:28-102; mapping src/nodeClass.js:12-66). Let op
de default-drift editor vs schema en de enum-case-mismatch:
| Subtree · veld | Default (schema / editor) | Betekenis |
|---|---|---|
basin.volume / height | 50 m³ / 4 m | lege-keldervolume en -hoogte → oppervlak A |
basin.inflowLevel / outflowLevel / overflowLevel | 1,5 / 0,2 / 3,8 m | instroom-invert, zuigpeil, overstortpeil boven vloer |
basin.inletPipeDiameter / outletPipeDiameter | ⚠ 0,4 / 0,3 m | drift schema (0,4) vs editor (0,3); runtime ongebruikt |
hydraulics.basinBottomRef | ⚠ 0 / 1 | drift schema (0) vs editor (1); runtime ongebruikt |
hydraulics.minHeightBasedOn | outlet | bepaalt minVol (inlet vs outlet) |
control.mode | levelbased | ⚠ schema-enum is camelCase (flowBased, pressureBased) terwijl dispatcher + allowedModes lowercase zijn — een camelCase-waarde wordt niet herkend |
control.basinNetSource | auto | netto-flow-bron: auto / level / flow |
control.levelbased.minLevel / startLevel | 0,3 / 1 m | hard-stop; inschakel-rising-edge (mag bewust bóven inflowLevel) |
control.levelbased.stopLevel / holdLevel | ⚠ null / 0,5 · null / 1 | bewuste schema-null (guard tegen null→0-merge); editor zet 0,5 resp. 1 |
control.levelbased.maxLevel · curveType · logCurveFactor | 3,8 m · linear · 9 | 100 %-saturatie; vorm up-curve |
safety.dryRunThresholdPercent / highVolumeSafetyThresholdPercent | 2 % / 98 % | marges voor de afgeleide dry-run- resp. high-volume-drempel |
De volledige boom (general, functionality, basin,
hydraulics, control, safety, output, asset, alarms, simulation) staat in
configs/pumpingStation.json. control.pressureBased/flowBased/
percentageBased/powerBased bevatten volledige PID-parametrering zonder
implementatie; manualOverrideTimeoutMinutes en tickIntervalMs
worden in src/ niet gelezen (de runtime-tick is de statische
tickInterval = 1000).
Alle punten zijn code-observaties d.d. 2026-07-17 (statisch, niet in runtime
geverifieerd); elk draagt een pad:regel-verwijzing.
src/specificClass.js:25-28 zegt dat flow/netFlowRate
in m³/h worden geëmit, maar de gedeclareerde unitPolicy.output zet beide op
m3/s (specificClass.js:32-39); alleen de statusbadge converteert
zichtbaar naar m³/h (:272). aanname: mogelijk
dwingt de outputlaag elders alsnog m³/h af — binnen deze node niet zichtbaar.flowbased is een placeholder
(src/control/flowBased.js:1-6);
pressureBased/percentageBased/powerBased/hybrid bestaan alleen in het schema,
inclusief ongebruikte PID-parameters (configs/pumpingStation.json:614-759).control.mode is camelCase
(flowBased, pressureBased;
configs/pumpingStation.json:449-471), terwijl dispatcher en
allowedModes-default lowercase zijn (src/control/index.js:5-9).
Een camelCase-waarde zou niet herkend worden; onzeker of dit in de praktijk optreedt
(de editor gebruikt lowercase).alarms, simulation,
manualOverrideTimeoutMinutes en de hydraulics-velden (staticHead,
pipelineLength, maxDischargeHead, buisdiameters, …) worden in
src/ alleen opgeslagen/doorgegeven, nooit berekend
(src/nodeClass.js:20-32, src/basin/BasinGeometry.js:19-20,38-39).
Dit node berekent bewust geen systeemkromme/werkpunt — dat doet
machineGroupControl.basinBottomRef 1 vs 0; stopLevel/holdLevel
editor-0,5/1 vs schema-null (dat laatste gedocumenteerd als bewuste keuze,
configs/pumpingStation.json:535). De enum-beschrijvingen van
defaultFluid zijn corrupt: ze beschrijven putvormen i.p.v. fluïda
(configs/pumpingStation.json:414-428) — aanname:
mogelijk onbedoeld.Date.now()
(flowAggregator.js:97,112-113); klokverschuiving of pauzes vertalen zich
direct in volume-integratiefouten (negatieve Δt wordt op 0 geklemd).src/control/levelBased.js:263-275) als manual
(src/control/manual.js:33-42) waarschuwen gericht dat demand nergens heen
kan en adviseren een volledige redeploy.Number(null) === 0); de stopLevel-guard (levelBased.js:121-126)
en null-checks in thresholdValidator.js:69-73 en
levelBased.js:169-173 zijn daar verdedigingen tegen.test/edge/-map;
erkende hiaten in test/_output-manifest.md (geen dedicated
poort-0/1/2-contracttests). test/basic/_probe_upstream_emit.test.js oogt als
achtergebleven debug-probe — aanname: onzeker of die bewust
deel van de suite is.https://lab.wbd-rd.nl/rnd-lab/projects/evolv-pumpingstation.CONTRACT.md en test/_output-manifest.md in
de node-repo (RnD/pumpingStation).rotatingMachine (één pomp, event-driven) en
machineGroupControl (demand→pompen, systeemkromme/werkpunt).RnD/EVOLV (super-repo 74c4089, 2026-07-03) — submodule
pumpingStation geanalyseerd @ gitlink-pin 9b090d3012c8. De
werkkopie draagt lokale (ongecommitte) wijzigingen en komt inhoudelijk overeen met
origin/main @ 5719459 (op 2026-07-17 git fetch
gedraaid); de brief-feiten zijn tegen die werkkopie code-geverifieerd. Analysedatum
2026-07-17.pad:regel-verwijzing. De interactieve figuren en de
contract-console zijn getrouwe herimplementaties, vooraf met node tegen de
echte modules (BasinGeometry, flowAggregator, levelBased._scaleLevelToFlowPercent,
thresholdValidator) doorgerekend (testgevallen als commentaar in de pagina-JS);
illustratieve aannames zijn expliciet als "voorbeeldgemaal"/"voorbeeldgroep" gelabeld.