Compare commits

3 Commits

Author SHA1 Message Date
znetsixe
46fc8dddf7 style: palette swatch → (domain-hue redesign 2026-05-21)
Sidebar swatch now follows function family rather than S88 level, so the
palette is visually identifiable instead of monochromatically blue. Editor-group
rectangles in flow.json still follow S88 — only the registerType color changed.
Full table + rationale: superproject .claude/rules/node-red-flow-layout.md §10.0
and .claude/refactor/OPEN_QUESTIONS.md (2026-05-21 entry).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 15:05:58 +02:00
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
znetsixe
346a3ce2ab fix(reactor): X_A_init default 0.001 → 200; add timeStep-unit regression test
Two fixes for the reactor unit-confusion drift surfaced in the 2026-05-19
wiki uplift:

1. X_A_init default in reactor.html was 0.001 g COD/m³, which is
   effectively zero nitrifying biomass — the reactor cannot nitrify
   ammonia under that initial condition (per the project memory note,
   ~50 mg/L is the minimum). Aligned to the schema default of 200 in
   generalFunctions/src/configs/reactor.json. Same change in
   test/helpers/factories.js so the test factory mirrors the operational
   default; tests that need low-biomass scenarios already override.

2. New test/basic/timestep-units.basic.test.js locks in the
   `config.timeStep is interpreted as seconds` contract — verifies the
   engine's days-stored / seconds-input invariant and asserts the
   schema declares `unit: "s"`, `default: 1`. Companion to the schema
   fix in the generalFunctions submodule.

Full test suite: 49/49 pass (was 46/46 + 3 new).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 13:01:10 +02:00
4 changed files with 71 additions and 4 deletions

View File

@@ -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 |

View File

@@ -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,7 +267,8 @@
<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="json">json</option>
<option value="frost">frost</option>
<option value="json">json</option>
<option value="csv">csv</option>
</select>
</div>

View 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');
});

View File

@@ -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,