src/groupOps/ groupOperatingPoint + groupCurves (pure functions)
src/totals/ totalsCalculator (dynamic + absolute + active)
src/combinatorics/ pumpCombinations (validPumpCombinations + checkSpecialCases)
src/optimizer/ bestCombination (CoG) + bepGravitation (BEP-G + marginal-cost)
src/efficiency/ groupEfficiency (calc + distance helpers)
src/dispatch/ demandDispatcher (LatestWinsGate-based; replaces
_dispatchInFlight + _delayedCall)
src/commands/ canonical names from start (set.mode/scaling/demand,
child.register) + legacy aliases
CONTRACT.md inputs/outputs/events surface
53 basic tests pass (52 new + 1 pre-existing).
specificClass.js / nodeClass.js untouched — integration in P4 wave 2.
Findings flagged via agents (TODO append to OPEN_QUESTIONS.md):
- calcGroupEfficiency.maxEfficiency is actually the mean (misleading name)
- checkSpecialCases has a no-op `return false` inside forEach
- MGC doesn't route cmd.startup/shutdown/estop — confirm if station broadcasts need it
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
118 lines
5.5 KiB
JavaScript
118 lines
5.5 KiB
JavaScript
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;
|