hydraulic efficiency η = (Q·ΔP)/P + asset registry rename

The pre-existing efficiency formula `η = flow/power` produced tiny SI-unit
values (m³/J ≈ 1e-5), was monotonic in ctrl for centrifugal-pump curves
(no interior peak), and made NCog collapse to 0 — which cascaded into MGC
reporting BEP-position 0.0% always. Replaced with hydraulic efficiency
η = (Q·ΔP)/P_shaft, the dimensionless 0..1 ratio that has a real BEP and
matches the form MGC's group-level math uses.

- prediction/efficiencyMath.js:
  * calcEfficiencyCurve takes pressureDiffPa; η = 0 when dP missing
  * calcCog guards (yMax > yMin) before computing NCog (was unguarded /0)
  * calcEfficiency falls back to predictFlow.currentF when measured ΔP is
    missing, so predicted-variant calls still produce a meaningful η before
    the differential measurement settles
- specificClass.js:
  * Asset-registry lookup renamed: 'machine' → 'rotatingmachine' (matches
    the datasets/assetData/ rename in generalFunctions). The error path
    quotes the new filename so operators can find it.
  * Two-call-site fix: with default-param stateConfig={}, the single-arg
    constructor path (BaseNodeAdapter calls `new Machine(this.config)`
    after pre-setting Machine._pendingExtras) was silently clobbering the
    pre-set extras. Only overwrite when the caller explicitly passes them.
  * Push port 0 deltas (notifyOutputChanged) after prediction updates so
    dashboards see state + predicted-flow changes as they happen.
- pressure/pressureRouter.js: routing + fallback hardening (the trigger
  for the bep-distance-cascade reproduction).
- display/workingCurves.js: Q-H curve generator extended.
- New tests:
  * test/integration/qh-curve.integration.test.js — Q-H curve shape
  * test/integration/bep-distance-cascade.integration.test.js — reproduces
    the dashboard report (absDistFromPeak=0, NCog=0, efficiency=0 after a
    setpoint move) at the unit level so future regressions fail loudly.

Full suite: 214/214 pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
znetsixe
2026-05-14 22:52:24 +02:00
parent 28344c6810
commit 394a972d10
9 changed files with 371 additions and 44 deletions

View File

@@ -43,8 +43,24 @@ class Machine extends BaseDomain {
// ES6 forbids `this` before super(). Single-threaded JS means stashing
// on the class itself between the caller's args and super() is race-free;
// configure() picks the extras up immediately after.
constructor(machineConfig = {}, stateConfig = {}, errorMetricsConfig = {}) {
Machine._pendingExtras = { stateConfig, errorMetricsConfig };
//
// Two call sites exist:
// - nodeClass.buildDomainConfig() pre-sets Machine._pendingExtras and
// then BaseNodeAdapter calls `new Machine(this.config)` (single-arg).
// - Tests / direct callers pass (machineConfig, stateConfig, errMetrics)
// explicitly.
// With default-param `stateConfig={}`, the single-arg path was silently
// clobbering the pre-set extras with an empty object, so the state machine
// booted with schema defaults (warmingup=5s, speed=1%/s, mode=dynspeed)
// regardless of what the editor saved. Only overwrite when an explicit
// value is provided.
constructor(machineConfig = {}, stateConfig, errorMetricsConfig) {
if (stateConfig !== undefined || errorMetricsConfig !== undefined) {
Machine._pendingExtras = {
stateConfig: stateConfig ?? {},
errorMetricsConfig: errorMetricsConfig ?? {},
};
}
super(machineConfig);
}
@@ -72,7 +88,7 @@ class Machine extends BaseDomain {
// If the registry has no entry for this model, assetMetadata is null and
// we'll error out with a clear message below.
this.assetMetadata = this.model
? assetResolver.resolveAssetMetadata('machine', this.model)
? assetResolver.resolveAssetMetadata('rotatingmachine', this.model)
: null;
if (!this.model) {
@@ -81,7 +97,7 @@ class Machine extends BaseDomain {
return;
}
if (!this.assetMetadata) {
this.logger.error(`rotatingMachine: model '${this.model}' not found in asset registry (datasets/assetData/machine.json). Cannot derive supplier/type/units.`);
this.logger.error(`rotatingMachine: model '${this.model}' not found in asset registry (datasets/assetData/rotatingmachine.json). Cannot derive supplier/type/units.`);
this._installNullPredictors();
return;
}
@@ -291,6 +307,10 @@ class Machine extends BaseDomain {
this.measurements.type('power').variant('predicted').position('atEquipment').value(0, Date.now(), pu);
}
this._updatePredictionHealth();
// Push port 0 deltas so downstream dashboards / probes see state +
// predicted-flow updates as they happen. BaseNodeAdapter listens for
// 'output-changed' on this.emitter to fire _emitOutputs().
this.notifyOutputChanged();
}
updatePosition() {
@@ -302,6 +322,7 @@ class Machine extends BaseDomain {
this.calcDistanceBEP(efficiency, cog, minEfficiency);
}
this._updatePredictionHealth();
this.notifyOutputChanged();
}
// ── mode + input dispatch ──────────────────────────────────────────
@@ -371,7 +392,8 @@ class Machine extends BaseDomain {
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 dP = this.groupPredictFlow.currentF;
const { peakIndex } = this.calcEfficiencyCurve(powerCurve, flowCurve, dP);
const yMin = this.groupPredictFlow.currentFxyYMin;
const yMax = this.groupPredictFlow.currentFxyYMax;
if (yMax <= yMin) return 0;
@@ -381,7 +403,7 @@ class Machine extends BaseDomain {
// ── efficiency math (delegates) ────────────────────────────────────
calcCog() { return eff.calcCog(this); }
calcEfficiencyCurve(p, f) { return eff.calcEfficiencyCurve(p, f); }
calcEfficiencyCurve(p, f, dP) { return eff.calcEfficiencyCurve(p, f, dP); }
calcEfficiency(power, flow, variant) { return eff.calcEfficiency(this, power, flow, variant); }
calcDistanceBEP(e, max, min) { return eff.calcDistanceBEP(this, e, max, min); }
calcDistanceFromPeak(e, peak) { return eff.calcDistanceFromPeak(e, peak); }