Auto-generated topic-contract + data-model sections via shared wikiGen script. Hand-written Mermaid diagrams for position-in-platform, code map, child registration, lifecycle, configuration, state chart (where applicable). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
14 KiB
reactor
Reflects code as of
b8247fc· regenerated2026-05-11vianpm run wiki:allIf this banner is stale, the page may be out of date. Treat as informative, not authoritative.
1. What this node is
reactor is an S88 Unit that wraps an ASM3 biological-process engine — either a CSTR (fully mixed tank) or a PFR (plug-flow with axial dispersion). It integrates 13 species (S_O, S_NH, X_H, X_TS, …) and emits the effluent vector each tick. Drives a settler downstream and accepts a recirculation pump child.
2. Position in the platform
flowchart LR
upstream[reactor<br/>upstream<br/>Unit]:::unit
reactor[reactor<br/>Unit]:::unit
settler[settler<br/>downstream<br/>Unit]:::unit
pump[rotatingMachine<br/>downstream<br/>Equipment]:::equip
tsens[measurement<br/>temperature<br/>atequipment]:::ctrl
osens[measurement<br/>oxygen<br/>position]:::ctrl
upstream -.stateChange.-> reactor
reactor -->|Fluent inlet=0| settler
pump -->|child.register downstream| reactor
tsens -->|temperature.measured.atequipment| reactor
osens -->|quantity (oxygen).measured.<position>| reactor
classDef unit fill:#50a8d9,color:#000
classDef equip fill:#86bbdd,color:#000
classDef ctrl fill:#a9daee,color:#000
S88 colours: Unit #50a8d9, Equipment #86bbdd, Control Module #a9daee. Source of truth: .claude/rules/node-red-flow-layout.md.
3. Capability matrix
| Capability | Status | Notes |
|---|---|---|
| ASM3 13-species ODE integration | ✅ | CSTR + PFR engines under kinetics/. |
| CSTR (fully mixed) | ✅ | Single concentration vector per tick. |
| PFR (axial discretization) | ✅ | resolution_L grid cells; emits GridProfile alongside Fluent. |
| Multi-inlet mixing | ✅ | n_inlets; each inlet receives its own data.fluent with inlet index. |
| Temperature reconcile from measurement | ✅ | temperature.measured.atEquipment writes engine.temperature. |
| Oxygen reconcile (PFR) | ✅ | quantity (oxygen).measured.<distance> maps to nearest grid cell. |
| KLa-driven aeration | ✅ | reactor.kla > 0 enables internal mass transfer; falls back to data.otr. |
| Speed-up factor (sim time) | ✅ | reactor.speedUpFactor accelerates wall-clock → process time. |
| Dispersion override (PFR) | ✅ | data.dispersion updates axial D. |
| Hot-swap engine type | ❌ | reactor_type is read once in configure(). |
4. Code map
flowchart TB
subgraph nodeRED["nodeClass.js — adapter (BaseNodeAdapter)"]
nc["buildDomainConfig()<br/>static DomainClass = Reactor<br/>static commands"]
end
subgraph domain["specificClass.js — orchestrator (BaseDomain)"]
sc["Reactor.configure()<br/>flatten config → build engine<br/>ChildRouter rules"]
end
subgraph kinetics["src/kinetics/"]
be["baseEngine.js<br/>shared ASM3 rate vector"]
cstr["cstr.js<br/>0-D integrator"]
pfr["pfr.js<br/>spatial discretization + dispersion"]
end
subgraph commands["src/commands/"]
cmds["index.js + handlers.js<br/>6 input topics"]
end
sc --> be
sc --> cstr
sc --> pfr
nc --> sc
nc --> cmds
| Module | Owns | Read first if you're changing… |
|---|---|---|
kinetics/baseEngine.js |
ASM3 stoichiometry + rate vector + species list. | Stoichiometric matrix, kinetic constants. |
kinetics/cstr.js |
0-D CSTR integrator + _connectMeasurement + _connectReactor. |
Mixed-tank behaviour, child wiring. |
kinetics/pfr.js |
Axial discretization, dispersion, grid profile emission. | PFR-specific behaviour, grid math. |
commands/ |
6 input descriptors + handlers (clock, fluent, OTR, temperature, dispersion, child). | Inbound topic API, alias deprecation. |
reaction_modules/ |
Optional plug-in reaction modules (legacy — not yet refactored). | Adding new bio-process modules. |
additional_nodes/ |
Sibling Node-RED nodes (recirculation-pump, settling-basin) shipped from this repo. |
Cross-node deploy in same package. |
5. Topic contract
Auto-generated from
src/commands/index.js. Do NOT hand-edit between the markers. Re-runnpm run wiki:contract.
| Canonical topic | Aliases | Payload | Effect |
|---|---|---|---|
data.clock |
clock |
any |
Pushes a value into the node's measurement stream. |
data.fluent |
Fluent |
object |
Pushes a value into the node's measurement stream. |
data.otr |
OTR |
any |
Pushes a value into the node's measurement stream. |
data.temperature |
Temperature |
any |
Pushes a value into the node's measurement stream. |
data.dispersion |
Dispersion |
any |
Pushes a value into the node's measurement stream. |
child.register |
registerChild |
any |
Parent/child plumbing — registers or unregisters a child node. |
6. Child registration
flowchart LR
subgraph kids["accepted children (softwareType)"]
m_t["measurement<br/>temperature"]:::ctrl
m_o["measurement<br/>quantity (oxygen)"]:::ctrl
r_up["reactor<br/>upstream"]:::unit
end
m_t -->|temperature.measured.atEquipment| h_meas[engine._connectMeasurement]
m_o -->|quantity (oxygen).measured.<pos>| h_meas
r_up -.stateChange.-> h_react[engine._connectReactor]
h_meas --> reconcile[reconcile T / O2 into engine state]
h_react --> pull[pull upstream effluent → Fs/Cs_in]
classDef ctrl fill:#a9daee,color:#000
classDef unit fill:#50a8d9,color:#000
| softwareType | filter | wired to | side-effect |
|---|---|---|---|
measurement |
any | engine._connectMeasurement |
temperature.measured.atEquipment → engine.temperature. PFR additionally honours quantity (oxygen).measured.<distance> → nearest grid cell DO. |
reactor |
upstream | engine._connectReactor |
Subscribes to upstream reactor's stateChange; pulls effluent into Fs[0] / Cs_in[0] before next integration step. |
7. Lifecycle — what one data.clock advance does
sequenceDiagram
participant clock as clock injector
participant reactor as reactor
participant engine as kinetics engine
participant downstream as settler / next reactor
participant out as Port-0 output
clock->>reactor: data.clock { timestamp }
reactor->>engine: updateState(timestamp)
Note over engine: n_iter steps,<br/>each timeStep × speedUpFactor
engine->>engine: integrate ASM3 rates
engine->>engine: emit 'stateChange'
reactor->>reactor: notifyOutputChanged
reactor->>out: Fluent { inlet=0, F, C[13] }
alt PFR
reactor->>out: GridProfile { grid, n_x, d_x, … }
end
out->>downstream: Fluent envelope
stateChange re-emits on reactor.emitter (BaseDomain emitter) so downstream reactors / settlers can listen. The effluent emission goes through the BaseNodeAdapter tick pipeline.
8. Data model — getOutput()
Port-0 process payload is the Fluent envelope (+ optional GridProfile for PFR). Port-1 telemetry is the scalar snapshot below.
| Key | Type | Unit | Sample |
|---|---|---|---|
S_HCO |
number | — | 5 |
S_I |
number | — | 30 |
S_N2 |
number | — | 0 |
S_NH |
number | — | 25 |
S_NO |
number | — | 0 |
S_O |
number | — | 0 |
S_S |
number | — | 70 |
X_A |
number | — | 200 |
X_H |
number | — | 2000 |
X_I |
number | — | 1000 |
X_S |
number | — | 100 |
X_STO |
number | — | 0 |
X_TS |
number | — | 3500 |
flow_total |
number | — | 0 |
temperature |
number | — | 20 |
Concrete sample (CSTR mid-integration, nitrifying):
{
"flow_total": 1000,
"temperature": 15.2,
"S_O": 2.1,
"S_I": 30,
"S_S": 12.4,
"S_NH": 0.8,
"S_N2": 4.3,
"S_NO": 18.6,
"S_HCO": 4.2,
"X_I": 1050,
"X_S": 65,
"X_H": 2150,
"X_STO": 4.5,
"X_A": 215,
"X_TS": 3680
}
Species ordering follows ASM3: indices 0–6 are soluble, 7–12 are particulate. flow_total is the effluent flow (m³/d); the reactor uses days as the time unit internally.
9. Configuration — editor form ↔ config keys
flowchart TB
subgraph editor["Node-RED editor form"]
f1[Reactor type CSTR / PFR]
f2[Volume m3]
f3[Length m + resolution]
f4[Alpha dispersion]
f5[KLa 1/h]
f6[Time step + speed-up]
f7[Initial state 13 species]
end
subgraph config["Domain config slice"]
c1[reactor.reactor_type]
c2[reactor.volume]
c3[reactor.length<br/>reactor.resolution_L]
c4[reactor.alpha]
c5[reactor.kla]
c6[reactor.timeStep<br/>reactor.speedUpFactor]
c7[initialState.* ASM3 keys]
end
f1 --> c1
f2 --> c2
f3 --> c3
f4 --> c4
f5 --> c5
f6 --> c6
f7 --> c7
| Form field | Config key | Default | Range | Where used |
|---|---|---|---|---|
| Reactor type | reactor.reactor_type |
CSTR |
enum: CSTR / PFR |
engine selection in _buildEngine |
| Volume (m³) | reactor.volume |
1000 |
> 0 | residence time, mass balance |
| Length (m) | reactor.length |
10 |
> 0 | PFR only — axial extent |
| Resolution L | reactor.resolution_L |
10 |
≥ 1 | PFR grid cell count |
| Alpha | reactor.alpha |
0.5 |
0–1 | dispersion vs plug-flow blend |
| Inlets | reactor.n_inlets |
1 |
≥ 1 | Fs[] / Cs_in[] array sizes |
| KLa (1/h) | reactor.kla |
0 |
≥ 0 | aeration mass transfer (NaN → use data.otr) |
| Time step (h) | reactor.timeStep |
0.001 |
≥ 0.0001 | integrator inner step |
| Speed-up factor | reactor.speedUpFactor |
1 |
≥ 1 | wall-clock → process-time multiplier |
| Initial S_NH | initialState.S_NH |
25 |
≥ 0 (mg/L) | starting ammonium |
| Initial X_H | initialState.X_H |
2000 |
≥ 0 (mg/L) | starting heterotroph biomass |
| Initial X_A | initialState.X_A |
200 |
≥ 0 (mg/L) | starting autotroph biomass — must be ≥ ~50 for nitrification |
| Initial X_TS | initialState.X_TS |
3500 |
≥ 0 (mg/L) | starting TSS — drives settler split |
10. State chart
Skipped — reactor has no FSM. It runs continuous-state ODE integration; the engine's only stateful event is stateChange, fired after every successful integration advance. See section 7 for the integration sequence.
11. Examples
| Tier | File | What it shows | Status |
|---|---|---|---|
| Basic | examples/basic.flow.json |
CSTR with one inlet, watch Fluent effluent |
✅ in repo |
| Integration | examples/integration.flow.json |
upstream reactor → reactor → settler chain | ✅ in repo |
| Edge | examples/edge.flow.json |
PFR with dispersion + multi-inlet | ✅ in repo |
| Companions | additional_nodes/* |
recirculation-pump + settling-basin Node-RED nodes shipped from this repo | ✅ in repo |
One screenshot per tier where helpful. PNG ≤ 200 KB under wiki/_partial-screenshots/reactor/.
12. Debug recipes
| Symptom | First thing to check | Where to look |
|---|---|---|
| Nitrification doesn't proceed (S_NH stays high) | initialState.X_A must be ≥ ~50 mg/L. Defaulting to 0.001 (a known footgun) means no autotrophs. |
generalFunctions/src/configs/reactor.json |
Fluent effluent flow zero |
No data.clock ticks arriving, or data.fluent never set Fs[0] > 0. |
commands/handlers.js, engine setInfluent |
PFR GridProfile not emitted |
reactor_type set to CSTR — only PFR emits grid. |
_buildEngine switch |
| Settler downstream not updating | stateChange event listener path: settler must subscribe to reactor.emitter, NOT reactor.measurements.emitter. |
settler _connectReactor |
| Temperature reconcile silently ignored | Child measurement's asset.type not temperature exactly, or positionVsParent not atEquipment. |
engine._connectMeasurement |
| Integrator slow / stalls | reactor.timeStep too small for speedUpFactor. Internal n_iter count blows up. |
engine.updateState |
wiki:datamodel script slow / hangs |
mathjs cold-start ~13 s; instantiation depends on it transitively. See known-limitations row 1. |
kinetics/baseEngine.js |
13. When you would NOT use this node
- Use reactor for ASM3 biological treatment modelling (activated sludge, nitrification, denitrification). For aerobic-only or simpler kinetics, the ASM3 species vector is overkill.
- Don't use reactor for a passive equalisation tank — the kinetics engines assume reactions are happening.
- Skip reactor when you only need a residence-time delay; a simple buffer node is lighter and doesn't require
mathjs.
14. Known limitations / current issues
| # | Issue | Tracked in |
|---|---|---|
| 1 | mathjs cold-start adds ~13 s to first require() — wiki:datamodel auto-gen may time out on the 60 s wrapper. Falls back to the hand-curated concrete sample block. |
.claude/refactor/OPEN_QUESTIONS.md — "mathjs slow load" |
| 2 | initialState.X_A default of 200 mg/L is correct; older config snapshots used 0.001 which silently disabled nitrification. Verify on every new deploy. |
generalFunctions/src/configs/reactor.json |
| 3 | getEffluent shape historically varied (array vs single envelope) — settler's _connectReactor tolerates both. Don't break the contract without updating settler. |
nodes/settler/src/specificClass.js → _connectReactor |
| 4 | additional_nodes/recirculation-pump and settling-basin are legacy companions — not yet refactored to BaseDomain. |
P6.5 follow-up |
| 5 | reaction_modules/ is a legacy plug-in directory not consumed by the current engines. Removal pending. |
P6.5 follow-up |