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>
89 lines
3.4 KiB
JavaScript
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 };
|