'use strict'; // Priority-based control strategies for machineGroupControl. // // equalFlowControl: distribute demand equally across priority-ordered active // machines, falling back to start/stop the next priority when the current // active set can't deliver. // // prioPercentageControl: percentage-style ctrl distribution (only valid with // normalized scaling). // // Both extracted verbatim from specificClass during the P4 refactor; the // orchestrator wires them in via the strategies map below. They depend on // the same group-curve helpers the optimizer uses, so allocation and power // evaluation stay on the equalised group operating point. const { POSITIONS } = require('generalFunctions'); const { groupFlow, groupCalcPower } = require('../groupOps/groupCurves'); function sortMachinesByPriority(machines, priorityList) { if (priorityList && Array.isArray(priorityList)) { return priorityList .filter(id => machines[id]) .map(id => ({ id, machine: machines[id] })); } return Object.entries(machines) .map(([id, machine]) => ({ id, machine })) .sort((a, b) => a.id - b.id); } function filterOutUnavailableMachines(list) { return list.filter(({ machine }) => { const state = machine.state.getCurrentState(); const validActionForMode = machine.isValidActionForMode('execsequence', 'auto'); return !(state === 'off' || state === 'coolingdown' || state === 'stopping' || state === 'emergencystop' || !validActionForMode); }); } function capFlowDemand(Qd, dynamicTotals, logger) { if (Qd < dynamicTotals.flow.min && Qd > 0) { logger?.warn?.(`Flow demand ${Qd} below min ${dynamicTotals.flow.min}; capping.`); return dynamicTotals.flow.min; } if (Qd > dynamicTotals.flow.max) { logger?.warn?.(`Flow demand ${Qd} above max ${dynamicTotals.flow.max}; capping.`); return dynamicTotals.flow.max; } return Qd; } async function equalFlowControl(ctx, Qd, _powerCap = Infinity, priorityList = null) { const { mgc } = ctx; try { mgc.equalizePressure(); const dynamicTotals = mgc.calcDynamicTotals(); Qd = capFlowDemand(Qd, dynamicTotals, mgc.logger); let machinesInPriorityOrder = sortMachinesByPriority(mgc.machines, priorityList); machinesInPriorityOrder = filterOutUnavailableMachines(machinesInPriorityOrder); const flowDistribution = []; let totalFlow = 0; let totalPower = 0; const totalCog = 0; const activeTotals = mgc.totals.activeTotals(); switch (true) { case (Qd < activeTotals.flow.min && activeTotals.flow.min !== 0): { let availableFlow = activeTotals.flow.min; for (let i = machinesInPriorityOrder.length - 1; i >= 0 && availableFlow > Qd; i--) { const m = machinesInPriorityOrder[i]; if (mgc.isMachineActive(m.id)) { flowDistribution.push({ machineId: m.id, flow: 0 }); availableFlow -= groupFlow(m.machine).currentFxyYMin; } } const remaining = machinesInPriorityOrder.filter(({ id }) => mgc.isMachineActive(id) && !flowDistribution.some(it => it.machineId === id)); const distributedFlow = Qd / remaining.length; for (const m of remaining) { flowDistribution.push({ machineId: m.id, flow: distributedFlow }); totalFlow += distributedFlow; totalPower += groupCalcPower(m.machine, distributedFlow); } break; } case (Qd > activeTotals.flow.max): { let i = 1; while (totalFlow < Qd && i <= machinesInPriorityOrder.length) { Qd = Qd / i; if (groupFlow(machinesInPriorityOrder[i - 1].machine).currentFxyYMax >= Qd) { for (let i2 = 0; i2 < i; i2++) { if (!mgc.isMachineActive(machinesInPriorityOrder[i2].id)) { flowDistribution.push({ machineId: machinesInPriorityOrder[i2].id, flow: Qd }); totalFlow += Qd; totalPower += groupCalcPower(machinesInPriorityOrder[i2].machine, Qd); } } } i++; } break; } default: { const countActive = machinesInPriorityOrder.filter(({ id }) => mgc.isMachineActive(id)).length; Qd /= countActive; for (let i = 0; i < countActive; i++) { flowDistribution.push({ machineId: machinesInPriorityOrder[i].id, flow: Qd }); totalFlow += Qd; totalPower += groupCalcPower(machinesInPriorityOrder[i].machine, Qd); } break; } } const fUnit = mgc.unitPolicy.canonical.power; const flUnit = mgc.unitPolicy.canonical.flow; mgc.operatingPoint.writeOwn('power', 'predicted', POSITIONS.AT_EQUIPMENT, totalPower, fUnit); mgc.operatingPoint.writeOwn('flow', 'predicted', POSITIONS.AT_EQUIPMENT, totalFlow, flUnit); mgc.measurements.type('efficiency').variant('predicted').position(POSITIONS.AT_EQUIPMENT).value(totalFlow / totalPower); mgc.measurements.type('Ncog').variant('predicted').position(POSITIONS.AT_EQUIPMENT).value(totalCog); await Promise.all(flowDistribution.map(async ({ machineId, flow }) => { const machine = mgc.machines[machineId]; const currentState = machine.state.getCurrentState(); if (flow > 0) { await machine.handleInput('parent', 'flowmovement', mgc._canonicalToOutputFlow(flow)); if (currentState === 'idle') { await machine.handleInput('parent', 'execsequence', 'startup'); } } else if (currentState === 'operational' || currentState === 'accelerating' || currentState === 'decelerating') { await machine.handleInput('parent', 'execsequence', 'shutdown'); } })); } catch (err) { mgc.logger?.error?.(err); } } async function prioPercentageControl(ctx, input, priorityList = null) { const { mgc } = ctx; try { if (input < 0) { await mgc.turnOffAllMachines(); return; } if (input > 100) input = 100; const numOfMachines = Object.keys(mgc.machines).length; const procentTotal = numOfMachines * input; const machinesNeeded = Math.ceil(procentTotal / 100); const activeTotals = mgc.totals.activeTotals(); const machinesActive = activeTotals.countActiveMachines; const machinesInPriorityOrder = sortMachinesByPriority(mgc.machines, priorityList); const ctrlDistribution = []; if (machinesNeeded > machinesActive) { machinesInPriorityOrder.forEach(({ id }, index) => { if (index < machinesNeeded) ctrlDistribution.push({ machineId: id, ctrl: 0 }); }); } if (machinesNeeded < machinesActive) { machinesInPriorityOrder.forEach(({ id }, index) => { if (mgc.isMachineActive(id)) { ctrlDistribution.push({ machineId: id, ctrl: index < machinesNeeded ? 100 : -1 }); } }); } if (machinesNeeded === machinesActive) { const ctrlPerMachine = procentTotal / machinesActive; machinesInPriorityOrder.forEach(({ id }) => { if (mgc.isMachineActive(id)) { ctrlDistribution.push({ machineId: id, ctrl: Math.max(0, Math.min(ctrlPerMachine, 100)) }); } }); } await Promise.all(ctrlDistribution.map(async ({ machineId, ctrl }) => { const machine = mgc.machines[machineId]; const currentState = machine.state.getCurrentState(); if (ctrl < 0 && (currentState === 'operational' || currentState === 'accelerating' || currentState === 'decelerating')) { await machine.handleInput('parent', 'execsequence', 'shutdown'); } else if (currentState === 'idle' && ctrl >= 0) { await machine.handleInput('parent', 'execsequence', 'startup'); } else if (currentState === 'operational' && ctrl > 0) { await machine.handleInput('parent', 'execmovement', ctrl); } })); const totalPower = []; const totalFlow = []; Object.values(mgc.machines).forEach(machine => { const p = mgc.operatingPoint.readChild(machine, 'power', 'predicted', POSITIONS.AT_EQUIPMENT, mgc.unitPolicy.canonical.power); const f = mgc.operatingPoint.readChild(machine, 'flow', 'predicted', POSITIONS.DOWNSTREAM, mgc.unitPolicy.canonical.flow); if (p !== null) totalPower.push(p); if (f !== null) totalFlow.push(f); }); const sumP = totalPower.reduce((a, b) => a + b, 0); const sumF = totalFlow.reduce((a, b) => a + b, 0); mgc.operatingPoint.writeOwn('power', 'predicted', POSITIONS.AT_EQUIPMENT, sumP, mgc.unitPolicy.canonical.power); mgc.operatingPoint.writeOwn('flow', 'predicted', POSITIONS.AT_EQUIPMENT, sumF, mgc.unitPolicy.canonical.flow); if (sumP > 0) { mgc.measurements.type('efficiency').variant('predicted').position(POSITIONS.AT_EQUIPMENT).value(sumF / sumP); } } catch (err) { mgc.logger?.error?.(err); } } module.exports = { equalFlowControl, prioPercentageControl, capFlowDemand, sortMachinesByPriority, filterOutUnavailableMachines };