governance + unit-self-describing demand + dashboard fixes
Two governance items from the 2026-05-14 quality review:
- test/_output-manifest.md enumerates every Port 0/1/2 key MGC emits, its
source, type, range, and which tests cover it in populated/degraded states
(per .claude/rules/output-coverage.md).
- src/control/strategies.js extracts computeEqualFlowDistribution as a pure
function so the equal-flow algorithm is testable without an MGC fixture.
test/basic/equalFlowDistribution.basic.test.js (6 tests) covers all three
demand branches and pins the legacy quirk where the default branch counts
active machines but iterates priority-ordered first-N (documented in the
test so the future cleanup is a deliberate change).
Plus rolled-up session work that landed alongside:
- set.demand is now unit-self-describing ({value, unit:'m3/h'|'l/s'|'%'|...}
or bare number = %); setScaling/scaling.current removed from MGC, commands,
editor (mgc.html), specificClass.
- _optimalControl + equalFlowControl now compute eta = (Q*dP)/P_shaft rather
than Q/P, keeping the metric in the same scale as each child's cog.
- groupEfficiency.calcRelativeDistanceFromPeak returns undefined (was 1) when
pumps are homogeneous (|max-min| < 1e-9). Dashboard treats undefined as
'-' instead of showing a misleading 100% / 0% reading.
- examples/02-Dashboard.json: auto-init inject so the dashboard populates at
deploy, NCog formatter normalizes the SUM emitted by MGC by
machineCountActive, Q-H fanout trims the flat-Q tail so the H axis isn't
stretched to 40m by curve-envelope clamp points, num/pct treat null AND
undefined as no-data (closes the +null === 0 trap).
- new test/integration/dashboard-fanout.integration.test.js (17 tests),
bep-distance-demand-sweep.integration.test.js (3 tests),
group-bep-cascade.integration.test.js -- total suite now 108/108 green.
- .gitignore: wiki/test.gif (143 MB screen recording, kept locally only).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
93
test/integration/group-bep-cascade.integration.test.js
Normal file
93
test/integration/group-bep-cascade.integration.test.js
Normal file
@@ -0,0 +1,93 @@
|
||||
'use strict';
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const MachineGroup = require('../../src/specificClass');
|
||||
const Machine = require('../../../rotatingMachine/src/specificClass');
|
||||
const baseCurve = require('../../../generalFunctions/datasets/assetData/curves/hidrostal-H05K-S03R.json');
|
||||
|
||||
/**
|
||||
* After fixing rotatingMachine + MGC to use hydraulic efficiency
|
||||
* (η = Q·ΔP / P_shaft) instead of raw flow/power, every BEP-related output
|
||||
* on MGC should be in the dimensionless 0..1 range and respond to demand
|
||||
* changes. This check ties the whole chain together:
|
||||
* - per-machine cog updates after equalize
|
||||
* - group efficiency measurement is hydraulic (matches scale of cogs)
|
||||
* - calcDistanceBEP(eff, mean(cog), min(cog)) is non-degenerate
|
||||
*/
|
||||
|
||||
const stateConfig = {
|
||||
time: { starting: 0, warmingup: 0, stopping: 0, coolingdown: 0 },
|
||||
movement: { speed: 1200, mode: 'staticspeed', maxSpeed: 1800 },
|
||||
};
|
||||
|
||||
function machineConfig(id, label) {
|
||||
return {
|
||||
general: { logging: { enabled: false, logLevel: 'error' }, name: label, id, unit: 'm3/h' },
|
||||
functionality: { softwareType: 'machine', role: 'rotationaldevicecontroller' },
|
||||
asset: { model: 'hidrostal-H05K-S03R', unit: 'm3/h' },
|
||||
mode: {
|
||||
current: 'auto',
|
||||
allowedActions: { auto: ['execsequence', 'execmovement', 'flowmovement', 'statuscheck'] },
|
||||
allowedSources: { auto: ['parent', 'GUI'] },
|
||||
},
|
||||
sequences: {
|
||||
startup: ['starting', 'warmingup', 'operational'],
|
||||
shutdown: ['stopping', 'coolingdown', 'idle'],
|
||||
emergencystop: ['emergencystop', 'off'],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function groupConfig() {
|
||||
return {
|
||||
general: { logging: { enabled: false, logLevel: 'error' }, name: 'TestGroup' },
|
||||
functionality: { softwareType: 'machinegroup', role: 'groupcontroller' },
|
||||
mode: { current: 'optimalcontrol' },
|
||||
};
|
||||
}
|
||||
|
||||
async function setupGroupWithTwoPumps() {
|
||||
const m1 = new Machine(machineConfig(1, 'pump-1'), stateConfig);
|
||||
const m2 = new Machine(machineConfig(2, 'pump-2'), stateConfig);
|
||||
m1.config.asset.machineCurve = baseCurve;
|
||||
m2.config.asset.machineCurve = baseCurve;
|
||||
await m1.handleInput('parent', 'execSequence', 'startup');
|
||||
await m2.handleInput('parent', 'execSequence', 'startup');
|
||||
|
||||
const mgc = new MachineGroup(groupConfig(), stateConfig);
|
||||
// Mutate the existing machines object — replacing the reference would
|
||||
// strand operatingPoint/totals/efficiency on the original empty bag.
|
||||
mgc.machines[1] = m1;
|
||||
mgc.machines[2] = m2;
|
||||
// Set header (system) pressure differential: 800/1200 mbar => 400 mbar = 40 kPa
|
||||
mgc.measurements.type('pressure').variant('measured').position('upstream').value(80000, Date.now(), 'Pa');
|
||||
mgc.measurements.type('pressure').variant('measured').position('downstream').value(120000, Date.now(), 'Pa');
|
||||
mgc.operatingPoint.equalize();
|
||||
return { mgc, m1, m2 };
|
||||
}
|
||||
|
||||
test('after equalize, each child cog is a dimensionless 0..1 hydraulic efficiency', async () => {
|
||||
const { m1, m2 } = await setupGroupWithTwoPumps();
|
||||
// Trigger updatePosition by setting ctrl explicitly
|
||||
m1.updatePosition();
|
||||
m2.updatePosition();
|
||||
for (const m of [m1, m2]) {
|
||||
assert.ok(Number.isFinite(m.cog), `cog must be finite, got ${m.cog}`);
|
||||
assert.ok(m.cog >= 0 && m.cog <= 1.0,
|
||||
`cog must be a 0..1 hydraulic efficiency, got ${m.cog}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('operatingPoint.headerDiffPa is set by equalize and matches measured differential', async () => {
|
||||
const { mgc, m1 } = await setupGroupWithTwoPumps();
|
||||
// Equalize reads from host measurements; falls back to children when
|
||||
// header is missing. Either path should produce headerDiffPa > 0.
|
||||
// headerDiff must equal the measured differential (40 kPa) once any
|
||||
// pressure source is populated.
|
||||
assert.equal(mgc.operatingPoint.headerDiffPa, 40000,
|
||||
`headerDiffPa should equal downstream-upstream = 40000 Pa, got ${mgc.operatingPoint.headerDiffPa}`);
|
||||
// Sanity: the host's child reference is still consumable for diagnostics.
|
||||
void m1.measurements;
|
||||
});
|
||||
Reference in New Issue
Block a user