Waterschap Brabantse Delta · R&D-lab · uitleg
measurement is de sensorlaag van EVOLV: hij zet ruwe input — een analoge
scalar of een digitaal multi-kanaals object — om naar geschaalde, gladgestreken en
outlier-gefilterde meetwaarden, en publiceert die in de gedeelde
MeasurementContainer waar parent-equipmentnodes op meelezen. Elk kanaal draait
één pipeline: outlier → offset → scaling → smoothing → output. De node is
tick-gedreven (vaste 1 s-tick), niet event-gedreven. Deze pagina volgt letterlijk
wat de code doet aan de hand van één voorbeeldsample en een interactieve figuur die de
pipeline getrouw naspeelt.
RnD/EVOLV, submodule measurement
@ c368bf8b2ce7 (== origin/main, herverifieerd 2026-07-17; zie
verantwoording onderaan).measurement is een S88-Control Module (lane L2, kleur
#a9daee; CLAUDE.md §"S88 classification";
CONTRACT.md:83-88) en daarmee een blad in de hiërarchie: hij accepteert
zelf géén kinderen. Als child registreert hij zich via Port 2
child.register bij een parent-equipmentnode — rotatingMachine,
pumpingStation, reactor of monster
(CONTRACT.md:83-88). Parents consumeren de meetwaarden niet via de poorten
maar via events op de gedeelde MeasurementContainer (README.md:5).
De node is tick-gedreven — static tickInterval = 1000
(src/nodeClass.js:12): elke seconde draait de adapter tick() en
daarna _emitOutputs() (BaseNodeAdapter.js:134-148); de statusbadge
ververst óók elke 1000 ms. Er is geen state-machine, alleen de
configuratieschakelaar mode.current = analog | digital
(runtime niet omschakelbaar, specificClass.js:16-23).
Rode draad door de hele pagina: één analoog drukkanaal — scaling
[4, 20] mA → [0, 250] mbar, offset 0,
smoothing mean/10, outlier zScore/3. Eén sample
{value: 12, unit:'mA'} loopt door de pipeline en levert
12 mA → 125,0 mbar. Alle getallen zijn met node tegen de
echte src/channel.js doorgerekend (2400 fuzz-samples matchten exact); de reeks in
de interactieve figuur is synthetisch en zo gelabeld.
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 rode-draadkanaal. Elke
bewering draagt een pad:regel-chip. Poortlabels
["process","dbase","parent"].
tickInterval = 1000 > 0 retourneert
_wireOutputs() vóór het aankoppelen van de output-changed-listener
(BaseNodeAdapter.js:137-153); de notifyOutputChanged()-emits uit de
analoge setter en calculateInput (specificClass.js:140,189) hebben
dus geen luisteraar — een dode event-route. Latentie tot 1 s. Het commentaar in
nodeClass.js:10-11 ("event-driven via de output-changed emit") beschrijft de
feitelijke draad niet.["process","dbase","parent"],
measurement.html:79-82). Poort 0/1 zijn delta-gecomprimeerd: een tick
zonder wijziging levert null → géén bericht (outputUtils.js:17-67).| Poort | Label | Belangrijkste keys |
|---|---|---|
| 0 | process |
Delta-gecomprimeerde snapshot. Analoog (specificClass.js:217-227):
mAbs, mPercent, totalMinValue (Infinity→0),
totalMaxValue (−Infinity→0), totalMinSmooth,
totalMaxSmooth. Digitaal: genest
{channels:{<key>:{key,type,position,unit,mAbs,mPercent,totalMin/MaxValue,totalMin/MaxSmooth}}}
(specificClass.js:128-132; channel.js:90-103) — het geneste
object wordt door de delta-compressor ge-JSON-stringified (outputUtils.js:29-33).
msg.topic = general.name. Formaat
process|json|csv. |
| 1 | dbase |
Zelfde snapshot, formatter uit output.dbase: influxdb
(default) | frost | json | csv. Influx-vorm
{measurement, fields, tags, timestamp} (influxdbFormatter.js:12-20)
met laag-cardinale tags (id, softwareType, role, positionVsParent, uuid, tagcode,
geoLocation_x/y/z, category, type, model, unit; lege waarden gedropt,
outputUtils.js:87-133). |
| 2 | parent |
Eenmalig, 100 ms na start: { topic:'child.register', payload:<node.id>,
positionVsParent, distance } — bewust legacy msg.topic-pad
(transitional handshake, BaseNodeAdapter.js:120-131;
CONTRACT.md:51-55). |
measurement is een blad: geen
kinderen. Hij registreert zich omhoog bij één parent (via draad, poort 2); de meetwaarden
bereiken de parent níet via een draad maar als in-process events op de gedeelde
MeasurementContainer-emitter. De echte sensor (edgeIO/veld) zit buiten deze
repo: veldwaarden komen binnen als measurement-sample of via de simulator.
Bij elke afgeronde output-wijziging voedt het kanaal
de container met type(<type>).variant('measured').position(<position>).value(outputAbs, …, unit)
(channel.js:215-220); de container emit
<type>.measured.<position> (positie lowercase), bv.
pressure.measured.upstream (MeasurementContainer.js:361) — dát is het
in-process pad waarover de parent meeleest.
Klik een stap (of gebruik ◀ ▶). Boven zie je waar in de pipeline je bent; rechts wat er
gebeurt, de waarde van het rode-draadvoorbeeld op dat punt, en het codepad. Let op de
volgorde: de echte runtime-pipeline is outlier → offset → scaling → smoothing
(channel.js:69-88) — niet de volgorde uit de editor-hulptekst (zie het
kern-inzicht verderop).
Deze figuur is de kern van het artifact: een getrouwe herimplementatie van
src/channel.js (node-getest, 2400 fuzz-samples matchten exact) op een
synthetische ruwe reeks (sinus + ruis + een paar spikes). Kies een
smoothing-methode (alle 12), zet outlierdetectie aan met een methode + drempel,
schakel scaling en verschuif de offset. Geplot: de ruwe reeks, de verwerkte reeks
en de verworpen outliers. De pipeline draait in de echte volgorde outlier → offset →
scaling → smoothing; het rolling window bevat gesmoothde waarden.
offset → scaling → smoothing →
outlier (measurement.html:864), maar de echte runtime-volgorde is
outlier → offset → scaling → smoothing (channel.js:69-88).
Gevolg: outlierdetectie kijkt naar de ruwe sample, vóór offset/scaling. Het
pipeline-diagram in de editor toont de runtime-volgorde overigens wél correct
(measurement.html:436-439).tickInterval=1000 wordt de output-changed-listener
nooit aangekoppeld (BaseNodeAdapter.js:137-153); de emits in
specificClass.js:140,189 hebben geen luisteraar. Outputs verschijnen
uitsluitend op het 1 s-tickritme, latentie tot 1 s.
aanname: het commentaar in nodeClass.js:10-11 is
mogelijk onbedoeld achtergebleven._applySmoothing
(channel.js:164) — het bevat dus gesmoothde waarden. De outlier-check
toetst een nieuwe raw sample tegen die smoothed-historie (channel.js:72).
Methodisch discutabel, maar zo gecodeerd.σ=0. De code doet
bewust géén early-return: z = (val===μ ? 0 : Infinity)
(channel.js:125-129) — elke afwijking wordt dan een outlier (z=∞ > thr).
Zie de preset in de figuur hierboven. modifiedZScore doet het omgekeerde: MAD=0 ⇒
nooit outlier (channel.js:142).type = asset.type, unit uit
asset.unit (specificClass.js:54-68). Digitaal: één kanaal per
channels[]-entry, elk met eigen {key,type,position,unit,scaling?,smoothing?,
outlierDetection?}; ontbrekende sub-secties vallen terug op de analoge topniveau-config
(specificClass.js:87-90). mode is config-only, runtime niet
omschakelbaar.README.md:38 claimt mode-keuze "via msg.mode" — er is
geen msg.mode-verwerking in de runtime. CONTRACT.md:76-79 beschrijft
een legacy 'mAbs'-emitter die nergens meer ge-emit wordt. En de
envelope-migratietabel (.claude/rules/command-envelope.md §4) noemt measurement
niet, terwijl de code feitelijk al op het envelope-pad zit
(src/commands/index.js:15-59).Lineaire schaling (indien scaling.enabled; channel.js:150-159),
met v eerst geclampt op [inputMin, inputMax]:
| Symbool | Betekenis | Eenheid | Herkomst |
|---|---|---|---|
| v | ge-offsette, geclampte raw input | raw-eenheid | channel.js:77,157 |
| inmin, inmax | raw-inputbereik | raw-eenheid | scaling.inputMin/inputMax |
| absmin, absmax | procesbereik na schaling | kanaal-unit | scaling.absMin/absMax |
| out | geschaalde waarde | kanaal-unit | channel.js:158 |
Rode draad: v = 12, [4,20]→[0,250] ⇒
out = 0 + (12−4)·250/16 = 125,0 mbar
(node-geverifieerd). Ongeldig inputbereik (≤ 0) → warn + fallback naar [0,1]
(channel.js:151-156).
Smoothing: rolling window (max smoothWindow) met 12 methodes
(channel.js:163-189): none, mean, min,
max, sd (n−1), median,
weightedMovingAverage (gewichten 1..n), lowPass (EMA α=0,2),
highPass (α=0,8), bandPass (lp+hp−x), kalman (vaste gain
g≈0,0909) en savitzkyGolay (5-punts [−3,12,17,12,−3]/35). Methode-namen
worden lowercased gematcht; onbekend → error-log + raw value door. Kalibratie (zero-set,
alleen bij stabiel window): offset = baseline − outputAbs (calibrator.js:50-66).
Er is geen alarm-/limietlogica in deze node.
measurement.html:29-76; schema
nodes/generalFunctions/src/configs/measurement.json). * = required.
UI→domein-mapping in nodeClass.buildDomainConfig() (nodeClass.js:15-40).| Sectie.veld | Default | Betekenis / enum |
|---|---|---|
mode.current | analog | analog / digital (config-only) |
asset.type | pressure | meettype = as in de MeasurementContainer |
scaling.enabled | false | lineaire schaling aan/uit |
scaling.inputMin/inputMax | 0 / 1 * | raw-inputbereik |
scaling.absMin/absMax | 50 / 100 * | procesbereik na schaling |
scaling.offset | 0 | additieve bias/zero (raw-eenheden) |
smoothing.smoothMethod | mean |
none / mean / min / max / sd / lowPass / highPass / weightedMovingAverage / bandPass / median / kalman / savitzkyGolay |
smoothing.smoothWindow | 10 * | venstergrootte (min 1) |
outlierDetection.enabled | false | outlierfilter aan/uit |
outlierDetection.method | zScore | zScore / iqr / modifiedZScore |
outlierDetection.threshold | 3 | drempel bij gekozen methode |
interpolation.percentMin/percentMax | 0 / 100 | percentagebereik mPercent |
calibration.stabilityThreshold | 0.01 | absolute stdDev-grens "stabiel venster" |
channels | [] | digitale kanaaldefinities {key,type,position,unit,…}; genegeerd in analoog |
src/editor/digital-channels.js:28-50). Canonieke types hebben unit-conversie;
custom types (humidity, co2, voc en elk ander onbekend type) gebruiken vrije
unit-tekst zonder conversie.| Type | Unit-suggesties |
|---|---|
pressure | Pa, kPa, MPa, hPa, bar, mbar, torr, psi |
flow | m³/s, m³/h, L/s, L/min, gpm |
power | W, kW, MW, hp |
temperature | C, K, F, R |
volume | mL, L, m³, gal |
length | mm, cm, m, km, in, ft |
mass | mg, g, kg, t, oz, lb |
energy | Wh, kWh, J, kJ, MJ |
humidity / co2 / voc + overig | custom — vrije unit-tekst, geen conversie |
Alle punten zijn code-observaties d.d. 2026-07-17 (statische analyse, niet in runtime
geverifieerd). Elk punt draagt een pad:regel-verwijzing; duiding is expliciet als
aanname gemarkeerd.
tickInterval=1000 heeft
notifyOutputChanged() geen listener; outputlatentie is tot 1 s
(BaseNodeAdapter.js:137-153; specificClass.js:140,189).
aanname: het comment in nodeClass.js:10-11 is
mogelijk onbedoeld achtergebleven.CONTRACT.md:76-79 beschrijft een legacy
'mAbs'-emitter-event dat in de huidige src/ nergens ge-emit wordt.README.md:38 noemt mode-keuze "via
msg.mode"; er is geen msg.mode-verwerking in de runtime-code.channel.js:164), terwijl de outlier-check een nieuwe raw sample
toetst (channel.js:72) — methodisch discutabel, maar zo gecodeerd.simulation.safeCalibrationTime
(configs/measurement.json:415-422) wordt in nodes/measurement/src/
nergens gelezen (aanname: mogelijk elders extern gebruikt).src/ een timeout/watchdog op
uitblijvende samples; onzeker of dit elders in de keten gebeurt.totalMinSmooth/totalMaxSmooth behandelt (0,0) als
"nog niet gezet" (channel.js:199-206); een eerste smoothed waarde van exact 0 is
niet te onderscheiden van niet-geïnitialiseerd..claude/rules/command-envelope.md §4 noemt measurement niet als gemigreerd,
terwijl de code feitelijk op het envelope-pad zit (src/commands/index.js:15-59);
onzeker of dat bewust is.https://lab.wbd-rd.nl/rnd-lab/projects/evolv-measurement.nodes/measurement/CONTRACT.md,
nodes/measurement/README.md en src/channel.js
(RnD/EVOLV op git.wbd-rd.nl).RnD/EVOLV (super-repo 74c4089, 2026-07-03) — submodule
measurement geanalyseerd @ c368bf8b2ce7, plus het feitendossier van
deze analyse. Analysedatum 2026-07-17.git fetch
gedraaid: measurement origin/main = c368bf8b2ce7 — gelijk aan de
geanalyseerde revisie (geen drift).pad:regel-verwijzing. De interactieve figuur en de console zijn
getrouwe herimplementaties, vooraf met node tegen de echte
src/channel.js doorgerekend (testgevallen als commentaar in de pagina-JS): scaling
12 mA op [4,20]→[0,250] = 125,0 mbar; mean/median/kalman op een vaste reeks; zScore σ=0 =
outlier; 2400 fuzz-samples matchten channel.js exact. De reeks in F1 is
synthetisch en zo gelabeld.