Files
reactor/wiki/Reference-Contracts.md
znetsixe cb49bb8b4d docs(wiki): full 5-page wiki matching the rotatingMachine reference format
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>
2026-05-19 09:42:11 +02:00

13 KiB
Raw Blame History

Reference — Contracts

code-ref

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.


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 &times; &Delta;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 &times; (sat(T) &minus; 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 &ge; 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 &ge; 1 Fs[] / Cs_in[] array size.
kLa reactor.kla 0 1/h, &ge; 0; set NaN to disable Enables internal aeration OTR = kla &middot; (sat(T) &minus; S_O). When NaN, data.otr is honoured instead.
Time step reactor.timeStep 0.001 &ge; 0.0001 Schema declares unit h; baseEngine.js converts by &divide; 86400 (treating it as seconds). See Limitations — timeStep unit mismatch.
Speed-up factor reactor.speedUpFactor 1 &ge; 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._connectMeasurementReactor_PFR._updateMeasurement PFR only. Maps measurement to nearest grid cell by clamp(round(pos / length &times; n_x), 0, n_x &minus; 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.


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