Files
reactor/wiki/Home.md
znetsixe d931bead0a P9.3: wiki/Home.md following 14-section visual-first template + wiki:* scripts
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>
2026-05-11 15:17:45 +02:00

286 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# reactor
> **Reflects code as of `b8247fc` · regenerated `2026-05-11` via `npm run wiki:all`**
> If 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
```mermaid
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.&lt;position&gt;| 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
```mermaid
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-run `npm run wiki:contract`.
<!-- BEGIN AUTOGEN: topic-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. |
<!-- END AUTOGEN: topic-contract -->
## 6. Child registration
```mermaid
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.&lt;pos&gt;| 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
```mermaid
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.
<!-- BEGIN AUTOGEN: data-model -->
| 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` |
<!-- END AUTOGEN: data-model -->
**Concrete sample** (CSTR mid-integration, nitrifying):
```json
{
"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 06 are soluble, 712 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
```mermaid
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` | 01 | 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 |