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