P5 wave 1: extract rotatingMachine concerns into focused modules
src/curves/ loader + normalizer (with cross-pressure anomaly
detection) + reverseCurve helper
src/prediction/ predictors (predictFlow/Power/Ctrl) +
groupPredictors (lazy group-scope views) +
OperatingPoint (pressure-driven prediction setpoints)
src/drift/ DriftAssessor (per-metric drift) + PredictionHealth
(composes flow/power/pressure into HealthStatus +
confidence sibling — see OPEN_QUESTIONS 2026-05-10)
src/pressure/ VirtualPressureChildren (dashboard-sim) +
PressureInitialization (real-vs-virtual tracking) +
PressureRouter (dispatches by position)
src/state/ stateBindings (state.emitter listener helper) +
isOperationalState
src/measurement/ measurementHandlers (dispatcher for flow/power/temp/pressure)
src/flow/ flowController (handleInput body — execSequence,
execMovement, flowMovement, emergencystop)
src/display/ workingCurves (showWorkingCurves + showCoG admin)
src/commands/ canonical names: set.mode, cmd.startup/shutdown/estop,
set.setpoint, set.flow-setpoint,
data.simulate-measurement, query.curves, query.cog,
child.register. execSequence demuxes by payload.action
to canonical cmd.* handlers.
CONTRACT.md inputs/outputs/events/children surface
110 basic tests pass (100 new + 10 pre-existing).
specificClass.js / nodeClass.js untouched — integration in P5 wave 2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
135
src/drift/driftAssessor.js
Normal file
135
src/drift/driftAssessor.js
Normal file
@@ -0,0 +1,135 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* DriftAssessor — extracted from rotatingMachine specificClass.
|
||||
*
|
||||
* Wraps the generalFunctions errorMetrics into a per-metric drift
|
||||
* pipeline (flow / power). Holds the latest drift objects so
|
||||
* predictionHealth can reuse them; the host node still mirrors them
|
||||
* onto its own fields for output compatibility.
|
||||
*/
|
||||
|
||||
class DriftAssessor {
|
||||
/**
|
||||
* @param {object} ctx
|
||||
* - errorMetrics: assessPoint(metricId, predicted, measured, opts) + assessDrift(...)
|
||||
* - measurements: MeasurementContainer (for assessDrift history pulls)
|
||||
* - driftProfiles: { flow, power, ... }
|
||||
* - resolveProcessRange(metricId, predicted, measured) -> { processMin, processMax }
|
||||
* - measurementPositionForMetric(metricId) -> string
|
||||
* - logger: { warn, debug, ... }
|
||||
*/
|
||||
constructor(ctx = {}) {
|
||||
this.errorMetrics = ctx.errorMetrics;
|
||||
this.measurements = ctx.measurements;
|
||||
this.driftProfiles = ctx.driftProfiles || {};
|
||||
this.resolveProcessRange = ctx.resolveProcessRange;
|
||||
this.measurementPositionForMetric = ctx.measurementPositionForMetric;
|
||||
this.logger = ctx.logger || { warn() {}, debug() {} };
|
||||
this.latest = { flow: null, power: null };
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute drift for a metric given a freshly-arrived measured value.
|
||||
* Returns the drift object (or null on error / non-finite inputs).
|
||||
*/
|
||||
updateMetricDrift(metricId, measuredValue, context = {}) {
|
||||
const position = this._positionForMetric(metricId);
|
||||
const predictedValue = this._getPredicted(metricId, position);
|
||||
const measured = Number(measuredValue);
|
||||
if (!Number.isFinite(predictedValue) || !Number.isFinite(measured)) return null;
|
||||
|
||||
const { processMin, processMax } = this._processRange(metricId, predictedValue, measured);
|
||||
const timestamp = Number(context.timestamp || Date.now());
|
||||
const profile = this.driftProfiles[metricId] || {};
|
||||
|
||||
try {
|
||||
const drift = this.errorMetrics.assessPoint(metricId, predictedValue, measured, {
|
||||
...profile,
|
||||
processMin,
|
||||
processMax,
|
||||
predictedTimestamp: timestamp,
|
||||
measuredTimestamp: timestamp,
|
||||
});
|
||||
if (drift && drift.valid) this.latest[metricId] = drift;
|
||||
return drift;
|
||||
} catch (err) {
|
||||
this.logger.warn(`Drift update failed for metric '${metricId}': ${err.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull stored predicted/measured series and run a full drift assessment.
|
||||
*/
|
||||
assessDrift(measurement, processMin, processMax) {
|
||||
const metricId = String(measurement || '').toLowerCase();
|
||||
const position = this._positionForMetric(metricId);
|
||||
const predicted = this.measurements
|
||||
?.type(metricId).variant('predicted').position(position).getAllValues();
|
||||
const measured = this.measurements
|
||||
?.type(metricId).variant('measured').position(position).getAllValues();
|
||||
if (!predicted?.values || !measured?.values) return null;
|
||||
|
||||
return this.errorMetrics.assessDrift(
|
||||
predicted.values,
|
||||
measured.values,
|
||||
processMin,
|
||||
processMax,
|
||||
{
|
||||
metricId,
|
||||
predictedTimestamps: predicted.timestamps,
|
||||
measuredTimestamps: measured.timestamps,
|
||||
...(this.driftProfiles[metricId] || {}),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pure helper: reduce a confidence figure by drift severity and push
|
||||
* matching flag strings. Returns the updated confidence.
|
||||
*/
|
||||
applyDriftPenalty(drift, confidence, flags, prefix) {
|
||||
if (!drift || !drift.valid || !Number.isFinite(drift.nrmse)) return confidence;
|
||||
if (drift.immediateLevel >= 3) {
|
||||
confidence -= 0.3;
|
||||
flags.push(`${prefix}_high_immediate_drift`);
|
||||
} else if (drift.immediateLevel === 2) {
|
||||
confidence -= 0.2;
|
||||
flags.push(`${prefix}_medium_immediate_drift`);
|
||||
} else if (drift.immediateLevel === 1) {
|
||||
confidence -= 0.1;
|
||||
flags.push(`${prefix}_low_immediate_drift`);
|
||||
}
|
||||
if (drift.longTermLevel >= 2) {
|
||||
confidence -= 0.1;
|
||||
flags.push(`${prefix}_long_term_drift`);
|
||||
}
|
||||
return confidence;
|
||||
}
|
||||
|
||||
_positionForMetric(metricId) {
|
||||
if (typeof this.measurementPositionForMetric === 'function') {
|
||||
return this.measurementPositionForMetric(metricId);
|
||||
}
|
||||
return metricId === 'flow' ? 'downstream' : 'atEquipment';
|
||||
}
|
||||
|
||||
_processRange(metricId, predicted, measured) {
|
||||
if (typeof this.resolveProcessRange === 'function') {
|
||||
return this.resolveProcessRange(metricId, predicted, measured);
|
||||
}
|
||||
const lo = Math.min(predicted, measured);
|
||||
const hi = Math.max(predicted, measured);
|
||||
return { processMin: lo, processMax: hi > lo ? hi : lo + 1 };
|
||||
}
|
||||
|
||||
_getPredicted(metricId, position) {
|
||||
return Number(
|
||||
this.measurements
|
||||
?.type(metricId).variant('predicted').position(position).getCurrentValue(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DriftAssessor;
|
||||
Reference in New Issue
Block a user