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>
95 lines
3.8 KiB
JavaScript
95 lines
3.8 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* PressureRouter — routes a measured pressure value into the right
|
|
* MeasurementContainer slot and triggers the downstream cascade
|
|
* (preferred-pressure resolve → predicted recompute → drift → health)
|
|
* on every pressure write, matching the pre-refactor
|
|
* `updateMeasuredPressure` semantics.
|
|
*
|
|
* Why the cascade runs for virtual sources too: dashboard-sim pressure
|
|
* sliders route through virtual children, and the operator expects the
|
|
* predicted flow/power/efficiency/Cog to refresh on every slider tick.
|
|
* The cascade is idempotent — running it on a virtual write is cheap
|
|
* and matches what a real sensor would trigger.
|
|
*
|
|
* Why getPressure() runs first: getMeasuredPressure() writes the new
|
|
* pressure differential onto predictFlow/Power/Ctrl.fDimension. Only
|
|
* after that does updatePosition() compute flow/power via
|
|
* predictFlow.y(x) — otherwise calcFlowPower runs against a stale
|
|
* fDimension and the prediction lags one update behind the slider.
|
|
*/
|
|
|
|
class PressureRouter {
|
|
/**
|
|
* @param {object} ctx
|
|
* - measurements: MeasurementContainer
|
|
* - virtualPressureChildIds: { upstream, downstream } (kept for debug only)
|
|
* - resolveMeasurementUnit(type, unit) -> canonical unit string (throws on invalid)
|
|
* - getPressure?(): resolves preferred pressure and pushes fDimension to predictors
|
|
* - updatePosition?(): recomputes predicted flow/power/efficiency/CoG at current ctrl
|
|
* - refreshDrift?(): refreshes pressure drift status
|
|
* - refreshHealth?(): refreshes prediction-health status
|
|
* - logger
|
|
*/
|
|
constructor(ctx = {}) {
|
|
this.measurements = ctx.measurements;
|
|
this.virtualPressureChildIds = ctx.virtualPressureChildIds || {};
|
|
this.resolveMeasurementUnit = ctx.resolveMeasurementUnit || ((_t, u) => u);
|
|
this.getPressure = ctx.getPressure;
|
|
this.updatePosition = ctx.updatePosition;
|
|
this.refreshDrift = ctx.refreshDrift;
|
|
this.refreshHealth = ctx.refreshHealth;
|
|
this.logger = ctx.logger || { warn() {}, debug() {} };
|
|
}
|
|
|
|
/**
|
|
* Route a measured pressure to the right container slot.
|
|
* @returns {boolean} true on successful write, false on rejection.
|
|
*/
|
|
route(position, value, context = {}) {
|
|
const pos = String(position || '').toLowerCase();
|
|
const childId = context.childId;
|
|
let unit;
|
|
try {
|
|
unit = this.resolveMeasurementUnit('pressure', context.unit);
|
|
} catch (err) {
|
|
this.logger.warn(`Rejected pressure update: ${err.message}`);
|
|
return false;
|
|
}
|
|
|
|
this.measurements
|
|
?.type('pressure').variant('measured').position(pos).child(childId)
|
|
.value(value, context.timestamp, unit);
|
|
|
|
const isVirtual = this._isVirtual(childId);
|
|
this.logger.debug(`Pressure routed: ${value} ${unit} at ${pos} from ${context.childName || 'child'} (${childId || 'unknown-id'}) virtual=${isVirtual}`);
|
|
|
|
// Legacy order: resolve preferred pressure (writes fDimension to
|
|
// predictors) BEFORE recomputing predicted flow/power at the current
|
|
// control position. Skipping any of these on virtual sources broke
|
|
// the dashboard-sim demo (NCog / efficiency / absDistFromPeak stuck
|
|
// at 0, predicted flow/power not updating with the pressure slider).
|
|
let p;
|
|
if (typeof this.getPressure === 'function') {
|
|
p = this.getPressure();
|
|
this.logger.debug(`Using pressure: ${p} for calculations`);
|
|
}
|
|
if (typeof this.updatePosition === 'function') this.updatePosition();
|
|
if (typeof this.refreshDrift === 'function') this.refreshDrift();
|
|
if (typeof this.refreshHealth === 'function') this.refreshHealth();
|
|
|
|
return true;
|
|
}
|
|
|
|
_isVirtual(childId) {
|
|
if (childId == null) return false;
|
|
for (const id of Object.values(this.virtualPressureChildIds)) {
|
|
if (id === childId) return true;
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
module.exports = PressureRouter;
|