'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;