Waterschap Brabantse Delta · R&D-lab · uitleg

EVOLV · monster — één bemonsteringsrun: van debiet tot puls

eigenaar R&D-lab laatst getest 2026-07-17 versie 2.0 concept
monster is de EVOLV-besturingsnode voor een monsternamekast op een RWZI. Het is een pulsteller, geen analyzer: de node telt pulsen — elke puls één deelmonster van 0,05 L — over een bemonsteringsvenster, en bepaalt de pulscadans debietproportioneel uit gemeten/handmatig debiet, regenverwachting (Open-Meteo) en het AQUON-rooster, met bewaking van emmer-constraints (volume, gewicht, cooldown). Hij analyseert geen constituenten (NH4/NO3/COD/TSS) — daarvoor koppel je een aparte analyzer-node. De node is tick-gedreven (elke 1000 ms) en levert proces-, Influx- en registratie-output op drie poorten. Deze pagina volgt letterlijk wat de code doet aan de hand van één voorbeeldrun en een interactieve figuur die de pulsintegrator getrouw naspeelt.

Doel & rol in de keten

monster stuurt debietproportionele bemonstering: hoe meer water passeert, hoe sneller de pulsen elkaar opvolgen, zodat het verzamelmonster het werkelijke debiet representeert (nodes/monster/README.md:5-9). In de S88-classificatie is de node een Unit (lane L4, groepskleur #50a8d9; nodes/monster/CLAUDE.md). Als child registreert hij zich bij zijn parent via de Port-2 child.register-handshake; als parent aanvaardt hij zelf alleen measurement-kinderen met asset.type = 'flow' (src/specificClass.js:37,99-108). Functionele rol samplingCabinet, softwareType monster (configs/monster.json).

Pulsteller, geen analyse. README, CLAUDE.md en CONTRACT.md leggen de scope vast als "pulse counter — measures sampled volumes and pulse cadence; does not analyze constituents". Er zitten dan ook geen analyseparameters in code of config — alleen AQUON-monsterpuntcodes als rooster-filter. Let op: het klasse-commentaar src/specificClass.js:3 noemt de node nog "multi-parameter biological process monitoring" — dat is verouderd/onjuist en spreekt de vastgelegde scope tegen; de code zelf telt uitsluitend pulsen (code-observatie 2026-07-17).

Tick-gedreven. De adapter zet static tickInterval = 1000: elke seconde draait Monster.tick() (debiet ophalen → volume integreren → eventueel een puls). Output verschijnt dus op het seconde-ritme, niet event-gedreven. Bij een gezet tickInterval heeft het interne notifyOutputChanged()-event geen listener — de outputs komen uitsluitend uit de interval-callback en de emit is hier effectief een no-op (zelfde patroon als measurement; src/specificClass.js:152-158; BaseNodeAdapter.js:136-153).

Rode draad door de hele pagina: een monsternamekast met minVolume 5 L, maxWeight 23 kg, emptyWeightBucket 3maxVolume 20 L; targetVolume = 5·√(20/5) = √(min·max) = 10 LtargetPuls = round(10/0,05) = 200; absMaxPuls = round(55/0,05) = 1100. Alle getallen zijn met node tegen parameters.js + samplingProgram.js doorgerekend (testgevallen 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 de rode-draadkast (targetVolume 10 L / targetPuls 200). Elke bewering draagt een pad:regel-chip. Poortlabels ["process","dbase","parent"].

Stuurinput · 1 poort
Bericht
Poortenoutput · 3 poorten
Volledige poort-referentie
Poortcontract (labels ["process","dbase","parent"], monster.html:53-56). Poort 0/1 zijn delta-gecomprimeerd: alleen gewijzigde velden; geen wijziging → geen bericht (outputUtils.js:17-41). Object-waarden (behalve Date) worden ge-JSON-stringified.
PoortLabelBelangrijkste keys / gedrag
0process Delta-snapshot (src/io/output.js:30-65) bovenop de geflattende measurements: pulse, running, bucketVol, bucketWeight, sumPuls, m3PerPuls (+ alias m3PerPulse), m3Total, q, predFlow, predM3PerSec, predictedRateM3h, pulsesRemaining, pulseFraction, flowToNextPulseM3, timeToNextPulseSec, targetVolumeM3, targetProgressPct, targetDeltaL/targetDeltaM3, missedSamples, sampleCooldownMs, minSampleIntervalSec, nominalFlowMin, flowMax, invalidFlowBounds, maxVolume, minVolume, nextDate, daysPerYear, timePassed, timeLeft, sumRain, avgRain.
1dbase Zelfde snapshot via de influxdb-formatter, met config-tags (id, softwareType, role, positionVsParent, uuid, tagcode, geoLocation, category, type, model, unit; lege/undefined gedropt). Eigen delta-state per format (outputUtils.js:87-133).
2parent Eenmalig bij opstart (na 100 ms): { topic:'child.register', payload:node.id, positionVsParent, distance } (BaseNodeAdapter.js:117-131); daarna ongebruikt.

Wie praat met wie

De parent staat boven (registratie via draad, poort 2); measurement-kinderen (flow) staan onder en komen als in-process events binnen. De omringende e2e-flow (gestippeld, buiten de node zelf) levert de externe bronnen — regen (Open-Meteo), rooster (AQUON SFTP-CSV) en debiet (PLC/measurement) — en verwerkt de node-output: het pulscommando naar de PLC plus telemetrie naar InfluxDB / Z-Info. Onderscheid dus wat de node doet (midden) van wat de keten doet (gestippeld).

parent-node groep / RWZI-unit monster deze node · S88 Unit · tick 1000 ms Open-Meteo regen (keten) AQUON SFTP-CSV rooster (keten) PLC / measurement debiet (keten) PLC pulscommando (keten) InfluxDB / Z-Info telemetrie (keten) flow measurement up/down/atEquipment flow measurement asset.type = flow p2 registratie rain / schedule / flow (verbs) p0/p1 → keten in-process events (measurement.emitter → flow.measured.*)
registratie · via draad (poort 2) telemetrie · via draad (poort 0/1) in-process events · measurement-emitter omringende keten · buiten deze node (e2e-flow)

De node zelf doet: verbs valideren, debiet blenden, regen aggregeren, het rooster lezen, de pulsintegrator draaien en de drie poorten vullen. De keten (gestippeld) haalt de regen op bij Open-Meteo, het rooster via AQUON SFTP, het debiet uit een PLC/measurement, en zet de output door naar de PLC (puls) en naar InfluxDB/Z-Info (telemetrie/rapportage). Die externe knopen zitten alleen in de voorbeeldflow examples/02-integrated-e2e.json, niet in de node (examples/README.md:12-17).

De reis van één bemonsteringsrun

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 parameters.js/samplingProgram.js doorgerekend) en het codepad. Stap 7 draagt het deling-door-nul-randgeval — het kernpunt van deze node.

Kernlogica: doelvolume & pulscadans

Doelvolume = meetkundig gemiddelde. De grenzen komen uit src/parameters/parameters.js:10-17: maxVolume = maxWeight − emptyWeightBucket (kg ≈ L). Het doelvolume is niet het rekenkundige, maar het meetkundig gemiddelde van min en max — het ligt daardoor logaritmisch tussen de grenzen en overweegt de kleine emmer niet:

Vtarget= Vmin· VmaxVmin = Vmin·Vmax , Ntarget= round(Vtarget0,05)
SymboolBetekenisEenheidHerkomst
Vtargetdoelvolume emmer einde run (targetVolume) — bij 5–20 L = 10 LLparameters.js:15
Vminminimaal emmervolume (minVolume, default 5)Lconfig constraints.minVolume
VmaxmaxWeight − emptyWeightBucket — bij 23−3 = 20 LLparameters.js:11
Ntargetpulsdoel (targetPuls) — round(10/0,05) = 200; absMaxPuls = round(55/0,05) = 1100parameters.js:14,16

Pulscadans (met de deling-door-nul-val). Bij run-start wordt het volume-per-puls vastgezet uit de voorspelling; daarna integreert elke tick het passerende volume en vuurt een puls zodra de integrator 1 bereikt (samplingProgram.js:41,105):

mpuls= round(VpredNtarget) , pp+ (q/3600)·Δtmpuls , puls bij p1sumPuls<1100
SymboolBetekenisEenheidHerkomst
mpulsvolume per puls (m3PerPuls), geheel afgerondwordt 0 zodra Vpred < 0,5·NtargetsamplingProgram.js:41
Vpredvoorspeld venstervolume (predFlow) = max(0, debiet · samplingHours)samplingProgram.js:14-21
ppulsintegrator (temp_pulse); bij een puls: p ← p − 1samplingProgram.js:85,105
qeffectief debiet (blend gemeten/handmatig)m³/hflowTracker.js:49-56
Δtwall-clock sinds vorige tick (eerste tick: 0)ssamplingProgram.js:23-27

Debietproportioneel, interactief. m3PerPuls is bij run-start vastgezet uit de voorspelde inflow; tijdens de run drijft het werkelijke debiet de integrator. Hoger werkelijk debiet ⇒ snellere pulsen; loopt het de cooldown voorbij, dan worden pulsen geblokkeerd en tellen ze als missedSamples. Zet samplingtime op 0 (de editor-default!) en predFlow wordt 0 → m3PerPuls = 0 → de integrator deelt door nul.

6 m³/h
6 m³/h
24 h
23 kg
60 s
Uitkomst van de run
Wat je ziet
minVolume vast op 5 L. De trap is bucketVol(t) — elke puls +0,05 L. De targetVolume-lijn (√(min·max)) en het maxVolume-plafond staan ingetekend. Werkelijk > voorspeld debiet ⇒ overschot & missedSamples; samplingtime = 0m3PerPuls = 0 ⇒ deling door nul.

Randgevallen & aandachtspunten

Alle punten zijn code-observaties d.d. 2026-07-17 (statisch, niet in runtime geverifieerd), elk met pad:regel. Duidingen zijn expliciet als aanname gemarkeerd.

⚠ Deling door nul — kernpunt
m3PerPuls = round(predFlow / targetPuls) wordt 0 zodra predFlow < 0,5·targetPuls (= < 100 m³ bij targetPuls 200) — bijvoorbeeld bij samplingtime = 0, de editor-default. Daarna is temp_pulse += m3PerTick / m3PerPuls een deling door 0 → Infinity; er is geen guard. Bij Infinity vuurt de puls elke cooldown-periode tot absMaxPuls (1100). aanname: of dit live optreedt is onzeker (samplingProgram.js:41,105; brief §8.1).
Meetkundig gemiddelde
targetVolume = minVolume·√(maxVolume/minVolume) = √(min·max), niet het rekenkundige gemiddelde. Bij 5–20 L geeft dat 10 L (rekenkundig zou 12,5 L zijn) — het doel ligt logaritmisch tussen de grenzen.
Verouderd klasse-commentaar
Het klasse-commentaar noemt de node "multi-parameter biological process monitoring"; dat is fout. README/CLAUDE.md/CONTRACT.md én de code zeggen: pulse counter, geen analyse (NH4/NO3/COD/TSS).
Tick-driven no-op notify
notifyOutputChanged() emit 'output-changed', maar bij een gezet tickInterval luistert niemand daarnaar — output komt uit de interval-callback. De emit is een no-op (zoals measurement).
Grove cadans door round
Math.round op m³/puls stapt per 1 m³ en kan bij lage volumes fors afwijken van targetPuls. In de figuur: predFlow 144 geeft round(0,72) = m3PerPuls 1 i.p.v. 0,72 → grovere puls → ~144 i.p.v. 200 pulsen → onderschot (7,2 L vs. targetVolume 10 L).
Dode config-keys
samplingperiod, subSampleVolume, storageTemperature, flowmeter, closedSystem, intakeSpeed, intakeDiameter staan in het schema maar worden nergens gelezen (grep bevestigd). Ook predFactor = 0.7 en currHour gezet-maar-ongebruikt; minPuls/maxPuls berekend maar niet sturend (specificClass.js:50,56).
Eenheids- & default-drift
samplingtime: schema zegt "(in seconds)", editor "(h)" en de code rekent in uren (·3600·1000) — de schema-beschrijving is fout. maxweight: editor-default 22 (→ maxVolume 19) vs. schema/adapter 23 (→ 20). aanname: mogelijk onbedoeld.
Regen bevroren tijdens run
Her-aggregatie alleen bij !running; een run gebruikt de regenindex van vóór de start. lastRainUpdate wordt wél ververst, dus de staleness-guard (>2 u) blijft "vers" ook al is de aggregatie oud.
nextDate gemengd type
nextDate is een Date-object (init, +1 jaar) óf een epoch-getal (uit het rooster). Vóór de eerste schedule is het undefinedDate.now() >= undefined is false → alleen handmatige start mogelijk.
Roostervalidatie alleen rij 0
updateMonsternametijden checkt alleen value[0] op de vijf velden; een array met één geldige eerste rij en corrupte rest wordt geaccepteerd.
httpAdmin zonder guard
/monster/menu.js en /monster/configData.js hebben geen RED.auth.needsPermission, terwijl .claude/rules/security.md dat voorschrijft (referentie edgeIO).
Geen output-manifest
Er is geen test/_output-manifest.md en geen dedicated output-*.test.js met populated/degraded-states per Port-0/1-key (.claude/rules/output-coverage.md niet ingevuld voor deze node).

Toestanden & configuratie

Er is geen formele state- of mode-machine: de toestand is een set booleans/getallen op het domein — running, pulse, i_start (startgate) en invalidFlowBounds (specificClass.js:63-96). Run-levenscyclus (samplingProgram.js:92-111): start zodra i_start gezet is óf now ≥ nextDate én !running, mits validateFlowBounds slaagt (min≥0, max>0, min<max, eindig); _beginRun zet m3PerPuls + stop_time = now + samplingtime·3600·1000; de tick integreert; na stop_time reset _endRun alles. De statusbadge (io/statusBadge.js:13-26): rood ring bij invalidFlowBounds, geel ring SAMPLING (Ns) + niveau bij actieve cooldown, groen dot AI: RUNNING bij normaal bemonsteren, grijs AI: IDLE bij idle. De verbs mode en modelPrediction zijn "reserved": setMode bestaat niet (no-op via typeof-guard), terwijl setModelPrediction wél predFlow zet (handlers.js:39-45).

Belangrijkste editor-velden (monster.html:12-50). Volledig schema: nodes/generalFunctions/src/configs/monster.json. Vast in code (niet configureerbaar): volume_pulse = 0,05 L per puls en cap_volume = 55 L (specificClass.js:78,82).
VeldDefault (editor)Betekenis
samplingtime0lengte bemonsteringsvenster in uren (default 0 = deling-door-nul-val)
minvolume5minimaal emmervolume (L)
maxweight22maximaal emmergewicht (kg); schema/adapter = 23 — zie drift
emptyWeightBucket3leeggewicht emmer (kg)
nominalFlowMin / flowMax0 / 0onder-/bovengrens voorspellingsband (m³/h)
maxRainRef10regenindex (mm) die op flowMax mapt
minSampleIntervalSec60pulscooldown (s); snellere pulsen → missedSamples
aquon_sample_name""AQUON-monsterpuntcode (dropdown menu 'aquon'); filtert het rooster (default in code '112100')

De AQUON-dropdown wordt gevuld via het MenuManager-menu 'aquon' (monster.js:19) uit de namespaces monsterSamples (codes + omschrijvingen) en monsterSpecs; bij selectie vult de editor de constraint-velden voor uit de spec (merge defaults ← per-sample), maar de velden blijven bewerkbaar (monster.html:94-165). In de huidige dataset zijn alle bySample-specs identiek aan de defaults.

Verder lezen

Bron statische code-analyse van de werkkopie RnD/EVOLV (super-repo 74c4089, 2026-07-03) — submodule monster geanalyseerd @ c4fb9baaf371, plus het feitendossier van deze analyse. Analysedatum 2026-07-17.
Versheids-herverificatie op 2026-07-17 is git fetch gedraaid: de submodule-tip origin/main = c4fb9baaf371 — identiek aan de geanalyseerde werkkopie. De aandachtspunten hierboven beschrijven die actuele status.
Methode statische code-analyse (geen runtime-verificatie); elk feit draagt een pad:regel-verwijzing. De interactieve figuur en de console zijn getrouwe herimplementaties, vooraf met node tegen parameters.js en samplingProgram.js doorgerekend (testgevallen als commentaar in de pagina-JS); aannames zijn expliciet zo gelabeld.
Beperking beschrijft uitsluitend de geanalyseerde revisie; gedrag op andere branches of deployments kan afwijken.
Contact R&D-lab · lab.wbd-rd.nl