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>
94 lines
4.3 KiB
JavaScript
94 lines
4.3 KiB
JavaScript
const { POSITIONS } = require('generalFunctions');
|
|
|
|
// Group-scope measurement read/write + header equalization.
|
|
//
|
|
// Pulled out of specificClass during the P4 refactor: the equalization
|
|
// logic is the source of truth for the "one consistent header operating
|
|
// point" that the optimizer and totals modules both depend on. Keeping it
|
|
// in one place makes the order-of-operations explicit (read header, write
|
|
// onto every machine's group-scope predicts).
|
|
class GroupOperatingPoint {
|
|
constructor(ctx = {}) {
|
|
// ctx: { measurements, machines, unitPolicy, logger }
|
|
// Late-binding via getters in the orchestrator works too — but
|
|
// passing the live references avoids re-plumbing setters.
|
|
this.ctx = ctx;
|
|
}
|
|
|
|
get measurements() { return this.ctx.measurements; }
|
|
get machines() { return this.ctx.machines; }
|
|
get unitPolicy() { return this.ctx.unitPolicy; }
|
|
get logger() { return this.ctx.logger; }
|
|
|
|
readChild(machine, type, variant, position, unit = null) {
|
|
return machine?.measurements
|
|
?.type(type)
|
|
?.variant(variant)
|
|
?.position(position)
|
|
?.getCurrentValue(unit || undefined);
|
|
}
|
|
|
|
writeOwn(type, variant, position, value, unit = null, timestamp = Date.now()) {
|
|
if (!Number.isFinite(value)) return;
|
|
this.measurements
|
|
.type(type)
|
|
.variant(variant)
|
|
.position(position)
|
|
.value(value, timestamp, unit || undefined);
|
|
}
|
|
|
|
// Force every machine's predict-curve interpolators to use the same
|
|
// (header) differential pressure for MGC's optimization. See the
|
|
// original _equalizeOperatingPoint commentary in specificClass for
|
|
// the full rationale (header source order, fDimension fallback).
|
|
equalize() {
|
|
const machines = this.machines || {};
|
|
if (Object.keys(machines).length === 0) return;
|
|
|
|
const pressureUnit = this.unitPolicy.canonical.pressure;
|
|
const groupHeaderDown = this.measurements
|
|
.type('pressure').variant('measured').position(POSITIONS.DOWNSTREAM)
|
|
.getCurrentValue(pressureUnit);
|
|
const groupHeaderUp = this.measurements
|
|
.type('pressure').variant('measured').position(POSITIONS.UPSTREAM)
|
|
.getCurrentValue(pressureUnit);
|
|
|
|
const childDown = [];
|
|
const childUp = [];
|
|
Object.values(machines).forEach(machine => {
|
|
const d = this.readChild(machine, 'pressure', 'measured', POSITIONS.DOWNSTREAM, pressureUnit);
|
|
const u = this.readChild(machine, 'pressure', 'measured', POSITIONS.UPSTREAM, pressureUnit);
|
|
if (Number.isFinite(d) && d > 0) childDown.push(d);
|
|
if (Number.isFinite(u) && u > 0) childUp.push(u);
|
|
});
|
|
|
|
const downIsHeader = Number.isFinite(groupHeaderDown) && groupHeaderDown > 0;
|
|
const upIsHeader = Number.isFinite(groupHeaderUp) && groupHeaderUp > 0;
|
|
const headerDownstream = downIsHeader ? groupHeaderDown : (childDown.length ? Math.max(...childDown) : 0);
|
|
const headerUpstream = upIsHeader ? groupHeaderUp : (childUp.length ? Math.min(...childUp) : 0);
|
|
|
|
const headerDiff = headerDownstream - headerUpstream;
|
|
if (!Number.isFinite(headerDiff) || headerDiff <= 0) {
|
|
this.logger?.debug?.(`Skipping equalization: invalid header diff ${headerDiff} (down=${headerDownstream}, up=${headerUpstream})`);
|
|
return;
|
|
}
|
|
|
|
this.logger?.debug?.(`Equalizing operating point: down=${headerDownstream}, up=${headerUpstream}, diff=${headerDiff}`);
|
|
|
|
Object.values(machines).forEach(machine => {
|
|
if (typeof machine.setGroupOperatingPoint === 'function') {
|
|
machine.setGroupOperatingPoint(headerDownstream, headerUpstream);
|
|
} else {
|
|
// Older rotatingMachine without the group API — direct
|
|
// fDimension write keeps demos working while submodules
|
|
// are rolled forward.
|
|
if (machine.predictFlow) machine.predictFlow.fDimension = headerDiff;
|
|
if (machine.predictPower) machine.predictPower.fDimension = headerDiff;
|
|
if (machine.predictCtrl) machine.predictCtrl.fDimension = headerDiff;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
module.exports = GroupOperatingPoint;
|