Files
machineGroupControl/src/totals/totalsCalculator.js

118 lines
5.5 KiB
JavaScript
Raw Normal View History

const { POSITIONS } = require('generalFunctions');
const { groupFlow, groupPower, groupNCog } = require('../groupOps/groupCurves');
// Aggregations across every machine in the group.
//
// calcAbsoluteTotals scans the full input-curve envelope (worst/best case
// over the pump's entire pressure range). calcDynamicTotals reads the
// current group operating point (after equalize). activeTotals only sums
// machines that are operationally active right now.
class TotalsCalculator {
constructor(ctx = {}) {
// ctx: { machines, unitPolicy, logger, operatingPoint, isMachineActive }
// operatingPoint is a GroupOperatingPoint instance (for readChild).
// isMachineActive is delegated back to the orchestrator so the
// state-machine vocabulary lives in one place.
this.ctx = ctx;
this.dynamicTotals = { flow: { min: Infinity, max: 0 }, power: { min: Infinity, max: 0 }, NCog: 0 };
this.absoluteTotals = { flow: { min: Infinity, max: 0 }, power: { min: Infinity, max: 0 } };
}
get machines() { return this.ctx.machines || {}; }
get unitPolicy() { return this.ctx.unitPolicy; }
get logger() { return this.ctx.logger; }
get operatingPoint() { return this.ctx.operatingPoint; }
isMachineActive(id) {
if (typeof this.ctx.isMachineActive === 'function') return this.ctx.isMachineActive(id);
const s = this.machines[id]?.state?.getCurrentState?.();
return s === 'operational' || s === 'accelerating' || s === 'decelerating';
}
calcAbsoluteTotals() {
const out = { flow: { min: Infinity, max: 0 }, power: { min: Infinity, max: 0 } };
Object.values(this.machines).forEach(machine => {
const totals = { flow: { min: Infinity, max: 0 }, power: { min: Infinity, max: 0 } };
Object.entries(machine.predictFlow.inputCurve).forEach(([pressure, xyCurve]) => {
const minFlow = Math.min(...xyCurve.y);
const maxFlow = Math.max(...xyCurve.y);
const minPower = Math.min(...machine.predictPower.inputCurve[pressure].y);
const maxPower = Math.max(...machine.predictPower.inputCurve[pressure].y);
if (minFlow < totals.flow.min) totals.flow.min = minFlow;
if (minPower < totals.power.min) totals.power.min = minPower;
if (maxFlow > totals.flow.max) totals.flow.max = maxFlow;
if (maxPower > totals.power.max) totals.power.max = maxPower;
});
if (totals.flow.min < out.flow.min) out.flow.min = totals.flow.min;
if (totals.power.min < out.power.min) out.power.min = totals.power.min;
out.flow.max += totals.flow.max;
out.power.max += totals.power.max;
});
// Empty-group + sentinel reset: Infinity / -Infinity are math
// artefacts of the reducer's initial values; downstream code
// expects clean zeros.
if (out.flow.min === Infinity) { this.logger?.warn?.('Flow min Infinity — zeroing.'); out.flow.min = 0; }
if (out.power.min === Infinity) { this.logger?.warn?.('Power min Infinity — zeroing.'); out.power.min = 0; }
if (out.flow.max === -Infinity) { this.logger?.warn?.('Flow max -Infinity — zeroing.'); out.flow.max = 0; }
if (out.power.max === -Infinity) { this.logger?.warn?.('Power max -Infinity — zeroing.'); out.power.max = 0; }
this.absoluteTotals = out;
return out;
}
calcDynamicTotals() {
const out = { flow: { min: Infinity, max: 0, act: 0 }, power: { min: Infinity, max: 0, act: 0 }, NCog: 0 };
const fUnit = this.unitPolicy.canonical.flow;
const pUnit = this.unitPolicy.canonical.power;
Object.values(this.machines).forEach(machine => {
if (!machine.hasCurve) {
this.logger?.error?.(`Machine ${machine.config?.general?.id} has no valid curve — skipping.`);
return;
}
const gpf = groupFlow(machine);
const gpp = groupPower(machine);
const minFlow = gpf.currentFxyYMin;
const maxFlow = gpf.currentFxyYMax;
const minPower = gpp.currentFxyYMin;
const maxPower = gpp.currentFxyYMax;
const actFlow = this.operatingPoint?.readChild(machine, 'flow', 'predicted', POSITIONS.DOWNSTREAM, fUnit) || 0;
const actPower = this.operatingPoint?.readChild(machine, 'power', 'predicted', POSITIONS.AT_EQUIPMENT, pUnit) || 0;
if (minFlow < out.flow.min) out.flow.min = minFlow;
if (minPower < out.power.min) out.power.min = minPower;
out.flow.max += maxFlow;
out.power.max += maxPower;
out.flow.act += actFlow;
out.power.act += actPower;
out.NCog += groupNCog(machine);
});
this.dynamicTotals = out;
return out;
}
activeTotals() {
const out = { flow: { min: 0, max: 0 }, power: { min: 0, max: 0 }, countActiveMachines: 0 };
Object.entries(this.machines).forEach(([id, machine]) => {
if (!this.isMachineActive(id)) return;
const gpf = groupFlow(machine);
const gpp = groupPower(machine);
out.flow.min += gpf.currentFxyYMin;
out.flow.max += gpf.currentFxyYMax;
out.power.min += gpp.currentFxyYMin;
out.power.max += gpp.currentFxyYMax;
out.countActiveMachines += 1;
});
return out;
}
}
module.exports = TotalsCalculator;