Files
reactor/CONTRACT.md
znetsixe 75d0413994 docs(CONTRACT): approve reactor's ASM-textbook unit divergence
reactor uses mg/L for concentrations, m³/d internally, °C, and 1/h
for KLa — diverging from EVOLV's canonical Pa/m³/s/W/K. This was a
real drift surfaced by the wiki audit; consensus is to keep it
because the ASM kinetics literature universally uses these units and
fighting that convention would obscure the math without improving
correctness. Now documented as an explicit, approved exception with
the conversion boundary spelled out.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 16:35:36 +02:00

4.1 KiB

reactor — Contract

Hand-maintained for Phase 6; the ## Inputs table is generated from src/commands/index.js (see Phase 9 generator). Keep ≤ 80 lines.

Unit convention — approved exception to the canonical-unit rule

EVOLV's canonical units (CLAUDE.md, generalFunctions/CONTRACT.md) are Pa / m³/s / W / K. reactor diverges deliberately — it follows the ASM (Activated Sludge Model) kinetics literature convention:

  • Concentrations: mg/L (= g/m³), mmol/L for alkalinity.
  • Flow internally: m³/d (engine integrator runs in days; see baseEngine.js line 40 — timeStep config field is seconds, but the internal time base is days).
  • Temperature: °C.
  • KLa: 1/h per the schema; multiplied by the seconds-input timeStep inside _calcOTR — readers verifying the math should account for the day-internal time base.

Unit conversion at the parent/child boundary happens via MeasurementContainer.UnitPolicy and the convert utility. Other nodes (rotatingMachine, pumpingStation, …) honour canonical units; reactor is the only ASM-modelled node and pays the small cost of domain-textbook units to stay aligned with every published reactor reference.

Inputs (msg.topic on Port 0)

Canonical Aliases (deprecated) Payload Effect
data.clock clock msg.timestamp (ms since epoch) Calls source.updateState(timestamp) — advances the ASM kinetics integrator by n_iter time steps that fit between currentTime and the supplied timestamp (scaled by speedUpFactor).
data.fluent Fluent { inlet: number, F: number, C: number[13] } Writes the per-inlet flow rate (F, m³/d) and concentration vector (C) into engine.Fs[inlet] / engine.Cs_in[inlet].
data.otr OTR numeric Sets the externally-supplied oxygen transfer rate (used when kla is NaN).
data.temperature Temperature numeric or { value: number } Sets engine.temperature (°C). Non-numeric payloads are warned and ignored.
data.dispersion Dispersion numeric PFR only — sets the axial dispersion coefficient D (m²/d).
child.register registerChild child node id (string) Looks up the sibling node via RED.nodes.getNode(id) and delegates to source.childRegistrationUtils.registerChild with msg.positionVsParent.

Aliases log a one-time deprecation warning the first time they fire.

Outputs (msg.topic on Port 0/1/2)

  • Port 0 (process): every tick emits the engine's effluent: { topic: 'Fluent', payload: { inlet: 0, F, C: number[13] }, timestamp }. For a PFR an additional { topic: 'GridProfile', payload: { grid, n_x, d_x, length, species, timestamp } } message goes out on the same port before the effluent.
  • Port 1 (InfluxDB telemetry): formatted via outputUtils.formatMsg(..., 'influxdb') from getOutput() — carries flow_total, temperature, and one field per ASM3 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).
  • Port 2 (registration): at startup the node sends one { topic: 'child.register', payload: <node.id>, positionVsParent, distance } to its parent.

Events emitted by source.emitter

  • stateChange — fires after every updateState() that advances the integrator. Payload is the new currentTime (ms since epoch). Downstream reactors register via child.register and subscribe to this event to pull the upstream effluent on each advance.
  • output-changed — base notification fired by updateState() so the BaseNodeAdapter pipeline pushes outputs (currently used only as a heartbeat; effluent is emitted directly from the periodic tick).

Children accepted

  • measurement — subscribes to <type>.measured.<position> on the child's measurements.emitter. Recognised reconciliations: temperature.measured.atEquipment writes engine.temperature; PFR additionally honours quantity (oxygen).measured.<distance> to reconcile dissolved-oxygen concentration into the nearest grid cell.
  • reactor — registers as the upstream reactor; the downstream updateState pulls the upstream effluent into Fs[0] / Cs_in[0] before integrating.