Compare commits

...

1 Commits

Author SHA1 Message Date
znetsixe
d735f9485c docs(wiki): rewrite Home.md to full 14-section visual-first template
- Banner updated to c84dd78 / 2026-05-11
- Section 2: add diffuser (data.otr path, not child-register), upstream
  reactor stateChange, settler downstream; switch to ~~~mermaid fences
- Section 4: accurate code-map — cstr/pfr extend baseEngine, not peer nodes
- Section 6: split measurement into temperature + oxygen(PFR) rows; clarify
  diffuser is NOT a registered child; switch to ~~~mermaid fences
- Section 7: expand sequence with n_iter formula, DO capping, GridProfile alt
- Section 9: correct timeStep unit note (schema h vs HTML label s), add all
  13 init fields, note X_A HTML default footgun, enum-casing note in cell
- Section 14: add row #6 (reactor_type enum lowercasing / toUpperCase guard)
  and row #7 (timeStep unit mismatch — label vs schema vs engine conversion)

AUTOGEN markers (topic-contract, data-model) untouched — regenerated clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:07:07 +02:00

View File

@@ -1,6 +1,6 @@
# reactor # reactor
> **Reflects code as of `b8247fc` · regenerated `2026-05-11` via `npm run wiki:all`** > **Reflects code as of `c84dd78` · 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. > If this banner is stale, the page may be out of date. Treat as informative, not authoritative.
## 1. What this node is ## 1. What this node is
@@ -9,27 +9,32 @@
## 2. Position in the platform ## 2. Position in the platform
```mermaid ~~~mermaid
flowchart LR flowchart LR
upstream[reactor<br/>upstream<br/>Unit]:::unit upstream[reactor<br/>upstream<br/>Unit]:::unit
reactor[reactor<br/>Unit]:::unit reactor[reactor<br/>Unit]:::unit
settler[settler<br/>downstream<br/>Unit]:::unit settler[settler<br/>downstream<br/>Unit]:::unit
pump[rotatingMachine<br/>downstream<br/>Equipment]:::equip diffuser[diffuser<br/>Equipment]:::equip
tsens[measurement<br/>temperature<br/>atequipment]:::ctrl tsens[measurement<br/>temperature<br/>atequipment]:::ctrl
osens[measurement<br/>oxygen<br/>position]:::ctrl osens[measurement<br/>oxygen<br/>at distance]:::ctrl
upstream -.stateChange.-> reactor upstream -.stateChange.-> reactor
reactor -->|Fluent inlet=0| settler reactor -->|Fluent inlet=0| settler
pump -->|child.register downstream| reactor settler -.stateChange.-> reactor
tsens -->|temperature.measured.atequipment| reactor diffuser -->|data.otr| reactor
osens -->|quantity (oxygen).measured.&lt;position&gt;| reactor tsens -->|child.register| reactor
osens -->|child.register| reactor
tsens -->|temperature.measured.atEquipment| reactor
osens -->|quantity(oxygen).measured.distance| reactor
classDef unit fill:#50a8d9,color:#000 classDef unit fill:#50a8d9,color:#000
classDef equip fill:#86bbdd,color:#000 classDef equip fill:#86bbdd,color:#000
classDef ctrl fill:#a9daee,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`. S88 colours: Unit `#50a8d9`, Equipment `#86bbdd`, Control Module `#a9daee`. Source of truth: `.claude/rules/node-red-flow-layout.md`.
reactor sits at the Unit level. A diffuser (Equipment) sends aeration rates via `data.otr` — it is NOT registered as a child. Measurement children (Control Module) register and supply temperature or dissolved-oxygen reconciliation. Settler is a downstream Unit that listens to `stateChange` to pull effluent; an upstream reactor drives this reactor the same way.
## 3. Capability matrix ## 3. Capability matrix
| Capability | Status | Notes | | Capability | Status | Notes |
@@ -38,7 +43,7 @@ S88 colours: Unit `#50a8d9`, Equipment `#86bbdd`, Control Module `#a9daee`. Sour
| CSTR (fully mixed) | ✅ | Single concentration vector per tick. | | CSTR (fully mixed) | ✅ | Single concentration vector per tick. |
| PFR (axial discretization) | ✅ | `resolution_L` grid cells; emits `GridProfile` alongside `Fluent`. | | 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. | | 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`. | | Temperature reconcile from measurement | ✅ | `temperature.measured.atEquipment` writes `engine.temperature`. Code constant: `POSITIONS.AT_EQUIPMENT`. |
| Oxygen reconcile (PFR) | ✅ | `quantity (oxygen).measured.<distance>` maps to nearest grid cell. | | 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`. | | 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. | | Speed-up factor (sim time) | ✅ | `reactor.speedUpFactor` accelerates wall-clock → process time. |
@@ -47,28 +52,28 @@ S88 colours: Unit `#50a8d9`, Equipment `#86bbdd`, Control Module `#a9daee`. Sour
## 4. Code map ## 4. Code map
```mermaid ~~~mermaid
flowchart TB flowchart TB
subgraph nodeRED["nodeClass.js — adapter (BaseNodeAdapter)"] subgraph nodeRED["nodeClass.js — adapter (BaseNodeAdapter)"]
nc["buildDomainConfig()<br/>static DomainClass = Reactor<br/>static commands"] nc["buildDomainConfig()<br/>static DomainClass = Reactor<br/>static commands"]
end end
subgraph domain["specificClass.js — orchestrator (BaseDomain)"] subgraph domain["specificClass.js — orchestrator (BaseDomain)"]
sc["Reactor.configure()<br/>flatten config → build engine<br/>ChildRouter rules"] sc["Reactor.configure()<br/>_flattenEngineConfig()<br/>_buildEngine() → CSTR or PFR<br/>ChildRouter.onRegister rules"]
end end
subgraph kinetics["src/kinetics/"] subgraph kinetics["src/kinetics/"]
be["baseEngine.js<br/>shared ASM3 rate vector"] be["baseEngine.js<br/>BaseReactorEngine<br/>influent state, OTR, T<br/>_connectMeasurement / _connectReactor<br/>updateState() → n_iter ticks"]
cstr["cstr.js<br/>0-D integrator"] cstr["cstr.js<br/>Reactor_CSTR extends BaseReactorEngine<br/>Forward-Euler 0-D integrator"]
pfr["pfr.js<br/>spatial discretization + dispersion"] pfr["pfr.js<br/>Reactor_PFR extends BaseReactorEngine<br/>FD spatial grid + Danckwerts BC"]
end end
subgraph commands["src/commands/"] subgraph commands["src/commands/"]
cmds["index.js + handlers.js<br/>6 input topics"] cmds["index.js — 6 descriptors<br/>handlers.js — 6 pure fns"]
end end
sc --> be
sc --> cstr
sc --> pfr
nc --> sc nc --> sc
nc --> cmds nc --> cmds
``` sc --> be
cstr --> be
pfr --> be
~~~
| Module | Owns | Read first if you're changing… | | Module | Owns | Read first if you're changing… |
|---|---|---| |---|---|---|
@@ -98,51 +103,55 @@ flowchart TB
## 6. Child registration ## 6. Child registration
```mermaid ~~~mermaid
flowchart LR flowchart LR
subgraph kids["accepted children (softwareType)"] subgraph kids["accepted children (softwareType)"]
m_t["measurement<br/>temperature"]:::ctrl m_t["measurement<br/>temperature<br/>positionVsParent=atEquipment"]:::ctrl
m_o["measurement<br/>quantity (oxygen)"]:::ctrl m_o["measurement<br/>quantity (oxygen)<br/>positionVsParent=distance (numeric)"]:::ctrl
r_up["reactor<br/>upstream"]:::unit r_up["reactor<br/>positionVsParent=upstream"]:::unit
end end
m_t -->|temperature.measured.atEquipment| h_meas[engine._connectMeasurement] m_t -->|temperature.measured.atEquipment| h_meas["engine._connectMeasurement<br/>(baseEngine.js)"]
m_o -->|quantity (oxygen).measured.&lt;pos&gt;| h_meas m_o -->|quantity(oxygen).measured.distance| h_meas
r_up -.stateChange.-> h_react[engine._connectReactor] r_up -.stateChange.-> h_react["engine._connectReactor<br/>(baseEngine.js)"]
h_meas --> reconcile[reconcile T / O2 into engine state] h_meas --> reconcile["reconcile T → engine.temperature<br/>reconcile O2 → state grid cell (PFR only)"]
h_react --> pull[pull upstream effluent → Fs/Cs_in] h_react --> pull["pull upstream getEffluent<br/>→ Fs[0] / Cs_in[0] before next tick"]
classDef ctrl fill:#a9daee,color:#000 classDef ctrl fill:#a9daee,color:#000
classDef unit fill:#50a8d9,color:#000 classDef unit fill:#50a8d9,color:#000
``` ~~~
| softwareType | filter | wired to | side-effect | | 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. | | `measurement` | `asset.type = temperature`, `positionVsParent = atEquipment` | `engine._connectMeasurement` | Writes `engine.temperature`. CSTR only honours temperature; PFR additionally reconciles `quantity (oxygen).measured.<distance>` (numeric position) → 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. | | `measurement` | `asset.type = quantity (oxygen)`, `positionVsParent = <numeric distance>` | `engine._connectMeasurement``pfr._updateMeasurement` | PFR only: maps measurement to nearest grid cell by `round(pos / length × n_x)`. |
| `reactor` | `positionVsParent = upstream` | `engine._connectReactor` | Subscribes to upstream reactor's `stateChange`; pulls `getEffluent` into `Fs[0]` / `Cs_in[0]` before next integration step. |
`diffuser` is NOT a registered child — it feeds aeration via `data.otr` on Port 0. No child-registration handshake is involved.
## 7. Lifecycle — what one `data.clock` advance does ## 7. Lifecycle — what one `data.clock` advance does
```mermaid ~~~mermaid
sequenceDiagram sequenceDiagram
participant clock as clock injector participant clock as clock injector
participant reactor as reactor participant reactor as reactor (specificClass)
participant engine as kinetics engine participant engine as kinetics engine (CSTR/PFR)
participant downstream as settler / next reactor participant downstream as settler / next reactor
participant out as Port-0 output participant out as Port-0 output
clock->>reactor: data.clock { timestamp } clock->>reactor: data.clock { timestamp }
reactor->>engine: updateState(timestamp) reactor->>engine: updateState(timestamp)
Note over engine: n_iter steps,<br/>each timeStep × speedUpFactor Note over engine: n_iter = floor(speedUpFactor × Δt / timeStep)<br/>each step calls tick(timeStep)
engine->>engine: integrate ASM3 rates engine->>engine: integrate ASM3 rates (CSTR: Forward Euler / PFR: FD)
engine->>engine: emit 'stateChange' engine->>engine: cap S_O to saturation, clip negatives to 0
reactor->>reactor: notifyOutputChanged engine->>engine: emit 'stateChange' (currentTime)
reactor->>out: Fluent { inlet=0, F, C[13] } reactor->>reactor: notifyOutputChanged → getOutput()
alt PFR reactor->>out: getOutput() → {flow_total, temperature, S_O … X_TS}
reactor->>out: GridProfile { grid, n_x, d_x, … } alt PFR engine
reactor->>out: GridProfile { grid[n_x][13], n_x, d_x, length, species }
end end
out->>downstream: Fluent envelope out->>downstream: Fluent { inlet=0, F, C[13] } via stateChange listener
``` ~~~
`stateChange` re-emits on `reactor.emitter` (BaseDomain emitter) so downstream reactors / settlers can listen. The effluent emission goes through the BaseNodeAdapter tick pipeline. `stateChange` re-emits on `reactor.emitter` (BaseDomain emitter) — wired in `specificClass.configure()`. Downstream settlers or chained reactors subscribed via `_connectReactor` call their own `updateState` on each `stateChange` event. The tick loop is opt-in (tick-driven via `static tickInterval`) because the reactor integrates process-time steps that have no fixed wall-clock mapping.
## 8. Data model — `getOutput()` ## 8. Data model — `getOutput()`
@@ -196,25 +205,27 @@ Species ordering follows ASM3: indices 06 are soluble, 712 are particulate
## 9. Configuration — editor form ↔ config keys ## 9. Configuration — editor form ↔ config keys
```mermaid ~~~mermaid
flowchart TB flowchart TB
subgraph editor["Node-RED editor form"] subgraph editor["Node-RED editor form (reactor.html)"]
f1[Reactor type CSTR / PFR] f1["Reactor type: CSTR / PFR"]
f2[Volume m3] f2["Volume (m³)"]
f3[Length m + resolution] f3["Length (m) + Resolution — PFR only"]
f4[Alpha dispersion] f4["Alpha α (boundary condition blend)"]
f5[KLa 1/h] f5["Number of inlets"]
f6[Time step + speed-up] f6["kLa (d⁻¹) — internal aeration"]
f7[Initial state 13 species] f7["13 × initial concentration fields"]
f8["Time step (s label) + Speed-up factor"]
end end
subgraph config["Domain config slice"] subgraph config["Domain config slice (reactor.json)"]
c1[reactor.reactor_type] c1[reactor.reactor_type]
c2[reactor.volume] c2[reactor.volume]
c3[reactor.length<br/>reactor.resolution_L] c3["reactor.length<br/>reactor.resolution_L"]
c4[reactor.alpha] c4[reactor.alpha]
c5[reactor.kla] c5[reactor.n_inlets]
c6[reactor.timeStep<br/>reactor.speedUpFactor] c6[reactor.kla]
c7[initialState.* ASM3 keys] c7["initialState.S_O … X_TS"]
c8["reactor.timeStep (unit: h per schema)<br/>reactor.speedUpFactor"]
end end
f1 --> c1 f1 --> c1
f2 --> c2 f2 --> c2
@@ -223,22 +234,24 @@ flowchart TB
f5 --> c5 f5 --> c5
f6 --> c6 f6 --> c6
f7 --> c7 f7 --> c7
``` f8 --> c8
~~~
| Form field | Config key | Default | Range | Where used | | Form field | Config key | Schema default | Range | Where used |
|---|---|---|---|---| |---|---|---|---|---|
| Reactor type | `reactor.reactor_type` | `CSTR` | enum: `CSTR` / `PFR` | engine selection in `_buildEngine` | | Reactor type | `reactor.reactor_type` | `CSTR` | enum: `CSTR` / `PFR` (schema validator lowercases; `_buildEngine` toUpperCase guards) | engine selection in `Reactor._buildEngine()` |
| Volume (m³) | `reactor.volume` | `1000` | > 0 | residence time, mass balance | | Volume (m³) | `reactor.volume` | `1000` | > 0 | residence time, mass balance |
| Length (m) | `reactor.length` | `10` | > 0 | PFR only — axial extent | | Length (m) | `reactor.length` | `10` | > 0 | PFR only — axial extent |
| Resolution L | `reactor.resolution_L` | `10` | ≥ 1 | PFR grid cell count | | Resolution | `reactor.resolution_L` | `10` | ≥ 1 | PFR grid cell count `n_x` |
| Alpha | `reactor.alpha` | `0.5` | 01 | dispersion vs plug-flow blend | | Alpha | `reactor.alpha` | `0.5` | 01 | Danckwerts (0) vs Dirichlet (1) inlet BC |
| Inlets | `reactor.n_inlets` | `1` | ≥ 1 | `Fs[]` / `Cs_in[]` array sizes | | 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`) | | kLa (d⁻¹) | `reactor.kla` | `0` | ≥ 0; set to `NaN` to use `data.otr` instead | `_calcOTR()` in `baseEngine.js` |
| Time step (h) | `reactor.timeStep` | `0.001` | ≥ 0.0001 | integrator inner step | | Time step | `reactor.timeStep` | `0.001` | ≥ 0.0001 | integrator inner step (schema says `h`; HTML label says `s` — see limitation #6) |
| Speed-up factor | `reactor.speedUpFactor` | `1` | ≥ 1 | wall-clock → process-time multiplier | | Speed-up factor | `reactor.speedUpFactor` | `1` | ≥ 1 | `n_iter = floor(speedUpFactor × Δt_wall / timeStep_days)` |
| Initial S_O | `initialState.S_O` | `0` | ≥ 0 (mg/L) | starting dissolved oxygen (caps to saturation in first tick) |
| Initial S_NH | `initialState.S_NH` | `25` | ≥ 0 (mg/L) | starting ammonium | | 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_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_A | `initialState.X_A` | `200` | ≥ 0 (mg/L) | starting autotroph biomass — must be ≥ ~50 mg/L for nitrification; HTML default is `0.001` (footgun) |
| Initial X_TS | `initialState.X_TS` | `3500` | ≥ 0 (mg/L) | starting TSS — drives settler split | | Initial X_TS | `initialState.X_TS` | `3500` | ≥ 0 (mg/L) | starting TSS — drives settler split |
## 10. State chart ## 10. State chart
@@ -278,8 +291,10 @@ One screenshot per tier where helpful. PNG ≤ 200 KB under `wiki/_partial-scree
| # | Issue | Tracked in | | # | 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" | | 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. Two remedies tracked: tree-shake mathjs to used ops only; cache the instance. | `.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` | | 2 | `initialState.X_A` HTML default is `0.001` mg/L (silently disabling nitrification) but the schema default is `200` mg/L. Always check the deployed node's form value before expecting nitrification. | `reactor.html` line 38 vs `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` | | 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 | | 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 | | 5 | `reaction_modules/` is a legacy plug-in directory not consumed by the current engines. Removal pending. | P6.5 follow-up |
| 6 | `reactor_type` enum casing: the JSON schema validator lowercases the user-supplied value (`'PFR'``'pfr'`). `Reactor._buildEngine` calls `.toUpperCase()` to work around this until Phase 7 decides the platform-wide canonical casing. If the guard is removed prematurely, PFR config silently falls back to CSTR. | `.claude/refactor/OPEN_QUESTIONS.md` — "reactor schema enum lowercases reactor_type" |
| 7 | `timeStep` unit mismatch: the HTML form label says "Time step [s]" but `reactor.json` declares `unit: "h"`. `baseEngine.js` converts `config.timeStep` by `÷ 86 400` (seconds → days), suggesting the true input unit is seconds. Audited in OPEN_QUESTIONS.md Phase 5/6 cleanup list. | `baseEngine.js` line 40; `reactor.json` `timeStep.rules.unit`; `reactor.html` time-step label |