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>
97 lines
3.8 KiB
JavaScript
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 };
|