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>
9.6 KiB
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.mdand 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
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.
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):
data.fluent— inject an influent stream{inlet: 0, F: 1000, C: [...13 species...]}(m³/d, mg/L). The 13 species follow ASM3 ordering.data.temperature— set reactor temperature (default 20 °C; nitrification rates depend on this).data.otr(ifklaisNaN) or rely on the configuredklafor internal aeration.data.clock— push wall-clockmsg.timestampto advance the integrator. The engine computesn_iter = floor(speedUpFactor × Δt_wall / timeStep_days)internal Euler / FD steps and integrates them in one shot.- Watch Port 0 (
Fluentenvelope 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_NHfalling andS_NOrising (nitrification proceeding). Save aswiki/_partial-gifs/reactor/01-basic-cstr.gif, target ≤ 1 MB aftergifsicle -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
stateChangeafter every successful advance.
What you'll see come out
Sample Port 0 message (CSTR mid-integration, nitrifying):
{
"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:
{
"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
lengthaxis intoresolution_Lgrid cells (n_x). - Emits a
GridProfilemessage before the effluent eachupdateState. - Honours
data.dispersionto 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.
Need more?
| Page | What you'll find |
|---|---|
| Reference — Contracts | Full topic contract, config schema, child registration filters |
| Reference — Architecture | Code map, integration sequence, kinetics layout, output ports |
| Reference — Examples | Shipped example flows + debug recipes |
| Reference — Limitations | When not to use, known limitations, open questions |