hydraulic efficiency η = (Q·ΔP)/P + asset registry rename

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>
This commit is contained in:
znetsixe
2026-05-14 22:52:24 +02:00
parent 28344c6810
commit 394a972d10
9 changed files with 371 additions and 44 deletions

View File

@@ -0,0 +1,76 @@
'use strict';
const test = require('node:test');
const assert = require('node:assert/strict');
const Machine = require('../../src/specificClass');
const { buildQHCurve } = require('../../src/display/workingCurves');
const { makeMachineConfig, makeStateConfig } = require('../helpers/factories');
async function makeRunningMachine() {
const cfg = makeMachineConfig({
general: { id: 'rm-qh', name: 'qh-test', unit: 'm3/h', logging: { enabled: false, logLevel: 'error' } },
asset: {
supplier: 'hidrostal', category: 'pump', type: 'Centrifugal',
model: 'hidrostal-H05K-S03R', unit: 'm3/h',
curveUnits: { pressure: 'mbar', flow: 'm3/h', power: 'kW', control: '%' },
},
});
const m = new Machine(cfg, makeStateConfig());
await m.handleInput('parent', 'execSequence', 'startup');
m.updateMeasuredPressure(0, 'upstream', { unit: 'mbar', timestamp: Date.now(), childName: 'pt-up' });
m.updateMeasuredPressure(1500, 'downstream', { unit: 'mbar', timestamp: Date.now(), childName: 'pt-down' });
await m.handleInput('parent', 'execMovement', 60);
return m;
}
test('buildQHCurve returns one (Q, H) point per pressure slice in envelope', async () => {
const m = await makeRunningMachine();
const r = buildQHCurve(m, 60);
assert.ok(!r.error, `should not error, got ${r.error}`);
assert.ok(Array.isArray(r.points) && r.points.length > 0, 'must return points array');
for (const pt of r.points) {
assert.ok(Number.isFinite(pt.Q), `Q must be finite, got ${pt.Q}`);
assert.ok(Number.isFinite(pt.H), `H must be finite, got ${pt.H}`);
assert.ok(pt.Q > 0, `Q must be > 0, got ${pt.Q}`);
assert.ok(pt.H > 0, `H must be > 0, got ${pt.H}`);
}
// Centrifugal pump: as head rises (higher pressure slice), flow drops.
// Verify monotone non-increasing Q across rising H.
const sortedByH = [...r.points].sort((a, b) => a.H - b.H);
for (let i = 1; i < sortedByH.length; i++) {
assert.ok(
sortedByH[i].Q <= sortedByH[i - 1].Q * 1.01 + 1e-6,
`flow should be non-increasing as head rises: ${JSON.stringify(sortedByH)}`,
);
}
});
test('buildQHCurve does not mutate predictor state', async () => {
const m = await makeRunningMachine();
const beforeF = m.predictFlow.fDimension;
const beforeX = m.predictFlow.currentX;
const beforeOutputY = m.predictFlow.outputY;
buildQHCurve(m, 60);
assert.equal(m.predictFlow.fDimension, beforeF, 'fDimension must be restored');
assert.equal(m.predictFlow.currentX, beforeX, 'currentX must be restored');
assert.ok(
Math.abs(m.predictFlow.outputY - beforeOutputY) < 1e-9,
`outputY must be restored, before=${beforeOutputY} after=${m.predictFlow.outputY}`,
);
});
test('buildQHCurve handles no-curve gracefully', () => {
const r = buildQHCurve({ hasCurve: false }, 50);
assert.ok(r.error, 'must report error');
assert.deepEqual(r.points, []);
});
test('buildQHCurve uses current ctrl when none provided', async () => {
const m = await makeRunningMachine();
const r = buildQHCurve(m);
assert.equal(r.ctrlPct, m.predictFlow.currentX,
`ctrlPct should default to current x, got ${r.ctrlPct} vs ${m.predictFlow.currentX}`);
});