Compare commits
4 Commits
cb49bb8b4d
...
dev-lzm
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46fc8dddf7 | ||
|
|
75d0413994 | ||
|
|
346a3ce2ab | ||
|
|
6b8ae5cfc3 |
22
CONTRACT.md
22
CONTRACT.md
@@ -3,6 +3,28 @@
|
||||
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 |
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType("reactor", {
|
||||
category: "EVOLV",
|
||||
color: "#50a8d9",
|
||||
color: "#6FAE5F",
|
||||
defaults: {
|
||||
name: { value: "" },
|
||||
reactor_type: { value: "CSTR", required: true },
|
||||
@@ -35,7 +35,7 @@
|
||||
X_S_init: { value: 75., required: true },
|
||||
X_H_init: { value: 30., required: true },
|
||||
X_STO_init: { value: 0., required: true },
|
||||
X_A_init: { value: 0.001, required: true },
|
||||
X_A_init: { value: 200, required: true },
|
||||
X_TS_init: { value: 125.0009, required: true },
|
||||
|
||||
timeStep: { value: 1, required: true },
|
||||
@@ -267,6 +267,7 @@
|
||||
<label for="node-input-dbaseOutputFormat"><i class="fa fa-database"></i> Database Output</label>
|
||||
<select id="node-input-dbaseOutputFormat" style="width:60%;">
|
||||
<option value="influxdb">influxdb</option>
|
||||
<option value="frost">frost</option>
|
||||
<option value="json">json</option>
|
||||
<option value="csv">csv</option>
|
||||
</select>
|
||||
|
||||
44
test/basic/timestep-units.basic.test.js
Normal file
44
test/basic/timestep-units.basic.test.js
Normal file
@@ -0,0 +1,44 @@
|
||||
'use strict';
|
||||
|
||||
// Locks in the contract that `config.timeStep` is interpreted as SECONDS by
|
||||
// the reactor kinetics engine. Before 2026-05-19 the schema labelled the field
|
||||
// `unit: "h"` while reactor.html labelled it `[s]` and baseEngine divided by
|
||||
// 86400 (seconds-per-day) to convert to internal days. A 0.001 schema default
|
||||
// — read as hours — would have produced a 3.6 s step; read as seconds it is a
|
||||
// 1 ms step. The fix aligned the schema to seconds. This test prevents the
|
||||
// drift from reappearing.
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const { Reactor_CSTR } = require('../../src/specificClass');
|
||||
const { makeReactorConfig } = require('../helpers/factories');
|
||||
|
||||
const SECONDS_PER_DAY = 24 * 60 * 60;
|
||||
|
||||
function makeEngine(timeStepSeconds) {
|
||||
return new Reactor_CSTR(makeReactorConfig({ reactor_type: 'CSTR', n_inlets: 1, timeStep: timeStepSeconds }));
|
||||
}
|
||||
|
||||
test('engine stores timeStep in days, treating input as seconds', () => {
|
||||
const eng = makeEngine(1);
|
||||
assert.ok(Math.abs(eng.timeStep - 1 / SECONDS_PER_DAY) < 1e-15,
|
||||
`engine.timeStep should be 1/86400 days for a 1-second config; got ${eng.timeStep}`);
|
||||
});
|
||||
|
||||
test('engine timeStep scales linearly with config.timeStep (seconds in)', () => {
|
||||
const a = makeEngine(1);
|
||||
const b = makeEngine(10);
|
||||
assert.ok(Math.abs(b.timeStep - 10 * a.timeStep) < 1e-15,
|
||||
'engine.timeStep must scale linearly with config.timeStep; broke the seconds→days conversion');
|
||||
});
|
||||
|
||||
test('schema default for timeStep matches the seconds convention', () => {
|
||||
const path = require('node:path');
|
||||
const gfRoot = path.dirname(require.resolve('generalFunctions'));
|
||||
const schema = require(path.join(gfRoot, 'src/configs/reactor.json'));
|
||||
assert.equal(schema.reactor.timeStep.rules.unit, 's',
|
||||
'schema timeStep.unit must be "s" — engine treats input as seconds');
|
||||
assert.equal(schema.reactor.timeStep.default, 1,
|
||||
'schema timeStep.default must be 1 (1 second), matching reactor.html');
|
||||
});
|
||||
@@ -21,7 +21,7 @@ function makeUiConfig(overrides = {}) {
|
||||
X_S_init: 75,
|
||||
X_H_init: 30,
|
||||
X_STO_init: 0,
|
||||
X_A_init: 0.001,
|
||||
X_A_init: 200,
|
||||
X_TS_init: 125,
|
||||
timeStep: 1,
|
||||
enableLog: false,
|
||||
|
||||
@@ -15,16 +15,16 @@
|
||||
|
||||
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 — populate via wiki-gen tool (TODO) -->
|
||||
<!-- BEGIN AUTOGEN: topic-contract -->
|
||||
|
||||
| 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. |
|
||||
|---|---|---|---|---|
|
||||
| `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 -->
|
||||
|
||||
|
||||
Reference in New Issue
Block a user