- tools/physics-sanity/ — JS library of cross-node balance helpers
(mass / hydraulic / hydraulic-power / oxygen-transfer / energy) with
7 unit tests + a CLI demo. Designed for `require()` from per-node
integration tests where shape-based unit tests miss physically-
impossible plant states.
- tools/docker-compose.yml + tools/mcp/{node-red-admin,influxdb,browser}
scaffolding — placeholder Dockerfiles + a ROADMAP.md for the Node-RED
admin MCP. Compose file is the target shape for the Q3-2026 migration
to the central MCP server; the per-service Dockerfile stays in this
repo as the canonical definition either way. Implementations are TODO.
- tools/README.md — top-level tooling index; documents the CI order for
running every tool on a PR.
- .gitignore: ignore tools/.env (developer-specific MCP endpoints).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2.9 KiB
@evolv/physics-sanity
Cross-node physical-balance helpers. Import from any node's test files to assert that scenario states close mass, hydraulic, hydraulic-power, oxygen-transfer, or energy balances within a stated tolerance.
Why
Per-node unit tests verify shape and behaviour. They don't catch physically impossible plant states that arise from cross-node coupling — e.g. a pumpingStation reporting outflow > inflow + accumulation, or a diffuser reporting OTR inconsistent with its KLa × ΔC × V.
These helpers don't replace per-node tests. They sit on top of an integration scenario and assert the closing balance.
Usage
const sanity = require('../../../tools/physics-sanity');
test('three-pump station closes the hydraulic balance', () => {
// … drive the scenario, take a snapshot …
const r = sanity.assertHydraulicBalance({
headerSuctionPa: ps.suctionPressurePa,
headerDischargePa: ps.dischargePressurePa,
pumpHeadPa: sumOfPumpHeads,
frictionPa: pipeFrictionEstimate,
});
assert.equal(r.ok, true, sanity.reportToString(r));
});
Helpers exported
| Function | Asserts |
|---|---|
assertMassBalance({ inflowKgPerS, outflowKgPerS, accumulationKgPerS }) |
in - out - accumulation ≈ 0 |
assertHydraulicBalance({ headerSuctionPa, headerDischargePa, pumpHeadPa, frictionPa, staticHeadPa }) |
ΔP_headers ≈ pumpHead - friction - static |
assertHydraulicPower({ flowM3PerS, headPa, shaftPowerW, efficiency }) |
shaft ≈ Q·H / η |
assertOxygenTransfer({ klaPerS, csMgPerL, cMgPerL, otrKgPerS, volumeM3 }) |
OTR ≈ KLa · (Cs - C) · V |
assertEnergyBalance({ heatInW, workInW, heatOutW, workOutW, accumulationW }) |
Q_in + W_in ≈ Q_out + W_out + ΔE |
Each returns { ok, label, ...residuals }. reportToString(r) formats
for human-readable failure messages.
CLI demo
node tools/physics-sanity/bin/physics-sanity.js
Runs four sanity-check scenarios against the helpers (smoke-test for the library itself).
Tolerance defaults
| Domain | Absolute | Relative |
|---|---|---|
| mass | 1e-6 kg/s | 0.1 % |
| hydraulic ΔP | 50 Pa (0.5 mbar) | 0.1 % |
| hydraulic power | 1 W | 0.5 % |
| OTR | 1e-4 kg/s | 0.5 % |
| energy | 1 W | 0.1 % |
Override per call with absTol / relTol.
Where to use this
Out-of-the-box destinations:
| Scenario | Where to add | Calls |
|---|---|---|
| pumpingStation hydraulic closure | nodes/pumpingStation/test/integration/ |
assertHydraulicBalance, assertHydraulicPower |
| reactor → settler mass balance | nodes/reactor/test/integration/ |
assertMassBalance |
| diffuser OTR vs reactor uptake | nodes/diffuser/test/integration/ |
assertOxygenTransfer |
| machineGroupControl efficiency sanity | nodes/machineGroupControl/test/integration/ |
assertHydraulicPower |
A future tool can scan integration tests and report which scenarios do or don't have a closing-balance assertion.