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:
96
src/combinatorics/pumpCombinations.js
Normal file
96
src/combinatorics/pumpCombinations.js
Normal file
@@ -0,0 +1,96 @@
|
||||
// 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 };
|
||||
Reference in New Issue
Block a user