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:
128
test/basic/totalsCalculator.basic.test.js
Normal file
128
test/basic/totalsCalculator.basic.test.js
Normal file
@@ -0,0 +1,128 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const TotalsCalculator = require('../../src/totals/totalsCalculator');
|
||||
|
||||
const unitPolicy = {
|
||||
canonical: { pressure: 'Pa', flow: 'm3/s', power: 'W', temperature: 'K' },
|
||||
output: { pressure: 'Pa', flow: 'm3/s', power: 'W', temperature: 'K' },
|
||||
};
|
||||
const silent = { debug() {}, info() {}, warn() {}, error() {} };
|
||||
|
||||
function predictView(min, max) {
|
||||
return { currentF: (min + max) / 2, currentFxyYMin: min, currentFxyYMax: max };
|
||||
}
|
||||
|
||||
function makeMachine(id, opts = {}) {
|
||||
const {
|
||||
flowMin = 0.0, flowMax = 1.0,
|
||||
powerMin = 100, powerMax = 1000,
|
||||
state = 'operational',
|
||||
hasCurve = true,
|
||||
NCog = 0.5,
|
||||
// Input-curve envelope (for calcAbsoluteTotals): { [pressureKey]: { y: [...] } }
|
||||
inputCurve = null,
|
||||
actFlow = 0,
|
||||
actPower = 0,
|
||||
} = opts;
|
||||
|
||||
const fakeInput = inputCurve || {
|
||||
'50000': { y: [flowMin, (flowMin + flowMax) / 2, flowMax] },
|
||||
};
|
||||
const fakePower = inputCurve
|
||||
? Object.fromEntries(Object.keys(inputCurve).map(k => [k, { y: [powerMin, (powerMin + powerMax) / 2, powerMax] }]))
|
||||
: { '50000': { y: [powerMin, (powerMin + powerMax) / 2, powerMax] } };
|
||||
|
||||
return {
|
||||
config: { general: { id } },
|
||||
hasCurve,
|
||||
state: { getCurrentState: () => state },
|
||||
NCog,
|
||||
predictFlow: { inputCurve: fakeInput, ...predictView(flowMin, flowMax) },
|
||||
predictPower: { inputCurve: fakePower, ...predictView(powerMin, powerMax) },
|
||||
_actFlow: actFlow,
|
||||
_actPower: actPower,
|
||||
};
|
||||
}
|
||||
|
||||
function fakeOperatingPoint(/* machines */) {
|
||||
return {
|
||||
readChild(machine, type, _variant, _position /*, _unit */) {
|
||||
if (type === 'flow') return machine._actFlow;
|
||||
if (type === 'power') return machine._actPower;
|
||||
return null;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('calcAbsoluteTotals returns zeros when no machines', () => {
|
||||
const tc = new TotalsCalculator({ machines: {}, unitPolicy, logger: silent });
|
||||
const t = tc.calcAbsoluteTotals();
|
||||
assert.deepEqual(t, { flow: { min: 0, max: 0 }, power: { min: 0, max: 0 } });
|
||||
});
|
||||
|
||||
test('calcAbsoluteTotals scans curve envelope (sum of maxes, min of mins)', () => {
|
||||
const machines = {
|
||||
a: makeMachine('a', { flowMin: 0.1, flowMax: 0.5, powerMin: 100, powerMax: 500 }),
|
||||
b: makeMachine('b', { flowMin: 0.2, flowMax: 0.8, powerMin: 200, powerMax: 700 }),
|
||||
};
|
||||
const tc = new TotalsCalculator({ machines, unitPolicy, logger: silent });
|
||||
const t = tc.calcAbsoluteTotals();
|
||||
assert.equal(t.flow.min, 0.1);
|
||||
assert.equal(t.power.min, 100);
|
||||
// max is summed across all machines
|
||||
assert.equal(t.flow.max, 0.5 + 0.8);
|
||||
assert.equal(t.power.max, 500 + 700);
|
||||
});
|
||||
|
||||
test('calcDynamicTotals sums across machines and skips machines with no valid curve', () => {
|
||||
const machines = {
|
||||
a: makeMachine('a', { flowMin: 0.1, flowMax: 0.5, powerMin: 100, powerMax: 500, actFlow: 0.3, actPower: 300 }),
|
||||
b: makeMachine('b', { flowMin: 0.2, flowMax: 0.7, powerMin: 200, powerMax: 600, actFlow: 0.4, actPower: 400 }),
|
||||
skip: makeMachine('skip', { hasCurve: false }),
|
||||
};
|
||||
const tc = new TotalsCalculator({
|
||||
machines, unitPolicy, logger: silent,
|
||||
operatingPoint: fakeOperatingPoint(machines),
|
||||
});
|
||||
|
||||
const t = tc.calcDynamicTotals();
|
||||
|
||||
assert.equal(t.flow.min, 0.1);
|
||||
assert.equal(t.flow.max, 0.5 + 0.7);
|
||||
assert.equal(t.flow.act, 0.3 + 0.4);
|
||||
assert.equal(t.power.min, 100);
|
||||
assert.equal(t.power.max, 500 + 600);
|
||||
assert.equal(t.power.act, 300 + 400);
|
||||
assert.equal(t.NCog, machines.a.NCog + machines.b.NCog);
|
||||
});
|
||||
|
||||
test('activeTotals skips machines whose state is off or maintenance', () => {
|
||||
const machines = {
|
||||
a: makeMachine('a', { flowMin: 0.1, flowMax: 0.5, powerMin: 100, powerMax: 500, state: 'operational' }),
|
||||
b: makeMachine('b', { flowMin: 0.2, flowMax: 0.7, powerMin: 200, powerMax: 600, state: 'off' }),
|
||||
c: makeMachine('c', { flowMin: 0.3, flowMax: 0.9, powerMin: 300, powerMax: 900, state: 'maintenance' }),
|
||||
d: makeMachine('d', { flowMin: 0.05, flowMax: 0.4, powerMin: 50, powerMax: 400, state: 'accelerating' }),
|
||||
};
|
||||
const tc = new TotalsCalculator({ machines, unitPolicy, logger: silent });
|
||||
|
||||
const t = tc.activeTotals();
|
||||
assert.equal(t.countActiveMachines, 2); // a + d
|
||||
assert.equal(t.flow.min, 0.1 + 0.05);
|
||||
assert.equal(t.flow.max, 0.5 + 0.4);
|
||||
assert.equal(t.power.min, 100 + 50);
|
||||
assert.equal(t.power.max, 500 + 400);
|
||||
});
|
||||
|
||||
test('activeTotals honours the injected isMachineActive override', () => {
|
||||
const machines = {
|
||||
a: makeMachine('a', { flowMin: 0.1, flowMax: 0.5, powerMin: 100, powerMax: 500, state: 'operational' }),
|
||||
b: makeMachine('b', { flowMin: 0.2, flowMax: 0.7, powerMin: 200, powerMax: 600, state: 'operational' }),
|
||||
};
|
||||
const tc = new TotalsCalculator({
|
||||
machines, unitPolicy, logger: silent,
|
||||
isMachineActive: (id) => id === 'b',
|
||||
});
|
||||
const t = tc.activeTotals();
|
||||
assert.equal(t.countActiveMachines, 1);
|
||||
assert.equal(t.flow.max, 0.7);
|
||||
});
|
||||
Reference in New Issue
Block a user