91 lines
3.5 KiB
JavaScript
91 lines
3.5 KiB
JavaScript
|
|
const test = require('node:test');
|
||
|
|
const assert = require('node:assert/strict');
|
||
|
|
|
||
|
|
// Local stub for groupCurves — replace once ../groupOps/groupCurves lands.
|
||
|
|
const groupCurves = {
|
||
|
|
groupFlow: (m) => m.predictFlow,
|
||
|
|
groupPower: (m) => m.predictPower,
|
||
|
|
groupNCog: (m) => m.NCog ?? 0,
|
||
|
|
groupCalcPower: (m, f) => m.inputFlowCalcPower(f),
|
||
|
|
};
|
||
|
|
|
||
|
|
const { validPumpCombinations, checkSpecialCases } =
|
||
|
|
require('../../src/combinatorics/pumpCombinations');
|
||
|
|
|
||
|
|
function makeMachine({ id, state = 'off', mode = 'auto',
|
||
|
|
fMin = 0, fMax = 100, pMax = 100,
|
||
|
|
NCog = 0.5, validAction = true } = {}) {
|
||
|
|
return {
|
||
|
|
config: { general: { id } },
|
||
|
|
state: { getCurrentState: () => state },
|
||
|
|
currentMode: mode,
|
||
|
|
NCog,
|
||
|
|
predictFlow: { currentFxyYMin: fMin, currentFxyYMax: fMax },
|
||
|
|
predictPower: { currentFxyYMin: 0, currentFxyYMax: pMax },
|
||
|
|
inputFlowCalcPower: (flow) => flow * 0.5,
|
||
|
|
isValidActionForMode: () => validAction,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
const POSITIONS = { DOWNSTREAM: 'downstream' };
|
||
|
|
const baseCtx = (extra = {}) => ({
|
||
|
|
groupCurves,
|
||
|
|
logger: { warn: () => {}, debug: () => {}, error: () => {} },
|
||
|
|
readChildMeasurement: () => undefined,
|
||
|
|
POSITIONS,
|
||
|
|
unitPolicy: { canonical: { flow: 'm3/s' } },
|
||
|
|
...extra,
|
||
|
|
});
|
||
|
|
|
||
|
|
test('validPumpCombinations: 3 idle machines + Qd in range returns subsets that can deliver', () => {
|
||
|
|
const machines = {
|
||
|
|
a: makeMachine({ id: 'a', state: 'idle', fMin: 10, fMax: 50 }),
|
||
|
|
b: makeMachine({ id: 'b', state: 'idle', fMin: 10, fMax: 50 }),
|
||
|
|
c: makeMachine({ id: 'c', state: 'idle', fMin: 10, fMax: 50 }),
|
||
|
|
};
|
||
|
|
const combos = validPumpCombinations(machines, 40, baseCtx());
|
||
|
|
assert.ok(combos.length > 0, 'expected at least one combination');
|
||
|
|
// every combination must be able to deliver Qd
|
||
|
|
for (const subset of combos) {
|
||
|
|
const maxF = subset.reduce((s, id) => s + machines[id].predictFlow.currentFxyYMax, 0);
|
||
|
|
const minF = subset.reduce((s, id) => s + machines[id].predictFlow.currentFxyYMin, 0);
|
||
|
|
assert.ok(maxF >= 40);
|
||
|
|
assert.ok(minF <= 40);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test('validPumpCombinations: excludes machines in off/coolingdown/stopping/emergencystop', () => {
|
||
|
|
const machines = {
|
||
|
|
a: makeMachine({ id: 'a', state: 'off', fMin: 10, fMax: 50 }),
|
||
|
|
b: makeMachine({ id: 'b', state: 'coolingdown', fMin: 10, fMax: 50 }),
|
||
|
|
c: makeMachine({ id: 'c', state: 'stopping', fMin: 10, fMax: 50 }),
|
||
|
|
d: makeMachine({ id: 'd', state: 'emergencystop', fMin: 10, fMax: 50 }),
|
||
|
|
e: makeMachine({ id: 'e', state: 'idle', fMin: 10, fMax: 50 }),
|
||
|
|
};
|
||
|
|
const combos = validPumpCombinations(machines, 30, baseCtx());
|
||
|
|
// Only "e" can be in a combination
|
||
|
|
for (const subset of combos) {
|
||
|
|
for (const id of subset) assert.equal(id, 'e');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test('checkSpecialCases: reduces Qd by flow of manually controlled operational machines', () => {
|
||
|
|
const machines = {
|
||
|
|
a: makeMachine({ id: 'a', state: 'operational', mode: 'virtualControl' }),
|
||
|
|
b: makeMachine({ id: 'b', state: 'idle' }),
|
||
|
|
};
|
||
|
|
const ctx = baseCtx({
|
||
|
|
readChildMeasurement: (m, type, variant) => {
|
||
|
|
if (m.config.general.id === 'a' && variant === 'measured') return 12;
|
||
|
|
return undefined;
|
||
|
|
},
|
||
|
|
});
|
||
|
|
const adjusted = checkSpecialCases(machines, 50, ctx);
|
||
|
|
assert.equal(adjusted, 38);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('validPumpCombinations: no machines returns empty array', () => {
|
||
|
|
const combos = validPumpCombinations({}, 10, baseCtx());
|
||
|
|
assert.deepEqual(combos, []);
|
||
|
|
});
|