Replaces the prior stub/partial wiki with a Home + Reference-{Architecture,
Contracts,Examples,Limitations} + _Sidebar structure. Topic-contract and
data-model sections wrapped in AUTOGEN markers for the future wiki-gen tool.
Source-vs-spec contradictions surfaced and flagged inline (not silently
fixed). Pending-review notes mark sections that need a full node review.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
13 KiB
Reference — Contracts
Note
Full topic contract, configuration schema, and child-registration filters for
reactor. Source of truth:src/commands/index.js,src/specificClass.jsconfigure(), and the schema atgeneralFunctions/src/configs/reactor.json.Pending full node review (2026-05). Content reflects
CONTRACT.mdand current source only.For an intuitive overview, return to the 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 |
{timestamp: number} (ms since epoch). If absent, handler falls back to Date.now(). |
ms | Calls source.updateState(timestamp) — advances the ASM kinetics integrator by n_iter = floor(speedUpFactor × Δt / timeStep_days) steps that fit between currentTime and the supplied timestamp. Emits stateChange on completion. |
data.fluent |
Fluent |
{inlet: number, F: number, C: number[13]} |
F in m³/d (canonical); C in mg/L (S_HCO in mmol/L) | Writes the per-inlet flow rate into engine.Fs[inlet] and concentration vector into engine.Cs_in[inlet]. Registry-level unit normalisation is skipped; the handler stores values as supplied. |
data.otr |
OTR |
numeric (msg.payload is the OTR scalar) |
mg O₂ / L / d | Sets the externally-supplied oxygen transfer rate. Used by the kinetics engine only when kla is NaN; if kla is a finite number the internal mass-transfer formula kla × (sat(T) − S_O) is used and data.otr is ignored. |
data.temperature |
Temperature |
numeric or {value: number} |
°C | Sets engine.temperature. Non-numeric / non-finite payloads log a warn (Invalid temperature input: <raw>) and are dropped. |
data.dispersion |
Dispersion |
numeric | m²/d | PFR only. Sets axial dispersion coefficient D. The next updateState warns if local Peclet ≥ 2 or Courant ≥ 0.5. On CSTR the setter is a no-op (if (this.engine instanceof Reactor_PFR) guard in specificClass). |
child.register |
registerChild |
child node id (string) | — | Looks up the sibling via RED.nodes.getNode(id) and delegates to source.childRegistrationUtils.registerChild with msg.positionVsParent. Missing child / source logs a warn and short-circuits. |
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:
{
"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:
{
"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. |
| 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 | Intuitive overview |
| Reference — Architecture | Code map, integration sequence, kinetics |
| Reference — Examples | Shipped flows + debug recipes |
| Reference — Limitations | Known issues and open questions |
| EVOLV — Topic Conventions | Platform-wide topic rules |
| EVOLV — Telemetry | Port 0 / 1 / 2 InfluxDB layout |