155 lines
5.3 KiB
JavaScript
155 lines
5.3 KiB
JavaScript
//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;
|