Waterschap Brabantse Delta · R&D-lab · uitleg

EVOLV · diffuser — van luchtdebiet naar zuurstofoverdracht

eigenaar R&D-lab laatst getest 2026-07-17 versie 2.0 concept
diffuser modelleert één fijnbellige beluchtingszone: de gaszijde van een veld membraan-diffuserelementen op de bodem van een actiefslibreactor. Uit een gemeten luchtdebiet rekent hij — via een leverancierscurve (SSOTR en drukverlies versus specifieke flux, opgelost bij de geconfigureerde bottom-coverage) — de zuurstofoverdracht (kg O₂/h) en het drukverlies (mbar). Hij stuurt geen blower aan: hij rapporteert wat een gegeven luchtdebiet "koopt". De node is event-driven (geen tick-loop): hij rekent op setter-inputs, niet op een klok. Deze pagina volgt letterlijk wat de code doet aan de hand van één voorbeeld, een interactief contract-paneel en een curve-verkenner die de kernfuncties getrouw naspeelt.

Doel & rol in de keten

diffuser is een S88-Equipment Module (leaf, lane L3, kleur #86bbdd; CLAUDE.md, tabel "S88 classification"). Hij hangt als child onder een reactor / process cell: bij opstart registreert hij zich via de Port-2 child.register-handshake bij de parent, en hij registreert zelf géén children (CONTRACT.md:95-98). Naast oKgo2H is zijn belangrijkste product de zone-OTR (oZoneOtr), expliciet bedoeld als input voor de zuurstofbalans van de parent-reactor (CONTRACT.md:57-58). Defaults: softwareType 'diffuser', role 'Aeration diffuser', positionVsParent 'atEquipment' (generalFunctions/src/configs/diffuser.json:63-89).

De node is event-drivenstatic tickInterval = null: elke setter roept _recalculate() aan, die bij een echte wijziging notifyOutputChanged() vuurt; alleen de status-badge wordt elke 1000 ms gepolld (src/nodeClass.js:7-13; src/specificClass.js:83; BaseNodeAdapter.js:103-110,134-154). Output verschijnt dus alleen wanneer een input daadwerkelijk iets verandert — niet op een klok. Er is geen S88-state-machine: de toestand is een klein vlaggenmodel (idle/warning/alarm, src/specificClass.js:19-21).

Rode draad door de hele pagina: een zone van 100 elementen (GVA ELASTOX-R, 0,18 m²/element ⇒ 18 m² membraan), bottom-coverage 15 %, waterhoogte 5 m, α 0,65, header 500 mbar, zonevolume 1200 m³. Eén commando: msg.command = { flow: { value: 400, unit: 'm3/h' } }. Alle getallen in de rode draad zijn met node tegen de echte domainklasse (src/specificClass.js) en het echte curvebestand (gva-elastox-r.json) doorgerekend; de standalone reimpl in deze pagina matcht de node exact (testcases in de pagina-JS). Illustratieve aannames zijn expliciet zo gelabeld.

Praat met deze node

Het I/O-contract in één oogopslag: kies een verb, stel de waarde 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-draadzone (GVA ELASTOX-R). Elke bewering draagt een pad:regel-chip. Poortlabels ["process","dbase","parent"]; input-poort label control. Alle verbs lopen via het canonieke envelope msg.command = { <verb>: {value}, meta }; een legacy msg.topic-bericht wordt met warn gedropt (CONTRACT.md:6-30).

Stuurinput · 1 poort
Bericht
Poortenoutput · 3 poorten
Volledige poort-referentie
Poortcontract (labels ["process","dbase","parent"], diffuser.html:36-38; input-label control, :37). Poort 0 is delta-gecomprimeerd: alleen gewijzigde velden; géén wijziging ⇒ géén bericht (outputUtils.js:18-67). Sinds "Phase 6" drie i.p.v. vier outputs — de aparte reactor-controlpoort topic:'OTR' is opgegaan in poort 0 als oZoneOtr (CONTRACT.md:71-79).
PoortLabelBelangrijkste keys
0process Delta-gecomprimeerde snapshot uit getOutput() (src/specificClass.js:220-238): echo iPressure, iMWater, iFlow; genormaliseerd nFlow (Nm³/h, 2 dec.); curve-resultaat oOtr (g O₂/Nm³/m), oPLoss (= o_p_total, mbar), oKgo2H (kg O₂/h); oFlowElement (Nm³/h per element), oFluxPerM2 (Nm³/(h·m² membraan)), efficiency (0–100), slope, oZoneOtr; toestand idle (bool), warning (string[]), alarm (string[]).
1dbase Zelfde getOutput()-payload via de influxdb-formatter (measurement = node-naam; tags = geflattende config-metadata, lege/null-tags gedropt; fields = de gewijzigde waarden) (outputUtils.js:44-113; CONTRACT.md:62-64). Geen alwaysEmitFields.
2parent Eenmalig bij startup { topic:'child.register', payload:node.id, positionVsParent:'atEquipment', distance:null } — dit handshake-pad blijft platformbreed op legacy msg.topic (BaseNodeAdapter.js:117-132).

Wie praat met wie

De parent-reactor staat boven; de diffuser is een leaf — hij heeft geen eigen kinderen. Registratie loopt omhoog via poort 2 (draad), telemetrie via poort 0/1 (draad). De zone-OTR (oZoneOtr) op poort 0 is expliciet bedoeld als voeding voor de zuurstofbalans van de parent. Het gemeten luchtdebiet komt buiten deze repo vandaan (blower/veld) en bereikt de node als flow-commando.

reactor / process cell parent — O₂-balans diffuser deze node · S88 Equipment (leaf) blower / veld buiten deze repo p2 registratie p0/p1 → proces / Influx oZoneOtr → O₂-balans luchtdebiet → flow-commando
registratie · via draad (poort 2) telemetrie / zone-OTR · via draad (poort 0/1) veld → node · buiten deze repo

De reis van één luchtflow-update

Klik een stap in de lijst (of gebruik ◀ ▶). Boven zie je waar in de rekenketen je bent; rechts wat er gebeurt, de waarde van het rode-draadvoorbeeld op dat punt (met node tegen de echte domainklasse doorgerekend), en het codepad.

De leverancierscurves, interactief

De node lost per stap een leverancierscurve op: SSOTR (otr_curve) en drukverlies (p_curve) versus de specifieke flux J [Nm³/(h·m² membraan)] — de canonieke x-as van élk curvebestand. Kies een model, schuif de flux-werkpuntmarker. De otr_curve is geïndexeerd op bottom-coverage-families: bij een multi-coverage set (alleen aerostrip-phoenix) worden de twee omsluitende coverage-curves elk op de flux geïnterpoleerd en daarna lineair geblend naar rato van waar density tussen de keys ligt (_interpolateCurveByDensity, src/specificClass.js:93-121). Bij een single-coverage set (GVA, Jäger; key '0') is de coverage-schuif inert — de lookup negeert density. De p_curve is altijd single-family (density-argument 0, :168). Interpolatie is lineair, met endpoint-clamping (interpolation.js:116-117,173-200).

33 Nm³/(h·m²)
15 %
5 m 0,65

Dichtheid, drukken & het curve-model — de valkuilen

Vier code-observaties d.d. 2026-07-17, elk in de code geverifieerd. Ze beschrijven wat de code doet; de duiding is expliciet gelabeld.

oZoneOtr ×1000
getReactorOtr(V) = o_kgo2_h × 1000 × 24 / V — met o_kgo2_h in kg/h geeft ×1000 gram, dus dimensioneel g O₂/m³/dag. CONTRACT.md:57-58 en docs/otr-model.html:190 noemen het "kg O₂/m³/dag": factor 1000 verschil. Rode draad: 893,4 (code) = 0,8936 kg/m³/d (docs). aanname: docs verouderd, niet de code (src/specificClass.js:240-244).
Antoine mmHg/hPa
De verzadigingsdruk es = 10^(8,07131 − 1730,63/(233,426+t)) geeft water-mmHg, terwijl e·100 verderop e als hPa behandelt (src/specificClass.js:143-147). In de praktijk onschadelijk: beide aanroepen gebruiken RH = 0e = 0, dus pd = pPa exact (:42,157).
std-conditie 20 vs 0 °C
De code normaliseert op 20 °C en declareert units: { Nm3: { temp: 20, … } } (:266), maar de curvebestanden declareren stdAir: { temp_C: 0, … } (gva-elastox-r.json:8). aanname: mogelijk onbedoeld.
blend neemt onderste family
In het geblende multi-coverage-geval worden slope, minX en maxX alléén van de onderste coverage-curve genomen, niet geblend — alleen value, minY, maxY blenden (src/specificClass.js:118-119). aanname: mogelijk onbedoeld.
headerPressure verandert het antwoord
Genormaliseerde flow n_flow = (o_kg/n_kg)·flow met o_kg = ρ(atm+header, 0, 20) en n_kg = ρ(1013,25, 0, 20). Bij header 500 mbar: ratio 1,493 ⇒ 400 m³/h → 597,38 Nm³/h. Daarom verandert headerPressure het antwoord bij gelijk bedrijfsdebiet (:156-159; docs/otr-model.html:83).
y-as is per meter diepte
De curve-y is yAxisBasis: "ssotr-g-per-Nm3-per-m" (gva-elastox-r.json:13) — g O₂/Nm³ per meter inblaasdiepte. Daarom klopt de vermenigvuldiging met waterhoogte h in o_kgo2_h = OTR·n_flow·h·α [g→kg] (:175-176).
membraanoppervlak = placeholder
GVA-default: 0,18 m²/element is een placeholder, gespiegeld aan Jäger TD-65 — "those numbers will shift the moment we get a real GVA sheet" (gva-elastox-r.json:_meta). Config > curve-_meta > fallback 0,18 m² (:34-40).

Toestanden & configuratie

Er is geen S88-state-machine. De toestand is een klein vlaggenmodel: idle is true bij constructie en zodra i_flow ≤ 0 — dan worden alle afgeleide outputs genuld en warning/alarm gewist, in plaats van buiten de curve te extrapoleren (src/specificClass.js:19,70-78). warning en alarm bewaken de specifieke flux tegen het meetbereik van de ΔP-curve met hysterese: warning ±2 %, alarm ±10 % (:20-21,189-202). Badge-prioriteit: alarm (rood) > warning (geel, "⚠ …") > idle (grijs) > groen met tekst "<oKgo2H> kg o2 / h" (:213-218). Alle input loopt via het canonieke verb-envelope; de zes verbs (flow, density, waterHeight, headerPressure, elements, alfaFactor) dispatchen 1-op-1 naar de setters (src/commands/index.js:15-68; handlers.js:14-19).

Belangrijkste editor-velden (diffuser.html:9-33). Volledige defaults/enums: generalFunctions/src/configs/diffuser.json. ⚠️ buildDomainConfig leest ook i_pressure, i_local_atm_pressure, i_water_density, membraneAreaPerElement (src/nodeClass.js:30-37), maar géén van deze vier heeft een editorveld — via de editor vallen ze altijd op de fallback; headerPressure is runtime alleen via het gelijknamige commando te zetten.
VeldDefaultBetekenis
name"" (verplicht)Zonenaam; label wordt name_number, anders model of 'diffuser' (diffuser.html:40-43).
i_elements1Aantal diffuserelementen (afgerond, > 0 via _posInt).
i_diff_density15Bottom coverage [%] — fractie van de tankbodem bezet door membraan; sleutel van de curve-familie (diffuser.html:89-93).
i_m_water0Waterhoogte boven de diffusers [m] (herberekent statische druk).
alfaf0.7Alfa-factor (afvalwater vs. schoon water).
i_zone_volume0 (optioneel)Zonevolume [m³] voor oZoneOtr; 0 ⇒ oZoneOtr = 0.
model (asset)'gva-elastox-r'Curve-model via de asset-cascade; fallback-keten asset.model → model → 'gva-elastox-r', ook bij onbekend model (src/specificClass.js:246-259).
processOutputFormat / dbaseOutputFormat'process' / 'influxdb'Formatter poort 0 / poort 1 (diffuser.html:17-18,109-125).

Aandachtspunten uit de code-analyse

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

Verder lezen

Bron statische code-analyse van de werkkopie RnD/EVOLV (super-repo 74c4089, 2026-07-03) — submodule diffuser @ 4a9f5b4a50f7, plus het feitendossier van deze analyse. Analysedatum 2026-07-17.
Versheids-herverificatie op 2026-07-17 is git fetch gedraaid: de submodule-tip diffuser origin/main is 4a9f5b4a50f7 — identiek aan de geanalyseerde werkkopie. Geen open fix-branches voor deze node meegenomen.
Methode statische code-analyse (geen runtime-verificatie); elk feit draagt een pad:regel-verwijzing. De curve-lookup, dichtheids-normalisatie en OTR zijn vooraf met node tegen de echte domainklasse (src/specificClass.js) en de echte curvebestanden doorgerekend — de standalone reimpl in de pagina-JS matcht de node exact (testgevallen als commentaar in de JS); illustratieve aannames zijn expliciet zo gelabeld.
Beperking beschrijft de geanalyseerde revisie; gedrag op andere branches of deployments kan afwijken.
Contact R&D-lab · lab.wbd-rd.nl