Files
machineGroupControl/src/efficiency/groupEfficiency.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

91 lines
3.3 KiB
JavaScript

'use strict';
// Aggregates per-machine efficiency (cog) into group-level metrics and
// computes distance-from-peak. Extracted verbatim from specificClass.js
// (calcGroupEfficiency / calcDistanceFromPeak / calcRelativeDistanceFromPeak /
// calcDistanceBEP) so the orchestrator can delegate without inheriting
// the arithmetic.
class GroupEfficiency {
constructor(ctx = {}) {
this.ctx = ctx;
this.logger = ctx.logger || null;
this.interpolation = ctx.interpolation || null;
this.measurements = ctx.measurements || null;
this.machines = ctx.machines || null;
}
// Average of per-machine cog plus the worst-performing machine's cog.
// `maxEfficiency` is misleadingly named — it is in fact the MEAN cog
// across all machines, treated as the group-level "peak" target.
// Kept that way for behavioural parity with the original.
calcGroupEfficiency(machines) {
const target = machines || this.machines;
let cumEfficiency = 0;
let machineCount = 0;
let lowestEfficiency = Infinity;
Object.entries(target || {}).forEach(([_id, machine]) => {
cumEfficiency += machine.cog;
if (machine.cog < lowestEfficiency) {
lowestEfficiency = machine.cog;
}
machineCount++;
});
const maxEfficiency = cumEfficiency / machineCount;
const currentEfficiency = this._readCurrentEfficiency();
return { maxEfficiency, lowestEfficiency, currentEfficiency };
}
calcDistanceFromPeak(currentEfficiency, peakEfficiency) {
return Math.abs(currentEfficiency - peakEfficiency);
}
// Maps current efficiency onto [0..1] across [maxEfficiency..minEfficiency].
// Degenerate case (max === min) collapses the band to a point — return 1.
calcRelativeDistanceFromPeak(currentEfficiency, maxEfficiency, minEfficiency) {
let distance = 1;
if (currentEfficiency != null && maxEfficiency !== minEfficiency && this.interpolation) {
distance = this.interpolation.interpolate_lin_single_point(
currentEfficiency,
maxEfficiency,
minEfficiency,
0,
1,
);
}
return distance;
}
// Returns both abs + rel; orchestrator decides whether to mirror onto
// its own this.absDistFromPeak / this.relDistFromPeak fields.
calcDistanceBEP(currentEfficiency, maxEfficiency, minEfficiency) {
const absDistFromPeak = this.calcDistanceFromPeak(currentEfficiency, maxEfficiency);
const relDistFromPeak = this.calcRelativeDistanceFromPeak(
currentEfficiency,
maxEfficiency,
minEfficiency,
);
return { absDistFromPeak, relDistFromPeak };
}
// Pull the latest measured efficiency from the container if one was
// provided. Optional convenience — orchestrator may read it directly.
_readCurrentEfficiency() {
if (!this.measurements) return null;
try {
return this.measurements
.type('efficiency')
.variant('predicted')
.position('atequipment')
.getCurrentValue();
} catch (_err) {
return null;
}
}
}
module.exports = GroupEfficiency;