// 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 };