P4 wave 1: extract MGC concerns into focused modules
src/groupOps/ groupOperatingPoint + groupCurves (pure functions)
src/totals/ totalsCalculator (dynamic + absolute + active)
src/combinatorics/ pumpCombinations (validPumpCombinations + checkSpecialCases)
src/optimizer/ bestCombination (CoG) + bepGravitation (BEP-G + marginal-cost)
src/efficiency/ groupEfficiency (calc + distance helpers)
src/dispatch/ demandDispatcher (LatestWinsGate-based; replaces
_dispatchInFlight + _delayedCall)
src/commands/ canonical names from start (set.mode/scaling/demand,
child.register) + legacy aliases
CONTRACT.md inputs/outputs/events surface
53 basic tests pass (52 new + 1 pre-existing).
specificClass.js / nodeClass.js untouched — integration in P4 wave 2.
Findings flagged via agents (TODO append to OPEN_QUESTIONS.md):
- calcGroupEfficiency.maxEfficiency is actually the mean (misleading name)
- checkSpecialCases has a no-op `return false` inside forEach
- MGC doesn't route cmd.startup/shutdown/estop — confirm if station broadcasts need it
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
131
test/basic/groupOperatingPoint.basic.test.js
Normal file
131
test/basic/groupOperatingPoint.basic.test.js
Normal file
@@ -0,0 +1,131 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { MeasurementContainer, POSITIONS } = require('generalFunctions');
|
||||
const GroupOperatingPoint = require('../../src/groupOps/groupOperatingPoint');
|
||||
|
||||
const unitPolicy = {
|
||||
canonical: { pressure: 'Pa', flow: 'm3/s', power: 'W', temperature: 'K' },
|
||||
output: { pressure: 'Pa', flow: 'm3/s', power: 'W', temperature: 'K' },
|
||||
};
|
||||
|
||||
const silentLogger = { debug() {}, info() {}, warn() {}, error() {} };
|
||||
|
||||
function makeContainer() {
|
||||
return new MeasurementContainer({
|
||||
defaultUnits: unitPolicy.output,
|
||||
preferredUnits: unitPolicy.output,
|
||||
canonicalUnits: unitPolicy.canonical,
|
||||
storeCanonical: true,
|
||||
autoConvert: true,
|
||||
});
|
||||
}
|
||||
|
||||
function makeMachine(id, pressures = {}) {
|
||||
// pressures: { down?: Pa, up?: Pa } — written into a real container
|
||||
const m = {
|
||||
config: { general: { id } },
|
||||
measurements: makeContainer(),
|
||||
setGroupOperatingPointCalls: [],
|
||||
setGroupOperatingPoint(down, up) {
|
||||
this.setGroupOperatingPointCalls.push({ down, up });
|
||||
},
|
||||
};
|
||||
const now = Date.now();
|
||||
if (pressures.down != null) {
|
||||
m.measurements.type('pressure').variant('measured').position(POSITIONS.DOWNSTREAM).value(pressures.down, now, 'Pa');
|
||||
}
|
||||
if (pressures.up != null) {
|
||||
m.measurements.type('pressure').variant('measured').position(POSITIONS.UPSTREAM).value(pressures.up, now, 'Pa');
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
test('readChild returns value in requested unit when present', () => {
|
||||
const machines = {};
|
||||
const m = makeMachine('m1', { down: 150000 });
|
||||
machines[m.config.general.id] = m;
|
||||
const gop = new GroupOperatingPoint({ measurements: makeContainer(), machines, unitPolicy, logger: silentLogger });
|
||||
|
||||
const v = gop.readChild(m, 'pressure', 'measured', POSITIONS.DOWNSTREAM, 'Pa');
|
||||
assert.equal(v, 150000);
|
||||
});
|
||||
|
||||
test('readChild returns null when measurement missing', () => {
|
||||
const m = makeMachine('m1');
|
||||
const gop = new GroupOperatingPoint({ measurements: makeContainer(), machines: { m1: m }, unitPolicy, logger: silentLogger });
|
||||
|
||||
const v = gop.readChild(m, 'pressure', 'measured', POSITIONS.UPSTREAM, 'Pa');
|
||||
assert.equal(v, null);
|
||||
});
|
||||
|
||||
test("writeOwn writes to the group's measurements container", () => {
|
||||
const ownC = makeContainer();
|
||||
const gop = new GroupOperatingPoint({ measurements: ownC, machines: {}, unitPolicy, logger: silentLogger });
|
||||
|
||||
gop.writeOwn('flow', 'predicted', POSITIONS.AT_EQUIPMENT, 0.1, 'm3/s');
|
||||
const v = ownC.type('flow').variant('predicted').position(POSITIONS.AT_EQUIPMENT).getCurrentValue('m3/s');
|
||||
assert.equal(v, 0.1);
|
||||
});
|
||||
|
||||
test('writeOwn skips non-finite values', () => {
|
||||
const ownC = makeContainer();
|
||||
const gop = new GroupOperatingPoint({ measurements: ownC, machines: {}, unitPolicy, logger: silentLogger });
|
||||
|
||||
gop.writeOwn('flow', 'predicted', POSITIONS.AT_EQUIPMENT, NaN, 'm3/s');
|
||||
const v = ownC.type('flow').variant('predicted').position(POSITIONS.AT_EQUIPMENT).getCurrentValue('m3/s');
|
||||
assert.equal(v, null);
|
||||
});
|
||||
|
||||
test('equalize() pushes the worst-case header onto each machine when 3 pressures differ', () => {
|
||||
// No group header → max child downstream, min positive child upstream.
|
||||
// max(120k, 140k, 100k) = 140000, min(80k, 90k, 70k) = 70000.
|
||||
const machines = {
|
||||
a: makeMachine('a', { down: 120000, up: 80000 }),
|
||||
b: makeMachine('b', { down: 140000, up: 90000 }),
|
||||
c: makeMachine('c', { down: 100000, up: 70000 }),
|
||||
};
|
||||
const gop = new GroupOperatingPoint({ measurements: makeContainer(), machines, unitPolicy, logger: silentLogger });
|
||||
|
||||
gop.equalize();
|
||||
|
||||
for (const id of ['a', 'b', 'c']) {
|
||||
const last = machines[id].setGroupOperatingPointCalls.at(-1);
|
||||
assert.ok(last, `machine ${id} should have been called`);
|
||||
assert.equal(last.down, 140000);
|
||||
assert.equal(last.up, 70000);
|
||||
}
|
||||
});
|
||||
|
||||
test('equalize() is a no-op when there is no pressure data', () => {
|
||||
const machines = { a: makeMachine('a'), b: makeMachine('b') };
|
||||
const gop = new GroupOperatingPoint({ measurements: makeContainer(), machines, unitPolicy, logger: silentLogger });
|
||||
|
||||
gop.equalize();
|
||||
|
||||
assert.equal(machines.a.setGroupOperatingPointCalls.length, 0);
|
||||
assert.equal(machines.b.setGroupOperatingPointCalls.length, 0);
|
||||
});
|
||||
|
||||
test('equalize() is a no-op when machines map is empty', () => {
|
||||
const gop = new GroupOperatingPoint({ measurements: makeContainer(), machines: {}, unitPolicy, logger: silentLogger });
|
||||
assert.doesNotThrow(() => gop.equalize());
|
||||
});
|
||||
|
||||
test('equalize() falls back to direct fDimension when setGroupOperatingPoint is missing', () => {
|
||||
const m = {
|
||||
config: { general: { id: 'old' } },
|
||||
measurements: makeContainer(),
|
||||
predictFlow: { fDimension: 0 },
|
||||
predictPower: { fDimension: 0 },
|
||||
predictCtrl: { fDimension: 0 },
|
||||
};
|
||||
m.measurements.type('pressure').variant('measured').position(POSITIONS.DOWNSTREAM).value(200000, Date.now(), 'Pa');
|
||||
m.measurements.type('pressure').variant('measured').position(POSITIONS.UPSTREAM).value(100000, Date.now(), 'Pa');
|
||||
|
||||
const gop = new GroupOperatingPoint({ measurements: makeContainer(), machines: { old: m }, unitPolicy, logger: silentLogger });
|
||||
gop.equalize();
|
||||
|
||||
assert.equal(m.predictFlow.fDimension, 100000);
|
||||
assert.equal(m.predictPower.fDimension, 100000);
|
||||
assert.equal(m.predictCtrl.fDimension, 100000);
|
||||
});
|
||||
Reference in New Issue
Block a user