Files
machineGroupControl/test/basic/bestCombination.basic.test.js
znetsixe 619b1311d2 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>
2026-05-10 20:45:23 +02:00

68 lines
2.6 KiB
JavaScript

const test = require('node:test');
const assert = require('node:assert/strict');
const { calcBestCombination } = require('../../src/optimizer/bestCombination');
function makeMachine({ id, fMin = 0, fMax = 100, NCog = 0.5, costFn } = {}) {
return {
config: { general: { id } },
NCog,
predictFlow: { currentFxyYMin: fMin, currentFxyYMax: fMax },
predictPower: { currentFxyYMin: 0, currentFxyYMax: fMax * 2 },
// Power model: caller picks the cost function so we can shape who wins.
inputFlowCalcPower: costFn ?? ((flow) => flow * 1.0),
};
}
function mkCtx(machines) {
return {
machines,
groupCurves: {
groupFlow: (m) => m.predictFlow,
groupPower: (m) => m.predictPower,
groupNCog: (m) => m.NCog ?? 0,
groupCalcPower: (m, f) => m.inputFlowCalcPower(f),
},
logger: { debug: () => {} },
};
}
test('calcBestCombination: 1 machine in combination receives Qd clamped to its range', () => {
const machines = { a: makeMachine({ id: 'a', fMin: 5, fMax: 60 }) };
const ctx = mkCtx(machines);
const res = calcBestCombination([['a']], 40, ctx);
assert.ok(res.bestCombination);
assert.equal(res.bestCombination.length, 1);
assert.equal(res.bestCombination[0].flow, 40);
// Above max — clamps to max.
const high = calcBestCombination([['a']], 200, ctx);
assert.equal(high.bestCombination[0].flow, 60);
});
test('calcBestCombination: 2 machines with equal NCog split flow evenly', () => {
const machines = {
a: makeMachine({ id: 'a', NCog: 0.5, fMin: 0, fMax: 100 }),
b: makeMachine({ id: 'b', NCog: 0.5, fMin: 0, fMax: 100 }),
};
const ctx = mkCtx(machines);
const res = calcBestCombination([['a', 'b']], 40, ctx);
const aFlow = res.bestCombination.find(e => e.machineId === 'a').flow;
const bFlow = res.bestCombination.find(e => e.machineId === 'b').flow;
assert.ok(Math.abs(aFlow - bFlow) < 1e-6, `expected even split, got a=${aFlow} b=${bFlow}`);
assert.ok(Math.abs(aFlow + bFlow - 40) < 1e-6);
});
test('calcBestCombination: returns combination with the lowest total power', () => {
// Two combinations: [a] (expensive) vs [b] (cheap). Both can deliver Qd=20.
const machines = {
a: makeMachine({ id: 'a', fMin: 0, fMax: 100, costFn: (f) => f * 10 }),
b: makeMachine({ id: 'b', fMin: 0, fMax: 100, costFn: (f) => f * 1 }),
};
const ctx = mkCtx(machines);
const res = calcBestCombination([['a'], ['b']], 20, ctx);
assert.equal(res.bestCombination[0].machineId, 'b');
assert.equal(res.bestPower, 20);
});