Replaces the agent-written placeholder inside Reference-Contracts.md with the authoritative table generated from src/commands/index.js. Both the BEGIN and END markers are normalized to the canonical form used by `@evolv/wiki-gen`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
228 lines
12 KiB
Markdown
228 lines
12 KiB
Markdown
# Reference — Contracts
|
|
|
|

|
|
|
|
> [!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.
|
|
|
|
<!-- BEGIN AUTOGEN: topic-contract -->
|
|
|
|
| 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. |
|
|
|
|
<!-- END AUTOGEN: topic-contract -->
|
|
|
|
### 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": <m³/d>, "C": [<13 species, mg/L>] },
|
|
"timestamp": <ms since epoch>
|
|
}
|
|
```
|
|
|
|
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": <int>,
|
|
"d_x": <m>,
|
|
"length": <m>,
|
|
"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": <ms since epoch>
|
|
}
|
|
}
|
|
```
|
|
|
|
### 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`:
|
|
|
|
```
|
|
<EngineType> T=<°C>.X C F=<m³/d>.XX m³/d S_O=<mg/L>.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 = <numeric distance>` | `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: <x>` 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 |
|