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

74 lines
4.1 KiB
Markdown

# 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.