Files
rotatingMachine/src/pressure/pressureRouter.js
znetsixe 394a972d10 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>
2026-05-14 22:52:24 +02:00

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;