The pre-existing efficiency formula `η = flow/power` produced tiny SI-unit
values (m³/J ≈ 1e-5), was monotonic in ctrl for centrifugal-pump curves
(no interior peak), and made NCog collapse to 0 — which cascaded into MGC
reporting BEP-position 0.0% always. Replaced with hydraulic efficiency
η = (Q·ΔP)/P_shaft, the dimensionless 0..1 ratio that has a real BEP and
matches the form MGC's group-level math uses.
- prediction/efficiencyMath.js:
* calcEfficiencyCurve takes pressureDiffPa; η = 0 when dP missing
* calcCog guards (yMax > yMin) before computing NCog (was unguarded /0)
* calcEfficiency falls back to predictFlow.currentF when measured ΔP is
missing, so predicted-variant calls still produce a meaningful η before
the differential measurement settles
- specificClass.js:
* Asset-registry lookup renamed: 'machine' → 'rotatingmachine' (matches
the datasets/assetData/ rename in generalFunctions). The error path
quotes the new filename so operators can find it.
* Two-call-site fix: with default-param stateConfig={}, the single-arg
constructor path (BaseNodeAdapter calls `new Machine(this.config)`
after pre-setting Machine._pendingExtras) was silently clobbering the
pre-set extras. Only overwrite when the caller explicitly passes them.
* Push port 0 deltas (notifyOutputChanged) after prediction updates so
dashboards see state + predicted-flow changes as they happen.
- pressure/pressureRouter.js: routing + fallback hardening (the trigger
for the bep-distance-cascade reproduction).
- display/workingCurves.js: Q-H curve generator extended.
- New tests:
* test/integration/qh-curve.integration.test.js — Q-H curve shape
* test/integration/bep-distance-cascade.integration.test.js — reproduces
the dashboard report (absDistFromPeak=0, NCog=0, efficiency=0 after a
setpoint move) at the unit level so future regressions fail loudly.
Full suite: 214/214 pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
123 lines
4.4 KiB
JavaScript
123 lines
4.4 KiB
JavaScript
'use strict';
|
|
|
|
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
|
|
const PressureRouter = require('../../src/pressure/pressureRouter');
|
|
|
|
const SILENT = { warn() {}, debug() {} };
|
|
|
|
function makeFakeMeasurements() {
|
|
const writes = [];
|
|
return {
|
|
writes,
|
|
type() { return this; },
|
|
variant() { return this; },
|
|
position(p) { this._pos = p; return this; },
|
|
child(c) { this._child = c; return this; },
|
|
value(v, t, u) { writes.push({ pos: this._pos, child: this._child, value: v, t, u }); },
|
|
};
|
|
}
|
|
|
|
test('route("upstream", 1, ctx) writes to the upstream pressure slot', () => {
|
|
const meas = makeFakeMeasurements();
|
|
const router = new PressureRouter({
|
|
measurements: meas,
|
|
virtualPressureChildIds: { upstream: 'sim-u', downstream: 'sim-d' },
|
|
resolveMeasurementUnit: () => 'mbar',
|
|
logger: SILENT,
|
|
});
|
|
router.route('upstream', 1, { childId: 'real-1', unit: 'mbar', timestamp: 1234 });
|
|
assert.equal(meas.writes.length, 1);
|
|
assert.equal(meas.writes[0].pos, 'upstream');
|
|
assert.equal(meas.writes[0].child, 'real-1');
|
|
assert.equal(meas.writes[0].value, 1);
|
|
assert.equal(meas.writes[0].u, 'mbar');
|
|
});
|
|
|
|
test('virtual source: full cascade still runs (dashboard-sim must update predictions)', () => {
|
|
const meas = makeFakeMeasurements();
|
|
let pressCalled = 0, posCalled = 0, driftCalled = 0, healthCalled = 0;
|
|
const router = new PressureRouter({
|
|
measurements: meas,
|
|
virtualPressureChildIds: { upstream: 'sim-u', downstream: 'sim-d' },
|
|
resolveMeasurementUnit: () => 'mbar',
|
|
getPressure: () => { pressCalled++; return 100; },
|
|
updatePosition: () => { posCalled++; },
|
|
refreshDrift: () => { driftCalled++; },
|
|
refreshHealth: () => { healthCalled++; },
|
|
logger: SILENT,
|
|
});
|
|
router.route('upstream', 7, { childId: 'sim-u', unit: 'mbar' });
|
|
assert.equal(pressCalled, 1);
|
|
assert.equal(posCalled, 1);
|
|
assert.equal(driftCalled, 1);
|
|
assert.equal(healthCalled, 1);
|
|
});
|
|
|
|
test('real source: all refresh hooks called', () => {
|
|
const meas = makeFakeMeasurements();
|
|
let pressCalled = 0, posCalled = 0, driftCalled = 0, healthCalled = 0;
|
|
const router = new PressureRouter({
|
|
measurements: meas,
|
|
virtualPressureChildIds: { upstream: 'sim-u', downstream: 'sim-d' },
|
|
resolveMeasurementUnit: () => 'mbar',
|
|
getPressure: () => { pressCalled++; return 100; },
|
|
updatePosition: () => { posCalled++; },
|
|
refreshDrift: () => { driftCalled++; },
|
|
refreshHealth: () => { healthCalled++; },
|
|
logger: SILENT,
|
|
});
|
|
router.route('upstream', 7, { childId: 'real-pt-1', unit: 'mbar' });
|
|
assert.equal(pressCalled, 1);
|
|
assert.equal(posCalled, 1);
|
|
assert.equal(driftCalled, 1);
|
|
assert.equal(healthCalled, 1);
|
|
});
|
|
|
|
test('cascade order: getPressure runs before updatePosition (fDimension must be fresh when calcFlowPower runs)', () => {
|
|
const meas = makeFakeMeasurements();
|
|
const calls = [];
|
|
const router = new PressureRouter({
|
|
measurements: meas,
|
|
virtualPressureChildIds: { upstream: 'sim-u', downstream: 'sim-d' },
|
|
resolveMeasurementUnit: () => 'mbar',
|
|
getPressure: () => { calls.push('getPressure'); return 100; },
|
|
updatePosition: () => { calls.push('updatePosition'); },
|
|
refreshDrift: () => { calls.push('refreshDrift'); },
|
|
refreshHealth: () => { calls.push('refreshHealth'); },
|
|
logger: SILENT,
|
|
});
|
|
router.route('upstream', 7, { childId: 'real-pt-1', unit: 'mbar' });
|
|
assert.deepEqual(calls, ['getPressure', 'updatePosition', 'refreshDrift', 'refreshHealth']);
|
|
});
|
|
|
|
test('rejected unit returns false and skips the write', () => {
|
|
const meas = makeFakeMeasurements();
|
|
const warns = [];
|
|
const router = new PressureRouter({
|
|
measurements: meas,
|
|
virtualPressureChildIds: {},
|
|
resolveMeasurementUnit: () => { throw new Error('bad unit'); },
|
|
logger: { warn(m) { warns.push(m); }, debug() {} },
|
|
});
|
|
const ok = router.route('upstream', 1, { childId: 'x', unit: 'wat' });
|
|
assert.equal(ok, false);
|
|
assert.equal(meas.writes.length, 0);
|
|
assert.match(warns[0], /Rejected pressure update/);
|
|
});
|
|
|
|
test('childId null is treated as not-virtual', () => {
|
|
const meas = makeFakeMeasurements();
|
|
let posCalled = 0;
|
|
const router = new PressureRouter({
|
|
measurements: meas,
|
|
virtualPressureChildIds: { upstream: 'sim-u' },
|
|
resolveMeasurementUnit: () => 'mbar',
|
|
updatePosition: () => { posCalled++; },
|
|
logger: SILENT,
|
|
});
|
|
router.route('upstream', 2, { unit: 'mbar' });
|
|
assert.equal(posCalled, 1);
|
|
});
|