Files
machineGroupControl/src/groupOps/groupOperatingPoint.js
znetsixe 619b1311d2 P4 wave 1: extract MGC concerns into focused modules
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>
2026-05-10 20:45:23 +02:00

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;