diff --git a/src/specificClass.js b/src/specificClass.js index bab1fdd..d394cc4 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -75,7 +75,7 @@ class Machine { this.curve = this._normalizeMachineCurve(this.rawCurve); 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 - 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.predictCtrl = new predict({ curve: this.reverseCurve(this.config.asset.machineCurve.nq) }); // load reversed nq (x: flow, y: ctrl relationship) } 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.errorMetrics = new nrmse(errorMetricsConfig, this.logger); @@ -1013,7 +1025,7 @@ _callMeasurementHandler(measurementType, value, position, context) { const cPower = this.predictPower.y(cCtrl); return cPower; } - + // 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.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 calcCtrl(x) { if(this.hasCurve) {