Waterschap Brabantse Delta · R&D-lab · uitleg

EVOLV · pumpingStation — één kelder-tick: van balans tot demand

eigenaar R&D-lab laatst getest 2026-07-17 versie 2.0 concept
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.

Doel & rol in de keten

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

Praat met deze node

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.

Stuurinput · 1 poort
Bericht
Poortenoutput · 3 poorten
Volledige poort-referentie
Poortcontract (labels ["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).
PoortLabelBelangrijkste keys
0process 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).
1dbase 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).
2parent 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).

Wie praat met wie

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

parent (optioneel) MGC / geneste PS pumpingStation deze node · S88 Process Cell measurement ×n level/druk/flow → balans machineGroupControl demand-doel (setDemand) rotatingMachine losse pomp · overgeslagen bij groep pumpingStation genest child-station p2 registratie p0/p1 → proces / Influx demand ⬇ group.setDemand(pct,'%') in-process events (measurements.emitter)
registratie · via draad (poort 2) telemetrie · via draad (poort 0/1) demand ⬇ · in-process 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).

De reis van één kelder-tick

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.

Interactieve kelder-simulatie

Dit figuur speelt de flowAggregator-balansV = (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.

120 m³/h
9

De peil→demand-regelcurve, interactief

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

2,0 m
1,0 m 3,8 m
9

Veiligheid, netto-flow & de eigenaardigheden

Safety-asymmetrie — HET inzicht
Safety draait elke tick, ongeacht mode. Dry-run (alleen bij 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).
basinNetSource auto/level/flow
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-conventie (expliciet null)
Poort 0/1 emit nog niet beschikbare keys als expliciete 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.
Ongebruikte config-haakjes
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.
Wall-clock-integrator
Δ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 % is géén uit
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.

Kernmodellen & formules

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

ΔV= (QinQuit) ·Δt
SymboolBetekenisEenheidHerkomst
ΔVvolumeverandering van de kelder per tick (rode draad: +0,00833 m³ bij +30 m³/h over 1 s)flowAggregator.js:112-114
Qinbeste beschikbare instroom (measured vóór predicted, per zijde onafhankelijk)m³/sflowAggregator.js:69-93,105-108
Quitbeste beschikbare uitstroom (idem); spill op positie overflow telt hier niet meem³/sflowAggregator.js:99-108,148-155
Δtverstreken tijd sinds vorige tick, uit wall-clock (Date.now()), negatief geklemd op 0sflowAggregator.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 restant­hoogte × 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]:

y= ln(1+f·x) ln(1+f)
SymboolBetekenisEenheidHerkomst
ydemand-fractie, uitgestuurd als 0–100 % (linear: y = x)src/control/levelBased.js:26-46
xpeil genormaliseerd op [rampFoot = max(startLevel, holdLevel), maxLevel], geklemd op [0,1]levelBased.js:39-45,165-175
fkrommingsfactor 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).

Toestanden, commando's & configuratie

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

Command-vocabulaire (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).
VerbKindParamsEffect
modeset{value} ∈ manual/levelbased/flowbased/nonestrategie wisselen → changeMode (valideert tegen allowedModes)
calibratecmd{target ∈ volume|level, value, unit?}predicted-integrator resetten/ankeren; eenheid target-afhankelijk (m³ resp. m)
inflowdata{value, unit?} → m³/hinstroom in de balans → setManualInflow
outflowdata{value, unit?} → m³/huitstroom in de balans → setManualOutflow
demandset{value, unit?} → m³/hoperator-uitstroomsetpoint; alléén in manual mode
child.registertopologylegacy msg.topic-padchild-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:

Basin, hydraulics, control (levelbased) en safety — belangrijkste velden. ⚠ markeert een editor↔schema-drift of een dode/inconsistente waarde.
Subtree · veldDefault (schema / editor)Betekenis
basin.volume / height50 m³ / 4 mlege-keldervolume en -hoogte → oppervlak A
basin.inflowLevel / outflowLevel / overflowLevel1,5 / 0,2 / 3,8 minstroom-invert, zuigpeil, overstortpeil boven vloer
basin.inletPipeDiameter / outletPipeDiameter0,4 / 0,3 mdrift schema (0,4) vs editor (0,3); runtime ongebruikt
hydraulics.basinBottomRef0 / 1drift schema (0) vs editor (1); runtime ongebruikt
hydraulics.minHeightBasedOnoutletbepaalt minVol (inlet vs outlet)
control.modelevelbased⚠ schema-enum is camelCase (flowBased, pressureBased) terwijl dispatcher + allowedModes lowercase zijn — een camelCase-waarde wordt niet herkend
control.basinNetSourceautonetto-flow-bron: auto / level / flow
control.levelbased.minLevel / startLevel0,3 / 1 mhard-stop; inschakel-rising-edge (mag bewust bóven inflowLevel)
control.levelbased.stopLevel / holdLevelnull / 0,5 · null / 1bewuste schema-null (guard tegen null→0-merge); editor zet 0,5 resp. 1
control.levelbased.maxLevel · curveType · logCurveFactor3,8 m · linear · 9100 %-saturatie; vorm up-curve
safety.dryRunThresholdPercent / highVolumeSafetyThresholdPercent2 % / 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).

Aandachtspunten uit de code-analyse

Alle punten zijn code-observaties d.d. 2026-07-17 (statisch, niet in runtime geverifieerd); elk draagt een pad:regel-verwijzing.

Verder lezen

Bron statische code-analyse van de werkkopie 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.
Methode statische code-analyse (geen runtime-verificatie); elk feit draagt een 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.
Beperking beschrijft de geanalyseerde revisie; gedrag op andere branches of deployments kan afwijken.
Contact R&D-lab · lab.wbd-rd.nl