UnitPolicy now exposes canonical/output/curve as both methods AND frozen property bags, so this.unitPolicy = this.constructor.unitPolicy works directly. Removes the 14-line _unitView assembly in configure(). specificClass.js 336→318. 77/77 tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
211 lines
9.7 KiB
JavaScript
211 lines
9.7 KiB
JavaScript
'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 };
|