Group-scope predicts for MGC combination optimization

Adds a parallel set of Predict instances (groupPredictFlow / Power / Ctrl)
that share input curves with the pump's individual predicts but maintain
their own operating point. MGC drives these via setGroupOperatingPoint()
to evaluate every pump curve at one shared manifold differential during
combination optimization, without corrupting each pump's own diagnostic
outputs (which track that pump's local sensors).

Created lazily on first use so pumps without an MGC parent pay nothing.
Pairs with generalFunctions Predict.shareInputsFrom plumbing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rene De Ren
2026-05-08 11:20:17 +02:00
parent 399e0a8c01
commit ecd5a4864b

View File

@@ -75,7 +75,7 @@ class Machine {
this.curve = this._normalizeMachineCurve(this.rawCurve); this.curve = this._normalizeMachineCurve(this.rawCurve);
this.config = this.configUtils.updateConfig(this.config, { asset: { ...this.config.asset, machineCurve: this.curve } }); this.config = this.configUtils.updateConfig(this.config, { asset: { ...this.config.asset, machineCurve: this.curve } });
//machineConfig = { ...machineConfig, asset: { ...machineConfig.asset, machineCurve: this.curve } }; // Merge curve into machineConfig //machineConfig = { ...machineConfig, asset: { ...machineConfig.asset, machineCurve: this.curve } }; // Merge curve into machineConfig
this.predictFlow = new predict({ curve: this.config.asset.machineCurve.nq }); // load nq (x : ctrl , y : flow relationship) this.predictFlow = new predict({ curve: this.config.asset.machineCurve.nq }); // load nq (x : ctrl , y : flow relationship)
this.predictPower = new predict({ curve: this.config.asset.machineCurve.np }); // load np (x : ctrl , y : power relationship) this.predictPower = new predict({ curve: this.config.asset.machineCurve.np }); // load np (x : ctrl , y : power relationship)
this.predictCtrl = new predict({ curve: this.reverseCurve(this.config.asset.machineCurve.nq) }); // load reversed nq (x: flow, y: ctrl relationship) this.predictCtrl = new predict({ curve: this.reverseCurve(this.config.asset.machineCurve.nq) }); // load reversed nq (x: flow, y: ctrl relationship)
} catch (error) { } catch (error) {
@@ -87,6 +87,18 @@ class Machine {
} }
} }
// Group-scope predicts. These are parallel "views" of the same source
// curves used by an MGC parent for combination optimization. Created
// lazily on the first setGroupOperatingPoint() call so pumps that
// never have an MGC parent pay nothing. They share input-curve refs
// with the individual predicts (see Predict.shareInputsFrom) but
// maintain independent operating-point state, so the pump's own
// sensor stream and the MGC's group operating point can coexist.
this.groupPredictFlow = null;
this.groupPredictPower = null;
this.groupPredictCtrl = null;
this.groupNCog = 0;
this.state = new state(stateConfig, this.logger); // Init State manager and pass logger this.state = new state(stateConfig, this.logger); // Init State manager and pass logger
this.errorMetrics = new nrmse(errorMetricsConfig, this.logger); this.errorMetrics = new nrmse(errorMetricsConfig, this.logger);
@@ -1013,7 +1025,7 @@ _callMeasurementHandler(measurementType, value, position, context) {
const cPower = this.predictPower.y(cCtrl); const cPower = this.predictPower.y(cCtrl);
return cPower; return cPower;
} }
// If no curve data is available, log a warning and return 0 // If no curve data is available, log a warning and return 0
this.logger.warn(`No curve data available for power calculation. Returning 0.`); this.logger.warn(`No curve data available for power calculation. Returning 0.`);
this.measurements.type("power").variant("predicted").position('atEquipment').value(0, Date.now(), this.unitPolicy.canonical.power); this.measurements.type("power").variant("predicted").position('atEquipment').value(0, Date.now(), this.unitPolicy.canonical.power);
@@ -1021,6 +1033,70 @@ _callMeasurementHandler(measurementType, value, position, context) {
} }
// ---------- Group-scope operating point (MGC parent uses this) ----------
//
// The pump's individual predicts (predictFlow / predictPower / predictCtrl)
// are driven by THIS pump's own pressure sensors via getMeasuredPressure().
// For combination optimization an MGC parent needs every pump curve
// evaluated at ONE shared operating point (the manifold differential).
// Doing that on the individual predicts would corrupt the pump's own
// diagnostic outputs. So we keep a parallel set of predicts here that
// ONLY the MGC drives via setGroupOperatingPoint(). Pump's individual
// outputs are unaffected.
// Lazily create group-scope predicts that share input curves with the
// individual ones. Safe to call multiple times.
_ensureGroupPredicts() {
if (!this.hasCurve || !this.predictFlow || !this.predictPower || !this.predictCtrl) return;
if (this.groupPredictFlow && this.groupPredictPower && this.groupPredictCtrl) return;
this.groupPredictFlow = new predict({ shareInputsFrom: this.predictFlow });
this.groupPredictPower = new predict({ shareInputsFrom: this.predictPower });
this.groupPredictCtrl = new predict({ shareInputsFrom: this.predictCtrl });
}
// External (MGC) API: set the group operating point. Recomputes the
// group predicts at the new differential pressure and updates groupNCog.
// Does NOT touch this.predictFlow / predictPower / predictCtrl /
// this.NCog / this.measurements.
setGroupOperatingPoint(downstreamPa, upstreamPa) {
this._ensureGroupPredicts();
if (!this.groupPredictFlow || !this.groupPredictPower) return;
if (!Number.isFinite(downstreamPa) || !Number.isFinite(upstreamPa)) return;
const diff = downstreamPa - upstreamPa;
if (diff <= 0) return;
this.groupPredictFlow.fDimension = diff;
this.groupPredictPower.fDimension = diff;
if (this.groupPredictCtrl) this.groupPredictCtrl.fDimension = diff;
this.groupNCog = this._calcGroupCog();
}
// Power consumption at flow on the group operating point (used by
// MGC's marginal-cost refinement). Falls back to the individual
// calculation if the group predicts haven't been initialised.
groupCalcPower(flow) {
if (!this.groupPredictFlow || !this.groupPredictPower || !this.groupPredictCtrl) {
return this.inputFlowCalcPower(flow);
}
this.groupPredictCtrl.currentX = flow;
const cCtrl = this.groupPredictCtrl.y(flow);
this.groupPredictPower.currentX = cCtrl;
return this.groupPredictPower.y(cCtrl);
}
// Mirrors calcCog() but reads from group predicts. Returns the
// normalised cog (0..1) — the MGC optimizer uses this for BEP-Gravitation.
_calcGroupCog() {
if (!this.groupPredictFlow || !this.groupPredictPower) return 0;
const powerCurve = this.groupPredictPower.currentFxyCurve[this.groupPredictPower.currentF];
const flowCurve = this.groupPredictFlow.currentFxyCurve[this.groupPredictFlow.currentF];
if (!powerCurve?.y?.length || !flowCurve?.y?.length) return 0;
const { peakIndex } = this.calcEfficiencyCurve(powerCurve, flowCurve);
const yMin = this.groupPredictFlow.currentFxyYMin;
const yMax = this.groupPredictFlow.currentFxyYMax;
if (yMax <= yMin) return 0;
return (flowCurve.y[peakIndex] - yMin) / (yMax - yMin);
}
// Function to predict control value for a desired flow // Function to predict control value for a desired flow
calcCtrl(x) { calcCtrl(x) {
if(this.hasCurve) { if(this.hasCurve) {