//load local dependencies const EventEmitter = require('events'); //load all config modules const defaultConfig = require('./nrmseConfig.json'); const ConfigUtils = require('../configUtils'); class ErrorMetrics { constructor(config = {}, logger) { this.emitter = new EventEmitter(); // Own EventEmitter this.configUtils = new ConfigUtils(defaultConfig); this.config = this.configUtils.initConfig(config); // Init after config is set this.logger = logger; // For long-term NRMSD accumulation this.cumNRMSD = 0; this.cumCount = 0; } //INCLUDE timestamps in the next update OLIFANT meanSquaredError(predicted, measured) { if (predicted.length !== measured.length) { this.logger.error("Comparing MSE Arrays must have the same length."); return 0; } let sumSqError = 0; for (let i = 0; i < predicted.length; i++) { const err = predicted[i] - measured[i]; sumSqError += err * err; } return sumSqError / predicted.length; } rootMeanSquaredError(predicted, measured) { return Math.sqrt(this.meanSquaredError(predicted, measured)); } normalizedRootMeanSquaredError(predicted, measured, processMin, processMax) { const range = processMax - processMin; if (range <= 0) { this.logger.error("Invalid process range: processMax must be greater than processMin."); } const rmse = this.rootMeanSquaredError(predicted, measured); return rmse / range; } longTermNRMSD(input) { const storedNRMSD = this.cumNRMSD; const storedCount = this.cumCount; const newCount = storedCount + 1; // Update cumulative values this.cumCount = newCount; // Calculate new running average if (storedCount === 0) { this.cumNRMSD = input; // First value } else { // Running average formula: newAvg = oldAvg + (newValue - oldAvg) / newCount this.cumNRMSD = storedNRMSD + (input - storedNRMSD) / newCount; } if(newCount >= 100) { // Return the current NRMSD value, not just the contribution from this sample return this.cumNRMSD; } return 0; } normalizeUsingRealtime(predicted, measured) { const realtimeMin = Math.min(Math.min(...predicted), Math.min(...measured)); const realtimeMax = Math.max(Math.max(...predicted), Math.max(...measured)); const range = realtimeMax - realtimeMin; if (range <= 0) { throw new Error("Invalid process range: processMax must be greater than processMin."); } const rmse = this.rootMeanSquaredError(predicted, measured); return rmse / range; } detectImmediateDrift(nrmse) { let ImmDrift = {}; this.logger.debug(`checking immediate drift with thresholds : ${this.config.thresholds.NRMSE_HIGH} ${this.config.thresholds.NRMSE_MEDIUM} ${this.config.thresholds.NRMSE_LOW}`); switch (true) { case( nrmse > this.config.thresholds.NRMSE_HIGH ) : ImmDrift = {level : 3 , feedback : "High immediate drift detected"}; break; case( nrmse > this.config.thresholds.NRMSE_MEDIUM ) : ImmDrift = {level : 2 , feedback : "Medium immediate drift detected"}; break; case(nrmse > this.config.thresholds.NRMSE_LOW ): ImmDrift = {level : 1 , feedback : "Low immediate drift detected"}; break; default: ImmDrift = {level : 0 , feedback : "No drift detected"}; } return ImmDrift; } detectLongTermDrift(longTermNRMSD) { let LongTermDrift = {}; this.logger.debug(`checking longterm drift with thresholds : ${this.config.thresholds.LONG_TERM_HIGH} ${this.config.thresholds.LONG_TERM_MEDIUM} ${this.config.thresholds.LONG_TERM_LOW}`); switch (true) { case(Math.abs(longTermNRMSD) > this.config.thresholds.LONG_TERM_HIGH) : LongTermDrift = {level : 3 , feedback : "High long-term drift detected"}; break; case (Math.abs(longTermNRMSD) > this.config.thresholds.LONG_TERM_MEDIUM) : LongTermDrift = {level : 2 , feedback : "Medium long-term drift detected"}; break; case ( Math.abs(longTermNRMSD) > this.config.thresholds.LONG_TERM_LOW ) : LongTermDrift = {level : 1 , feedback : "Low long-term drift detected"}; break; default: LongTermDrift = {level : 0 , feedback : "No drift detected"}; } return LongTermDrift; } detectDrift(nrmse, longTermNRMSD) { const ImmDrift = this.detectImmediateDrift(nrmse); const LongTermDrift = this.detectLongTermDrift(longTermNRMSD); return { ImmDrift, LongTermDrift }; } // asses the drift assessDrift(predicted, measured, processMin, processMax) { // Compute NRMSE and check for immediate drift const nrmse = this.normalizedRootMeanSquaredError(predicted, measured, processMin, processMax); this.logger.debug(`NRMSE: ${nrmse}`); // cmopute long-term NRMSD and add result to cumalitve NRMSD const longTermNRMSD = this.longTermNRMSD(nrmse); // return the drift // Return the drift assessment object const driftAssessment = this.detectDrift(nrmse, longTermNRMSD); return { nrmse, longTermNRMSD, immediateLevel: driftAssessment.ImmDrift.level, immediateFeedback: driftAssessment.ImmDrift.feedback, longTermLevel: driftAssessment.LongTermDrift.level, longTermFeedback: driftAssessment.LongTermDrift.feedback }; } } module.exports = ErrorMetrics;