Files
machineGroupControl/src/optimizer/bestCombination.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

89 lines
3.4 KiB
JavaScript

// CoG-based combination optimizer.
// Pure function: picks the combination whose CoG-weighted flow allocation
// yields the lowest total power, clamped to each machine's curve envelope.
//
// `ctx` must provide:
// - machines: machineId -> machine
// - groupCurves: { groupFlow, groupNCog, groupCalcPower }
// - logger (optional, for debug traces)
const ROUND_2 = 100;
function calcBestCombination(combinations, Qd, ctx) {
const { machines, groupCurves, logger } = ctx;
const { groupFlow, groupNCog, groupCalcPower } = groupCurves;
let bestCombination = null;
let bestPower = Infinity;
let bestFlow = 0;
let bestCog = 0;
combinations.forEach(combination => {
const totalCoG = combination.reduce((sum, id) => {
return sum + Math.round((groupNCog(machines[id]) || 0) * ROUND_2) / ROUND_2;
}, 0);
// CoG-weighted initial distribution; if all CoGs are 0, split evenly.
let flowDistribution = combination.map(machineId => {
const machine = machines[machineId];
let flow;
if (totalCoG === 0) {
flow = Qd / combination.length;
} else {
flow = ((groupNCog(machine) || 0) / totalCoG) * Qd;
logger?.debug?.(`Machine Normalized CoG-based distribution ${machineId} flow: ${flow}`);
}
return { machineId, flow };
});
const clamped = flowDistribution.map(entry => {
const machine = machines[entry.machineId];
const min = groupFlow(machine).currentFxyYMin;
const max = groupFlow(machine).currentFxyYMax;
const clampedFlow = Math.min(max, Math.max(min, entry.flow));
return { ...entry, flow: clampedFlow, min, max, desired: entry.flow };
});
// Spill the unmet remainder once: distribute proportionally to each
// machine's *desired* share, weighted toward those with headroom.
let remainder = Qd - clamped.reduce((sum, entry) => sum + entry.flow, 0);
if (Math.abs(remainder) > 1e-6) {
const adjustable = clamped.filter(entry =>
remainder > 0 ? entry.flow < entry.max : entry.flow > entry.min,
);
const weightSum = adjustable.reduce((s, e) => s + e.desired, 0) || adjustable.length;
adjustable.forEach(entry => {
const weight = entry.desired / weightSum || 1 / adjustable.length;
const delta = remainder * weight;
const next = remainder > 0
? Math.min(entry.max, entry.flow + delta)
: Math.max(entry.min, entry.flow + delta);
remainder -= (next - entry.flow);
entry.flow = next;
});
}
flowDistribution = clamped;
let totalFlow = 0;
let totalPower = 0;
flowDistribution.forEach(({ machineId, flow }) => {
totalFlow += flow;
totalPower += groupCalcPower(machines[machineId], flow);
});
if (totalPower < bestPower) {
logger?.debug?.(`New best combination found: ${totalPower} < ${bestPower}`);
bestPower = totalPower;
bestFlow = totalFlow;
bestCog = totalCoG;
bestCombination = flowDistribution;
}
});
return { bestCombination, bestPower, bestFlow, bestCog };
}
module.exports = { calcBestCombination };