Replaces the prior stub/partial wiki with a Home + Reference-{Architecture,
Contracts,Examples,Limitations} + _Sidebar structure. Topic-contract and
data-model sections wrapped in AUTOGEN markers for the future wiki-gen tool.
Source-vs-spec contradictions surfaced and flagged inline (not silently
fixed). Pending-review notes mark sections that need a full node review.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
183 lines
9.6 KiB
Markdown
183 lines
9.6 KiB
Markdown
# reactor
|
|
|
|
  
|
|
|
|
A `reactor` models a single biological-treatment tank governed by the ASM3 (Activated Sludge Model No. 3) kinetics. It wraps either a CSTR (fully-mixed) or PFR (plug-flow with axial dispersion) integrator, accepts an influent stream + aeration rate, integrates the 13 ASM3 species each tick, and emits the effluent vector for the next Unit downstream (typically a `settler` or another `reactor`). A `diffuser` (Equipment Module) supplies aeration via `data.otr`; `measurement` children supply temperature and (PFR-only) dissolved-oxygen reconciliation.
|
|
|
|
> [!NOTE]
|
|
> Pending full node review (2026-05). Content reflects `CONTRACT.md` and current source only.
|
|
|
|
---
|
|
|
|
## At a glance
|
|
|
|
| Thing | Value |
|
|
|:---|:---|
|
|
| What it represents | One biological-treatment tank running ASM3 kinetics — aerated, anoxic, or anaerobic |
|
|
| S88 level | Unit |
|
|
| Use it when | You need an activated-sludge tank with nitrification / denitrification / heterotrophic growth modelled species-by-species |
|
|
| Don't use it for | Passive equalisation tanks (no reactions), simple residence-time delays (lighter buffer is better), aerobic-only contactors where ASM3's full 13-species vector is overkill |
|
|
| Children it accepts | `measurement` (temperature at equipment; PFR also: dissolved oxygen at numeric distance); upstream `reactor` |
|
|
| Parents / sinks it talks to | downstream `reactor` or `settler` (via `Fluent` on Port 0); `diffuser` pushes `data.otr` in |
|
|
|
|
---
|
|
|
|
## How it fits
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
upstream[reactor<br/>upstream<br/>Unit]:::unit
|
|
rx[reactor<br/>Unit]:::unit
|
|
settler[settler<br/>downstream<br/>Unit]:::unit
|
|
diffuser[diffuser<br/>Equipment]:::equip
|
|
tsens[measurement<br/>temperature<br/>atEquipment]:::ctrl
|
|
osens[measurement<br/>quantity (oxygen)<br/>at numeric distance, PFR only]:::ctrl
|
|
|
|
upstream -.stateChange.-> rx
|
|
rx -->|Fluent inlet=0| settler
|
|
diffuser -->|data.otr| rx
|
|
tsens -.measured.-> rx
|
|
osens -.measured.-> rx
|
|
tsens -->|child.register| rx
|
|
osens -->|child.register| rx
|
|
upstream -->|child.register<br/>positionVsParent=upstream| rx
|
|
|
|
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`.
|
|
|
|
reactor sits on lane **L4** (Unit). The `diffuser` (lane L3) is **not** a registered child — it just pushes aeration via the `data.otr` topic. A reactor chain (multi-stage treatment, e.g. anoxic → aerobic → aerobic) is built by registering each upstream reactor with `positionVsParent: 'upstream'`; downstream reactors then `getEffluent` from the upstream on every `stateChange`.
|
|
|
|
---
|
|
|
|
## Try it — 3-minute demo
|
|
|
|
Import the basic example flow, deploy, and watch a CSTR consume influent over the simulation clock.
|
|
|
|
```bash
|
|
curl -X POST -H 'Content-Type: application/json' \
|
|
--data @nodes/reactor/examples/basic.flow.json \
|
|
http://localhost:1880/flow
|
|
```
|
|
|
|
What to click after deploy (each inject maps one-to-one to a topic in [Reference — Contracts](Reference-Contracts#topic-contract)):
|
|
|
|
1. `data.fluent` — inject an influent stream `{inlet: 0, F: 1000, C: [...13 species...]}` (m³/d, mg/L). The 13 species follow ASM3 ordering.
|
|
2. `data.temperature` — set reactor temperature (default 20 °C; nitrification rates depend on this).
|
|
3. `data.otr` (if `kla` is `NaN`) **or** rely on the configured `kla` for internal aeration.
|
|
4. `data.clock` — push wall-clock `msg.timestamp` to advance the integrator. The engine computes `n_iter = floor(speedUpFactor × Δt_wall / timeStep_days)` internal Euler / FD steps and integrates them in one shot.
|
|
5. Watch Port 0 (`Fluent` envelope on every advance) and Port 1 (InfluxDB scalar fields: `flow_total`, `temperature`, `S_O`…`X_TS`).
|
|
|
|
> [!IMPORTANT]
|
|
> **GIF needed.** Demo recording of steps 1–5 with `S_NH` falling and `S_NO` rising (nitrification proceeding). Save as `wiki/_partial-gifs/reactor/01-basic-cstr.gif`, target ≤ 1 MB after `gifsicle -O3 --lossy=80`.
|
|
|
|
---
|
|
|
|
## The six things you'll send
|
|
|
|
| Topic | Aliases | Payload | What it does |
|
|
|:---|:---|:---|:---|
|
|
| `data.clock` | `clock` | `{timestamp: ms}` (or use `msg.timestamp`) | Advance the integrator. `updateState` computes how many internal steps fit between `currentTime` and the supplied timestamp (scaled by `speedUpFactor`) and runs them. |
|
|
| `data.fluent` | `Fluent` | `{inlet: number, F: number, C: number[13]}` | Set the per-inlet flow rate (`F`) and concentration vector (`C`). Stored in `engine.Fs[inlet]` / `engine.Cs_in[inlet]`. |
|
|
| `data.otr` | `OTR` | numeric | Set the externally-supplied oxygen transfer rate. Used when `kla` is `NaN`; ignored otherwise (internal mass transfer takes over). |
|
|
| `data.temperature` | `Temperature` | numeric or `{value: number}` | Set `engine.temperature` (°C). Non-numeric payloads are warned and ignored. |
|
|
| `data.dispersion` | `Dispersion` | numeric | **PFR only** — set axial dispersion coefficient `D` (m²/d). Triggers Peclet / Courant guard warnings on the next `updateState`. |
|
|
| `child.register` | `registerChild` | child node id (string) | Register a sibling node (`measurement`, upstream `reactor`) with this reactor. Port 2 wiring does this automatically in normal flows. |
|
|
|
|
> [!NOTE]
|
|
> Pending full node review (2026-05). reactor's command surface is data-push only — there is **no FSM, no setpoint, no mode**. The kinetics engine runs continuous-state ODE / PDE integration; the only stateful event is `stateChange` after every successful advance.
|
|
|
|
---
|
|
|
|
## What you'll see come out
|
|
|
|
Sample Port 0 message (CSTR mid-integration, nitrifying):
|
|
|
|
```json
|
|
{
|
|
"topic": "Fluent",
|
|
"payload": {
|
|
"inlet": 0,
|
|
"F": 1000,
|
|
"C": [2.1, 30, 12.4, 0.8, 4.3, 18.6, 4.2, 1050, 65, 2150, 4.5, 215, 3680]
|
|
},
|
|
"timestamp": 1747500000000
|
|
}
|
|
```
|
|
|
|
The `C` array is the 13-species ASM3 vector in fixed order (indices 0–6 soluble, 7–12 particulate). For a PFR an additional message goes out on the same port **before** the effluent each advance:
|
|
|
|
```json
|
|
{
|
|
"topic": "GridProfile",
|
|
"payload": {
|
|
"grid": [[...13...], [...13...], "...n_x rows..."],
|
|
"n_x": 10,
|
|
"d_x": 1.0,
|
|
"length": 10,
|
|
"species": ["S_O","S_I","S_S","S_NH","S_N2","S_NO","S_HCO","X_I","X_S","X_H","X_STO","X_A","X_TS"],
|
|
"timestamp": 1747500000000
|
|
}
|
|
}
|
|
```
|
|
|
|
Port 1 (InfluxDB telemetry) carries the same data flattened as scalar fields — `flow_total` (m³/d), `temperature` (°C), and one field per species (`S_O`, `S_I`, `S_S`, `S_NH`, `S_N2`, `S_NO`, `S_HCO`, `X_I`, `X_S`, `X_H`, `X_STO`, `X_A`, `X_TS`, mg/L; `S_HCO` is mmol/L).
|
|
|
|
| Field | Meaning |
|
|
|:---|:---|
|
|
| `S_O` | Dissolved oxygen. Capped to saturation at each tick via `_capDissolvedOxygen`. |
|
|
| `S_I` | Inert soluble COD. |
|
|
| `S_S` | Readily biodegradable substrate. |
|
|
| `S_NH` | Ammonium nitrogen. Drops during nitrification. |
|
|
| `S_N2` | Dinitrogen (denitrification end product). |
|
|
| `S_NO` | Nitrate / nitrite nitrogen. Rises during nitrification. |
|
|
| `S_HCO` | Alkalinity (bicarbonate, mmol/L). |
|
|
| `X_I` | Inert particulate COD. |
|
|
| `X_S` | Slowly biodegradable substrate. |
|
|
| `X_H` | Heterotrophic biomass. |
|
|
| `X_STO` | Stored COD in biomass. |
|
|
| `X_A` | Autotrophic biomass. **Must be ≥ ~50 mg/L for nitrification to proceed.** |
|
|
| `X_TS` | Total suspended solids. Drives the downstream settler split. |
|
|
| `flow_total` | Effluent volumetric flow (m³/d) — `sum(Fs)`. |
|
|
| `temperature` | Reactor temperature (°C). |
|
|
|
|
---
|
|
|
|
## The interesting bits
|
|
|
|
### CSTR vs PFR
|
|
|
|
The engine is selected once at `configure()` from `reactor.reactor_type`. The same input topics drive both, but PFR additionally:
|
|
|
|
- Discretises the tank along the `length` axis into `resolution_L` grid cells (`n_x`).
|
|
- Emits a `GridProfile` message **before** the effluent each `updateState`.
|
|
- Honours `data.dispersion` to set the axial dispersion coefficient.
|
|
- Reconciles oxygen measurements at a **numeric** `positionVsParent` (interpreted as distance from inlet) into the nearest grid cell.
|
|
- Warns when local Peclet ≥ 2 or Courant ≥ 0.5 (stability of the explicit FD scheme).
|
|
|
|
Hot-swapping engine type at runtime is not supported — redeploy the flow.
|
|
|
|
### Aeration: internal `kla` vs external `data.otr`
|
|
|
|
`reactor.kla > 0` enables internal mass-transfer: `OTR = kla × (sat(T) − S_O)`. Set `kla = NaN` to fall through to the externally-pushed `data.otr` value (the path a `diffuser` Equipment node uses).
|
|
|
|
### `X_A` footgun
|
|
|
|
The HTML editor form's default initial autotroph biomass is `0.001` mg/L — effectively zero, so nitrification never starts. The JSON schema default is `200` mg/L. Always check the deployed node's form value before expecting `S_NH` to drop. See [Reference — Limitations](Reference-Limitations#x_a-initial-default-footgun).
|
|
|
|
---
|
|
|
|
## Need more?
|
|
|
|
| Page | What you'll find |
|
|
|:---|:---|
|
|
| [Reference — Contracts](Reference-Contracts) | Full topic contract, config schema, child registration filters |
|
|
| [Reference — Architecture](Reference-Architecture) | Code map, integration sequence, kinetics layout, 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)
|