# diffuser ![code-ref](https://img.shields.io/badge/code--ref-4973a8b-blue) ![s88](https://img.shields.io/badge/S88-Equipment_Module-86bbdd) ![status](https://img.shields.io/badge/status-pending--review-yellow) A `diffuser` models a single aeration-diffuser zone — the gas-side dynamics of a fine-bubble grid sitting under a reactor's water column. Given a header pressure, water height, alpha factor, element count and incoming air flow, it normalises the air to Nm³/h, interpolates a supplier OTR curve (oxygen transfer rate vs specific flux) plus a ΔP curve, and emits oxygen-transfer power (kg O₂/h), efficiency, per-element flow, and a reactor-zone OTR. Used as a leaf Equipment Module under a `reactor` (or any aeration train). > [!NOTE] > Pending full node review (2026-05). Content reflects `CONTRACT.md`, `src/commands/index.js`, `src/specificClass.js` and `generalFunctions/src/configs/diffuser.json` only. Test scaffolding under `test/basic/`, `test/integration/`, `test/edge/` is still placeholder-level — a domain-test pass remains TODO. --- ## At a glance | Thing | Value | |:---|:---| | What it represents | One aeration-diffuser zone — the gas-side OTR + ΔP model of a fine-bubble grid | | S88 level | Equipment Module | | Use it when | You have a supplier OTR / ΔP curve and need oxygen-transfer + head-loss telemetry from a measured air flow | | Don't use it for | Coarse-bubble / jet aeration without a fine-bubble curve, or when the blower already publishes OTR (you'd duplicate the calc) | | Children it accepts | None — diffuser is a leaf | | Parents it talks to | `reactor` (typical), or any node that consumes `child.register` from Port 2 | --- ## How it fits ```mermaid flowchart LR blower[blower / MGC /
dashboard slider]:::unit -->|data.flow
Nm³/h| diff[diffuser
Equipment]:::equip setters[dashboard setters /
setup tab]:::ctrl -->|set.density
set.water-height
set.header-pressure
set.elements
set.alfa-factor| diff diff -->|child.register
positionVsParent=atEquipment| reactor[reactor / parent
Unit]:::unit diff -.->|Port 0: oOtr,
oKgo2H, oZoneOtr,
efficiency, slope| reactor classDef unit fill:#50a8d9,color:#000 classDef equip fill:#86bbdd,color:#000 classDef ctrl fill:#a9daee,color:#000 ``` S88 colours are anchored in `.claude/rules/node-red-flow-layout.md`. Per the layout rule, diffuser lives on lane **L3** of the Process Plant tab, wrapped in a `#86bbdd` Node-RED group. --- ## Try it — 3-minute demo Import the basic example flow, deploy, and drive a single diffuser zone through the OTR curve. ```bash curl -X POST -H 'Content-Type: application/json' \ --data @nodes/diffuser/examples/basic.flow.json \ http://localhost:1880/flow ``` > [!NOTE] > Example flows currently shipped: `examples/basic.flow.json`, `examples/integration.flow.json`, `examples/edge.flow.json`. The `examples/README.md` is a one-line placeholder ("Placeholder structure"); a proper per-tier README following the rotatingMachine template is TODO. What to send after deploy (each inject maps one-to-one to a topic in [Reference — Contracts](Reference-Contracts#topic-contract)): 1. `set.water-height = 5` — sets a 5 m column above the diffuser. Static head pressure is recomputed; `oPLoss` updates. 2. `set.elements = 100` — declares 100 elements in this zone. Per-element flow + specific flux now scale by this denominator. 3. `set.density = 15` — bottom-coverage percentage; selects which curve family is interpolated. 4. `set.header-pressure = 600` — gauge pressure above atmospheric (mbar). Air-density correction kicks in. 5. `set.alfa-factor = 0.7` — the α correction used in the kg O₂/h calculation. 6. `data.flow = 200` — push 200 Nm³/h into the model. Watch Port 0: `oOtr`, `oKgo2H`, `oFluxPerM2`, `efficiency`, `slope` all populate. Setting `data.flow = 0` flips `idle` back to true and resets the derived outputs. > [!IMPORTANT] > **GIF needed.** Demo recording of steps 1–6 with the live status badge. Save as `wiki/_partial-gifs/diffuser/01-basic-demo.gif`, target ≤ 1 MB after `gifsicle -O3 --lossy=80`. --- ## The six things you'll send | Topic | Aliases | Payload | What it does | |:---|:---|:---|:---| | `data.flow` | `air_flow` | `number` — Nm³/h | Pushes the measured air flow into the model. Clamped to ≥ 0. Triggers a full recompute. | | `set.density` | `density` | `number` — bottom-coverage % | Selects the curve family used for OTR interpolation. Curves multi-keyed by coverage get linearly interpolated; single-key curves are clamped. | | `set.water-height` | `height_water` | `number` — m | Sets the water column above the diffuser. Recomputes static head + total ΔP. Clamped to ≥ 0. | | `set.header-pressure` | `header_pressure` | `number` — mbar gauge | Header (supply) pressure above atmospheric. Feeds air-density correction. | | `set.elements` | `elements` | `number` — integer > 0 | Active element count. Drives per-element flow + total membrane area used for specific flux. | | `set.alfa-factor` | `alfaFactor` | `number` (typically 0–1) | Alpha correction used in the kg O₂/h calculation. | Aliases log a one-time deprecation warning the first time they fire. There are no query topics today (the entire state is on Port 0). --- ## What you'll see come out Sample Port 0 message (delta-compressed, while operational with flow = 200 Nm³/h, 100 elements, water height 5 m): ```json { "topic": "diffuser_1", "payload": { "iFlow": 200, "iPressure": 600, "iMWater": 5, "nFlow": 218.42, "oFlowElement": 2.18, "oFluxPerM2": 12.13, "oOtr": 16.4, "oPLoss": 540.7, "oKgo2H": 12.51, "oZoneOtr": 6.0, "efficiency": 73.2, "slope": 0.044, "idle": false, "warning": [], "alarm": [] } } ``` > [!NOTE] > The numbers above are illustrative — the exact values depend on the loaded supplier curve (`gva-elastox-r` by default). Run the basic example flow to see real values for your asset. | Field | Meaning | |:---|:---| | `iFlow` / `iPressure` / `iMWater` | Echo of the current setter inputs (Nm³/h, mbar, m). | | `nFlow` | Air flow normalised to standard conditions (Nm³/h, T=20 °C, p=1.01325 bar, RH=0). | | `oFlowElement` | `nFlow / elements` — flow per element. | | `oFluxPerM2` | Specific flux through the membrane (Nm³/(h·m²)) — the canonical x-axis of every supplier curve. | | `oOtr` | Interpolated oxygen transfer rate (g O₂ / Nm³). | | `oPLoss` | Total head loss in mbar — static head from the water column plus diffuser ΔP. | | `oKgo2H` | kg O₂ per hour at the current operating point. Uses α-factor and water height. | | `oZoneOtr` | Reactor-zone OTR in kg O₂ / m³ / day. Computed from `oKgo2H` against `diffuser.zoneVolume`; zero when zone volume is unset. | | `efficiency` | Combined OTR / ΔP efficiency (0–100). High OTR + low ΔP → high score. | | `slope` | Local OTR-vs-flux slope (g O₂/Nm³ per Nm³/(h·m²)). Useful as a "we're near the curve knee" indicator. | | `idle` | `true` when `iFlow ≤ 0` — derived predicates only, no FSM. | | `warning` / `alarm` | String arrays describing flow-per-element band excursions (`±2 %` warn / `±10 %` alarm hysteresis, hardcoded). | --- ## Not the same shape as rotatingMachine / measurement Where rotatingMachine emits per-measurement keys (`...`), diffuser uses a flat camelCase schema (`oOtr`, `oKgo2H`, `oFlowElement`, …). The diffuser does **not** currently publish typed `MeasurementContainer` series — parents must consume Port 0 directly. Promoting `oOtr` / `oZoneOtr` to typed measurement series is tracked in [Reference — Limitations](Reference-Limitations#known-limitations). --- ## Need more? | Page | What you'll find | |:---|:---| | [Reference — Contracts](Reference-Contracts) | Full topic contract, config schema, parent registration handshake | | [Reference — Architecture](Reference-Architecture) | Code map, OTR / ΔP pipeline, idle behaviour, output ports | | [Reference — Examples](Reference-Examples) | Shipped example flows + debug recipes | | [Reference — Limitations](Reference-Limitations) | When not to use, known limitations, open questions | [EVOLV master wiki](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Home) · [Topology Patterns](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topology-Patterns) · [Topic Conventions](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topic-Conventions)