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>
93 lines
4.7 KiB
JavaScript
93 lines
4.7 KiB
JavaScript
'use strict';
|
|
|
|
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
|
|
const Machine = require('../../src/specificClass');
|
|
const { makeMachineConfig, makeStateConfig } = require('../helpers/factories');
|
|
|
|
/**
|
|
* Reproduction harness for the dashboard report: after the pressure-router
|
|
* fix, the user sees absDistFromPeak=0, NCog=0, efficiency=0, predicted
|
|
* atEquipment flow blank, even after the machine is running and pressure
|
|
* sliders are being moved.
|
|
*
|
|
* This test mirrors the actual dashboard interaction:
|
|
* 1. start the machine (reach operational at ctrl=0)
|
|
* 2. set virtual pressure (dashboard slider equivalent)
|
|
* 3. move setpoint to non-zero ctrl
|
|
* 4. read the host fields + measurement values
|
|
*
|
|
* Every value should be non-zero after step 3. If anything is 0 here, the
|
|
* failure is reproducible at the unit level and we can patch it directly.
|
|
*/
|
|
|
|
async function makeRunningMachine() {
|
|
const cfg = makeMachineConfig({
|
|
general: { id: 'rm-bep', name: 'BEP-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');
|
|
assert.equal(m.state.getCurrentState(), 'operational');
|
|
return m;
|
|
}
|
|
|
|
test('after startup + pressure + ctrl move: NCog / efficiency / absDistFromPeak / flow-at-equipment are all non-zero', async () => {
|
|
const m = await makeRunningMachine();
|
|
|
|
// Dashboard slider equivalent — fire as virtual children (this is what
|
|
// simulateMeasurement does):
|
|
m.updateSimulatedMeasurement('pressure', 'upstream', 200, { unit: 'mbar' });
|
|
m.updateSimulatedMeasurement('pressure', 'downstream', 1100, { unit: 'mbar' });
|
|
|
|
// Move to a non-zero ctrl position.
|
|
await m.handleInput('parent', 'execMovement', 50);
|
|
|
|
// Read every metric the user reports as 0.
|
|
const flowDn = m.measurements.type('flow').variant('predicted').position('downstream').getCurrentValue('m3/h');
|
|
const flowAtEq = m.measurements.type('flow').variant('predicted').position('atEquipment').getCurrentValue('m3/h');
|
|
const powerAtEq = m.measurements.type('power').variant('predicted').position('atEquipment').getCurrentValue('kW');
|
|
const efficiency = m.measurements.type('efficiency').variant('predicted').position('atEquipment').getCurrentValue();
|
|
|
|
console.log(JSON.stringify({
|
|
state: m.state.getCurrentState(),
|
|
ctrl: m.state.getCurrentPosition(),
|
|
flowDn, flowAtEq, powerAtEq, efficiency,
|
|
NCog: m.NCog, cog: m.cog, cogIndex: m.cogIndex,
|
|
absDistFromPeak: m.absDistFromPeak, relDistFromPeak: m.relDistFromPeak,
|
|
minEfficiency: m.minEfficiency,
|
|
}, null, 2));
|
|
|
|
assert.ok(Number.isFinite(flowDn) && flowDn > 0, `flow downstream should be > 0, got ${flowDn}`);
|
|
assert.ok(Number.isFinite(flowAtEq) && flowAtEq > 0, `flow at-equipment should be > 0, got ${flowAtEq}`);
|
|
assert.ok(Number.isFinite(powerAtEq) && powerAtEq > 0, `power at-equipment should be > 0, got ${powerAtEq}`);
|
|
// Hydraulic efficiency η = (Q·ΔP)/P is a dimensionless 0..1 ratio. For
|
|
// a reasonable pump operating point it should be at least a few percent.
|
|
assert.ok(Number.isFinite(efficiency) && efficiency > 0.01,
|
|
`efficiency should be a meaningful 0..1 ratio (>1%), got ${efficiency}`);
|
|
assert.ok(efficiency <= 1.0,
|
|
`efficiency must be <= 1 (dimensionless ratio), got ${efficiency}`);
|
|
// Peak efficiency (cog) likewise should be a meaningful ratio.
|
|
assert.ok(Number.isFinite(m.cog) && m.cog > 0.01 && m.cog <= 1.0,
|
|
`cog (peak efficiency) should be a meaningful 0..1 ratio, got ${m.cog}`);
|
|
// NCog is the normalized flow at peak — depending on the curve, BEP can
|
|
// land at peakIndex=0 (yielding NCog=0). Just require finiteness here.
|
|
assert.ok(Number.isFinite(m.NCog) && m.NCog >= 0 && m.NCog <= 1,
|
|
`NCog should be finite 0..1, got ${m.NCog}`);
|
|
// Distance-from-peak is what the user actually reads. It should be finite
|
|
// and at non-BEP positions it should be > 0.
|
|
assert.ok(Number.isFinite(m.absDistFromPeak) && m.absDistFromPeak >= 0,
|
|
`absDistFromPeak should be finite >= 0, got ${m.absDistFromPeak}`);
|
|
assert.ok(Number.isFinite(m.relDistFromPeak) && m.relDistFromPeak >= 0 && m.relDistFromPeak <= 1,
|
|
`relDistFromPeak should be finite 0..1, got ${m.relDistFromPeak}`);
|
|
// At ctrl=50 the current efficiency must differ from peak (we're off BEP),
|
|
// so absDistFromPeak should be non-zero.
|
|
assert.ok(m.absDistFromPeak > 0,
|
|
`absDistFromPeak must be > 0 when off BEP, got ${m.absDistFromPeak}`);
|
|
});
|