Files
reactor/wiki/Home.md
znetsixe cb49bb8b4d docs(wiki): full 5-page wiki matching the rotatingMachine reference format
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>
2026-05-19 09:42:11 +02:00

9.6 KiB
Raw Blame History

reactor

code-ref s88 status

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

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):

  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 &times; &Delta;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_OX_TS).

Important

GIF needed. Demo recording of steps 15 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):

{
  "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 06 soluble, 712 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 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 &times; (sat(T) &minus; 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

EVOLV master wiki · Topology Patterns · Topic Conventions