# diffuser
  
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)