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;