Files
machineGroupControl/src/combinatorics/pumpCombinations.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

97 lines
3.8 KiB
JavaScript

// Pure subset/combination generators used by the optimizer.
// All callable through `ctx` so this file stays free of class state.
// `ctx` must provide:
// - groupCurves: { groupFlow, groupPower } (from ../groupOps/groupCurves)
// - logger (warn/debug)
// - readChildMeasurement(machine, type, variant, position, canonicalUnit)
// - POSITIONS, unitPolicy.canonical.flow
const EXCLUDED_STATES = new Set(['off', 'coolingdown', 'stopping', 'emergencystop']);
// Reduce demand by the flow that manually-driven operational machines
// are already delivering. Returns the adjusted Qd (may be < 0).
function checkSpecialCases(machines, Qd, ctx) {
const { logger, readChildMeasurement, POSITIONS, unitPolicy } = ctx;
const canonicalFlow = unitPolicy?.canonical?.flow;
Object.values(machines).forEach(machine => {
const state = machine.state?.getCurrentState?.();
const mode = machine.currentMode;
if (state !== 'operational') return;
if (mode !== 'virtualControl' && mode !== 'fysicalControl') return;
const measuredFlow = readChildMeasurement
? readChildMeasurement(machine, 'flow', 'measured', POSITIONS.DOWNSTREAM, canonicalFlow)
: undefined;
const predictedFlow = readChildMeasurement
? readChildMeasurement(machine, 'flow', 'predicted', POSITIONS.DOWNSTREAM, canonicalFlow)
: undefined;
let flow = 0;
if (Number.isFinite(measuredFlow) && measuredFlow !== 0) {
flow = measuredFlow;
} else if (Number.isFinite(predictedFlow) && predictedFlow !== 0) {
flow = predictedFlow;
} else {
// Unrecoverable: a machine is producing flow we can't quantify.
// Caller decides whether to abort the dispatch tick.
logger?.error?.(
"Dont perform calculation at all seeing that there is a machine working but we dont know the flow its producing"
);
return;
}
Qd = Qd - flow;
});
return Qd;
}
// Generate all non-empty machine subsets that can deliver Qd within powerCap.
// Inputs that can't possibly contribute (off / coolingdown / mode-locked) are
// excluded before the power set is built, so 2^N stays small in practice.
function validPumpCombinations(machines, Qd, ctx, powerCap = Infinity) {
const { groupCurves } = ctx;
const groupFlow = groupCurves?.groupFlow;
const groupPower = groupCurves?.groupPower;
Qd = checkSpecialCases(machines, Qd, ctx);
let subsets = [[]];
Object.keys(machines).forEach(machineId => {
const machine = machines[machineId];
const state = machine.state?.getCurrentState?.();
const validActionForMode =
typeof machine.isValidActionForMode === 'function'
? machine.isValidActionForMode('execsequence', 'auto')
: true;
if (EXCLUDED_STATES.has(state) || !validActionForMode) return;
const newSubsets = subsets.map(set => [...set, machineId]);
subsets = subsets.concat(newSubsets);
});
return subsets.filter(subset => {
if (subset.length === 0) return false;
const { maxFlow, minFlow, maxPower } = subset.reduce(
(acc, machineId) => {
const machine = machines[machineId];
const f = groupFlow(machine);
const p = groupPower(machine);
return {
maxFlow: acc.maxFlow + f.currentFxyYMax,
minFlow: acc.minFlow + f.currentFxyYMin,
maxPower: acc.maxPower + p.currentFxyYMax,
};
},
{ maxFlow: 0, minFlow: 0, maxPower: 0 },
);
return maxFlow >= Qd && minFlow <= Qd && maxPower <= powerCap;
});
}
module.exports = { validPumpCombinations, checkSpecialCases, EXCLUDED_STATES };