# Reference — Contracts ![code-ref](https://img.shields.io/badge/code--ref-0e34403-blue) > [!NOTE] > Full topic contract, configuration schema, and child-registration filters for `reactor`. Source of truth: `src/commands/index.js`, `src/specificClass.js` `configure()`, and the schema at `generalFunctions/src/configs/reactor.json`. > > Pending full node review (2026-05). Content reflects `CONTRACT.md` and current source only. > > For an intuitive overview, return to the [Home](Home). --- ## Topic contract The registry lives in `src/commands/index.js`. Each descriptor maps a canonical `msg.topic` to its handler; aliases emit a one-time deprecation warning the first time they fire. | Canonical topic | Aliases | Payload | Unit | Effect | |---|---|---|---|---| | `data.clock` | `clock` | any | — | Push the simulation clock tick (timestamp / dt) to the ASM solver. | | `data.fluent` | `Fluent` | `object` | — | Push the influent stream (payload: {F: flow m3/h, C: [concentrations mg/L]}). | | `data.otr` | `OTR` | any | — | Push the current oxygen-transfer rate into the reactor. | | `data.temperature` | `Temperature` | any | — | Push the current reactor temperature. | | `data.dispersion` | `Dispersion` | any | — | Push a dispersion/mixing parameter update. | | `child.register` | `registerChild` | any | — | Register a child node (settler / measurement) with this reactor. | ### Modes / sources / actions reactor has **no mode, no action allow-lists, no source gating**. All topics are accepted as long as the payload shape is valid. (Contrast with `rotatingMachine`, which gates every input through a mode × source matrix.) --- ## Data model — `getOutput()` shape Composed each tick by `src/specificClass.js` `getOutput()`. Used to build the Port-1 InfluxDB payload; Port 0 carries the engine's `getEffluent` envelope directly. ### Port-0 process payload The engine's effluent envelope, emitted on every successful `updateState` advance: ```json { "topic": "Fluent", "payload": { "inlet": 0, "F": , "C": [<13 species, mg/L>] }, "timestamp": } ``` For a PFR an additional message is sent **before** the `Fluent` on the same port each advance: ```json { "topic": "GridProfile", "payload": { "grid": [[<13 cells of n_x>]], "n_x": , "d_x": , "length": , "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": } } ``` ### Port-1 telemetry — scalar keys | Key | Type | Unit | Source | |:---|:---|:---|:---| | `flow_total` | number | m³/d | `sum(Fs)` from effluent envelope | | `temperature` | number | °C | `engine.temperature` | | `S_O` | number | mg/L | effluent `C[0]` — capped to saturation by `_capDissolvedOxygen` | | `S_I` | number | mg/L | effluent `C[1]` | | `S_S` | number | mg/L | effluent `C[2]` | | `S_NH` | number | mg/L | effluent `C[3]` | | `S_N2` | number | mg/L | effluent `C[4]` | | `S_NO` | number | mg/L | effluent `C[5]` | | `S_HCO` | number | mmol/L | effluent `C[6]` — alkalinity | | `X_I` | number | mg/L | effluent `C[7]` | | `X_S` | number | mg/L | effluent `C[8]` | | `X_H` | number | mg/L | effluent `C[9]` | | `X_STO` | number | mg/L | effluent `C[10]` | | `X_A` | number | mg/L | effluent `C[11]` | | `X_TS` | number | mg/L | effluent `C[12]` | Non-finite species values are **omitted** from the output (the `Number.isFinite` guard in `getOutput`); they are not emitted as `null`. Pick one convention per consumer (absent vs null) and document it — see `.claude/rules/output-coverage.md`. ### Species ordering The 13-species vector is **fixed**: | Index | Key | Group | |:---:|:---|:---| | 0 | `S_O` | soluble | | 1 | `S_I` | soluble | | 2 | `S_S` | soluble | | 3 | `S_NH` | soluble | | 4 | `S_N2` | soluble | | 5 | `S_NO` | soluble | | 6 | `S_HCO` | soluble | | 7 | `X_I` | particulate | | 8 | `X_S` | particulate | | 9 | `X_H` | particulate | | 10 | `X_STO` | particulate | | 11 | `X_A` | particulate | | 12 | `X_TS` | particulate | Don't reshuffle — `getOutput()` and `_flattenEngineConfig()` both depend on this exact order, as does `additional_nodes/settling-basin` and the downstream `settler` node. ### Status badge `getStatusBadge()` in `src/specificClass.js`: ``` T=<°C>.X C F=.XX m³/d S_O=.XX mg/L ``` Engine type is the constructor name with `Reactor_` stripped (so `CSTR` or `PFR`). Badge is always green-dot (no FSM-driven state). --- ## Configuration schema — editor form to config keys Source of truth: `generalFunctions/src/configs/reactor.json` plus `nodeClass.buildDomainConfig` (`src/nodeClass.js`). ### General (`config.general`) | Form field | Config key | Default | Notes | |:---|:---|:---|:---| | Name | `general.name` | `Reactor` | Human-readable. | | (auto-assigned) | `general.id` | `null` | Node-RED node id. | | Default unit | `general.unit` | `null` | Unused by the reactor's own logic (the engines pick up units from the schema's `rules.unit` strings); kept for parent compatibility. | | Log enabled | `general.logging.enabled` | `true` | Master switch. | | Log level | `general.logging.logLevel` | `info` | `debug` / `info` / `warn` / `error`. | ### Functionality (`config.functionality`) | Form field | Config key | Default | Notes | |:---|:---|:---|:---| | Position vs parent | `functionality.positionVsParent` | `atEquipment` | Used in the child-register payload that goes UP to whatever parent registers this reactor. Enum: `upstream` / `atEquipment` / `downstream`. | | (hidden) | `functionality.softwareType` | `reactor` | Constant. | | (hidden) | `functionality.role` | `Biological reactor for wastewater treatment` | Constant. | ### Reactor (`config.reactor`) | Form field | Config key | Schema default | Range / unit | Notes | |:---|:---|:---|:---|:---| | Reactor type | `reactor.reactor_type` | `CSTR` | enum: `CSTR` / `PFR` | Selected once at `configure()`. `_buildEngine` calls `.toUpperCase()` so `pfr` and `PFR` both resolve. | | Volume | `reactor.volume` | `1000` | m³, `> 0` | Used by mass balance and (PFR) surface-area derivation. | | Length | `reactor.length` | `10` | m, `> 0` | **PFR only.** Sets axial extent and grid pitch (`d_x = length / n_x`). | | Resolution | `reactor.resolution_L` | `10` | integer `≥ 1` | **PFR only.** Grid cell count `n_x`. | | Alpha | `reactor.alpha` | `0.5` | `0..1` | **PFR only.** Inlet boundary blend: `0` = pure Danckwerts, `1` = fully mixed inlet. | | Inlets | `reactor.n_inlets` | `1` | integer `≥ 1` | `Fs[]` / `Cs_in[]` array size. | | kLa | `reactor.kla` | `0` | 1/h, `≥ 0`; set `NaN` to disable | Enables internal aeration `OTR = kla · (sat(T) − S_O)`. When `NaN`, `data.otr` is honoured instead. | | Time step | `reactor.timeStep` | `0.001` | `≥ 0.0001` | Schema declares unit `h`; `baseEngine.js` converts by `÷ 86400` (treating it as seconds). See [Limitations — timeStep unit mismatch](Reference-Limitations#timestep-unit-mismatch). | | Speed-up factor | `reactor.speedUpFactor` | `1` | `≥ 1` | Multiplies wall-clock Δt when computing `n_iter`. `2` means twice as many internal steps per second. | ### Initial state (`config.initialState`) 13 starting concentrations, all written into the engine's `state` (CSTR: single row; PFR: replicated across all `n_x` grid cells at construction). | Form field | Config key | Schema default | HTML default | Unit | Notes | |:---|:---|:---|:---|:---|:---| | Initial S_O | `initialState.S_O` | `0` | check editor | mg/L | Capped to saturation on the first tick. | | Initial S_I | `initialState.S_I` | `30` | check editor | mg/L | Inert soluble COD. | | Initial S_S | `initialState.S_S` | `70` | check editor | mg/L | Readily biodegradable substrate. | | Initial S_NH | `initialState.S_NH` | `25` | check editor | mg/L | Ammonium — declines with nitrification. | | Initial S_N2 | `initialState.S_N2` | `0` | check editor | mg/L | Dinitrogen. | | Initial S_NO | `initialState.S_NO` | `0` | check editor | mg/L | Nitrate / nitrite. | | Initial S_HCO | `initialState.S_HCO` | `5` | check editor | mmol/L | Alkalinity. | | Initial X_I | `initialState.X_I` | `1000` | check editor | mg/L | Inert particulate COD. | | Initial X_S | `initialState.X_S` | `100` | check editor | mg/L | Slowly biodegradable substrate. | | Initial X_H | `initialState.X_H` | `2000` | check editor | mg/L | Heterotrophic biomass. | | Initial X_STO | `initialState.X_STO` | `0` | check editor | mg/L | Stored COD in biomass. | | Initial X_A | `initialState.X_A` | `200` | **`0.001`** | mg/L | **Footgun.** HTML default in `reactor.html` (per `CONTRACT.md`) is effectively zero, disabling nitrification. Always verify the deployed form value. | | Initial X_TS | `initialState.X_TS` | `3500` | check editor | mg/L | Total suspended solids — drives downstream settler split. | > [!WARNING] > The HTML form supplies its own defaults; for fields where they differ from the schema (notably `X_A`), the HTML wins at deploy time. Either match the schema in the HTML or audit every deployed flow. ### Unit policy reactor does **not** declare a UnitPolicy in `specificClass`. Units are carried in the schema's `rules.unit` strings (m³, m, 1/h, mg/L, mmol/L) and consumed by the engines without normalisation through MeasurementContainer's canonical-unit rule. Notable internal conversions: | Quantity | What the engine uses internally | Where converted | |:---|:---|:---| | `timeStep` | days | `baseEngine.js` line ~40: `timeStep = config.timeStep / 86400` | | `Fs` | m³/d (assumed by mass-balance formulas) | not converted — the caller is expected to push m³/d on `data.fluent` | | `temperature` | °C | stored as supplied (Celsius); `_calcOxygenSaturation(T)` expects °C | This is a known divergence from the platform-wide canonical-unit rule (`Pa` / `m³/s` / `W` / `K`). Tracked. --- ## Child registration Source: `src/specificClass.js` `configure()` (ChildRouter wiring) + `BaseReactorEngine._connectMeasurement` / `_connectReactor`. | Software type | Filter | Wired to | Side-effect | |:---|:---|:---|:---| | `measurement` | `asset.type = 'temperature'`, `positionVsParent = atEquipment` | `engine._connectMeasurement` → `_updateMeasurement` | Writes `engine.temperature`. CSTR only honours this. | | `measurement` | `asset.type = 'quantity (oxygen)'`, `positionVsParent = ` | `engine._connectMeasurement` → `Reactor_PFR._updateMeasurement` | **PFR only.** Maps measurement to nearest grid cell by `clamp(round(pos / length × n_x), 0, n_x − 1)`. Writes into `state[cell][S_O_INDEX]`. | | `reactor` | `positionVsParent = 'upstream'` | `engine._connectReactor` | Subscribes to upstream reactor's `stateChange`. Each event triggers downstream `updateState`, which pulls upstream `getEffluent` into `Fs[0]` / `Cs_in[0]` before integrating. | ### Not a child: `diffuser` `diffuser` (Equipment Module) is **not** registered as a reactor child. It feeds aeration via the `data.otr` topic on Port 0. No child-registration handshake is involved. If you want the diffuser's OTR to drive the reactor, wire the diffuser's process output to the reactor's input directly. ### Unrecognised softwareType `BaseReactorEngine.registerChild` logs `Unrecognized softwareType: ` and drops the registration. There is no `valve`, `rotatingMachine`, etc. acceptance path. --- ## Related pages | Page | Why | |:---|:---| | [Home](Home) | Intuitive overview | | [Reference — Architecture](Reference-Architecture) | Code map, integration sequence, kinetics | | [Reference — Examples](Reference-Examples) | Shipped flows + debug recipes | | [Reference — Limitations](Reference-Limitations) | Known issues and open questions | | [EVOLV — Topic Conventions](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topic-Conventions) | Platform-wide topic rules | | [EVOLV — Telemetry](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Telemetry) | Port 0 / 1 / 2 InfluxDB layout |