Complete general functions

This commit is contained in:
znetsixe
2025-05-26 17:09:18 +02:00
parent 47dfe3850f
commit 2e57034f14
44 changed files with 6776 additions and 0 deletions

View File

@@ -0,0 +1,297 @@
const ErrorMetrics = require('./errorMetrics');
// Dummy logger for tests
const logger = {
error: console.error,
debug: console.log,
info: console.log
};
const config = {
thresholds: {
NRMSE_LOW: 0.05,
NRMSE_MEDIUM: 0.10,
NRMSE_HIGH: 0.15,
LONG_TERM_LOW: 0.02,
LONG_TERM_MEDIUM: 0.04,
LONG_TERM_HIGH: 0.06
}
};
class ErrorMetricsTester {
constructor() {
this.totalTests = 0;
this.passedTests = 0;
this.failedTests = 0;
this.errorMetrics = new ErrorMetrics(config, logger);
}
assert(condition, message) {
this.totalTests++;
if (condition) {
console.log(`✓ PASS: ${message}`);
this.passedTests++;
} else {
console.log(`✗ FAIL: ${message}`);
this.failedTests++;
}
}
testMeanSquaredError() {
console.log("\nTesting Mean Squared Error...");
const predicted = [1, 2, 3];
const measured = [1, 3, 5];
const mse = this.errorMetrics.meanSquaredError(predicted, measured);
this.assert(Math.abs(mse - 1.67) < 0.1, "MSE correctly calculated");
}
testRootMeanSquaredError() {
console.log("\nTesting Root Mean Squared Error...");
const predicted = [1, 2, 3];
const measured = [1, 3, 5];
const rmse = this.errorMetrics.rootMeanSquaredError(predicted, measured);
this.assert(Math.abs(rmse - 1.29) < 0.1, "RMSE correctly calculated");
}
testNormalizedRMSE() {
console.log("\nTesting Normalized RMSE...");
const predicted = [100, 102, 104];
const measured = [98, 103, 107];
const processMin = 90, processMax = 110;
const nrmse = this.errorMetrics.normalizedRootMeanSquaredError(predicted, measured, processMin, processMax);
this.assert(typeof nrmse === 'number' && nrmse > 0, "Normalized RMSE calculated correctly");
}
testNormalizeUsingRealtime() {
console.log("\nTesting Normalize Using Realtime...");
const predicted = [100, 102, 104];
const measured = [98, 103, 107];
try {
const nrmse = this.errorMetrics.normalizeUsingRealtime(predicted, measured);
this.assert(typeof nrmse === 'number' && nrmse > 0, "Normalize using realtime calculated correctly");
} catch (error) {
this.assert(false, `Normalize using realtime failed: ${error.message}`);
}
// Test with identical values to check error handling
const sameValues = [100, 100, 100];
try {
this.errorMetrics.normalizeUsingRealtime(sameValues, sameValues);
this.assert(false, "Should throw error with identical values");
} catch (error) {
this.assert(true, "Correctly throws error when min/max are the same");
}
}
testLongTermNRMSD() {
console.log("\nTesting Long Term NRMSD Accumulation...");
// Reset the accumulation values
this.errorMetrics.cumNRMSD = 0;
this.errorMetrics.cumCount = 0;
let lastValue = 0;
for (let i = 0; i < 100; i++) {
lastValue = this.errorMetrics.longTermNRMSD(0.1 + i * 0.001);
}
this.assert(
this.errorMetrics.cumCount === 100 &&
this.errorMetrics.cumNRMSD !== 0 &&
lastValue !== 0,
"Long term NRMSD accumulates over 100 iterations"
);
// Test that values are returned only after accumulating 100 samples
this.errorMetrics.cumNRMSD = 0;
this.errorMetrics.cumCount = 0;
for (let i = 0; i < 99; i++) {
const result = this.errorMetrics.longTermNRMSD(0.1);
this.assert(result === 0, "No longTermNRMSD returned before 100 samples");
}
// Use a different value for the 100th sample to ensure a non-zero result
const result = this.errorMetrics.longTermNRMSD(0.2);
this.assert(result !== 0, "longTermNRMSD returned after 100 samples");
}
testDetectImmediateDrift() {
console.log("\nTesting Immediate Drift Detection...");
// Test high drift
let drift = this.errorMetrics.detectImmediateDrift(config.thresholds.NRMSE_HIGH + 0.01);
this.assert(drift.level === 3, "Detects high immediate drift correctly");
// Test medium drift
drift = this.errorMetrics.detectImmediateDrift(config.thresholds.NRMSE_MEDIUM + 0.01);
this.assert(drift.level === 2, "Detects medium immediate drift correctly");
// Test low drift
drift = this.errorMetrics.detectImmediateDrift(config.thresholds.NRMSE_LOW + 0.01);
this.assert(drift.level === 1, "Detects low immediate drift correctly");
// Test no drift
drift = this.errorMetrics.detectImmediateDrift(config.thresholds.NRMSE_LOW - 0.01);
this.assert(drift.level === 0, "Detects no immediate drift correctly");
}
testDetectLongTermDrift() {
console.log("\nTesting Long Term Drift Detection...");
// Test high drift
let drift = this.errorMetrics.detectLongTermDrift(config.thresholds.LONG_TERM_HIGH + 0.01);
this.assert(drift.level === 3, "Detects high long-term drift correctly");
// Test medium drift
drift = this.errorMetrics.detectLongTermDrift(config.thresholds.LONG_TERM_MEDIUM + 0.01);
this.assert(drift.level === 2, "Detects medium long-term drift correctly");
// Test low drift
drift = this.errorMetrics.detectLongTermDrift(config.thresholds.LONG_TERM_LOW + 0.01);
this.assert(drift.level === 1, "Detects low long-term drift correctly");
// Test no drift
drift = this.errorMetrics.detectLongTermDrift(config.thresholds.LONG_TERM_LOW - 0.01);
this.assert(drift.level === 0, "Detects no long-term drift correctly");
// Test negative drift values
drift = this.errorMetrics.detectLongTermDrift(-config.thresholds.LONG_TERM_HIGH - 0.01);
this.assert(drift.level === 3, "Detects negative high long-term drift correctly");
}
testDriftDetection() {
console.log("\nTesting Combined Drift Detection...");
let nrmseHigh = config.thresholds.NRMSE_HIGH + 0.01;
let ltNRMSD = 0;
let result = this.errorMetrics.detectDrift(nrmseHigh, ltNRMSD);
this.assert(
result !== null &&
result.ImmDrift &&
result.ImmDrift.level === 3 &&
result.LongTermDrift.level === 0,
"Detects high immediate drift with no long-term drift"
);
nrmseHigh = config.thresholds.NRMSE_LOW - 0.01;
ltNRMSD = config.thresholds.LONG_TERM_MEDIUM + 0.01;
result = this.errorMetrics.detectDrift(nrmseHigh, ltNRMSD);
this.assert(
result !== null &&
result.ImmDrift.level === 0 &&
result.LongTermDrift &&
result.LongTermDrift.level === 2,
"Detects medium long-term drift with no immediate drift"
);
nrmseHigh = config.thresholds.NRMSE_MEDIUM + 0.01;
ltNRMSD = config.thresholds.LONG_TERM_MEDIUM + 0.01;
result = this.errorMetrics.detectDrift(nrmseHigh, ltNRMSD);
this.assert(
result.ImmDrift.level === 2 &&
result.LongTermDrift.level === 2,
"Detects both medium immediate and medium long-term drift"
);
nrmseHigh = config.thresholds.NRMSE_LOW - 0.01;
ltNRMSD = config.thresholds.LONG_TERM_LOW - 0.01;
result = this.errorMetrics.detectDrift(nrmseHigh, ltNRMSD);
this.assert(
result.ImmDrift.level === 0 &&
result.LongTermDrift.level === 0,
"No significant drift detected when under thresholds"
);
}
testAssessDrift() {
console.log("\nTesting assessDrift function...");
// Reset accumulation for testing
this.errorMetrics.cumNRMSD = 0;
this.errorMetrics.cumCount = 0;
const predicted = [100, 101, 102, 103];
const measured = [90, 91, 92, 93];
const processMin = 90, processMax = 110;
let result = this.errorMetrics.assessDrift(predicted, measured, processMin, processMax);
this.assert(
result !== null &&
typeof result.nrmse === 'number' &&
typeof result.longTermNRMSD === 'number' &&
typeof result.immediateLevel === 'number' &&
typeof result.immediateFeedback === 'string' &&
typeof result.longTermLevel === 'number' &&
typeof result.longTermFeedback === 'string',
"assessDrift returns complete result structure"
);
this.assert(
result.immediateLevel > 0,
"assessDrift detects immediate drift with significant difference"
);
// Test with identical values
result = this.errorMetrics.assessDrift(predicted, predicted, processMin, processMax);
this.assert(
result.nrmse === 0 &&
result.immediateLevel === 0,
"assessDrift indicates no immediate drift when predicted equals measured"
);
// Test with slight drift
const measuredSlight = [100, 100.5, 101, 101.5];
result = this.errorMetrics.assessDrift(predicted, measuredSlight, processMin, processMax);
this.assert(
result !== null &&
result.nrmse < 0.05 &&
(result.immediateLevel < 2),
"assessDrift returns appropriate levels for slight drift"
);
// Test long-term drift accumulation
for (let i = 0; i < 100; i++) {
this.errorMetrics.assessDrift(
predicted,
measured.map(m => m + (Math.random() * 2 - 1)), // Add small random fluctuation
processMin,
processMax
);
}
result = this.errorMetrics.assessDrift(predicted, measured, processMin, processMax);
this.assert(
result.longTermNRMSD !== 0,
"Long-term drift accumulates over multiple assessments"
);
}
async runAllTests() {
console.log("\nStarting Error Metrics Tests...\n");
this.testMeanSquaredError();
this.testRootMeanSquaredError();
this.testNormalizedRMSE();
this.testNormalizeUsingRealtime();
this.testLongTermNRMSD();
this.testDetectImmediateDrift();
this.testDetectLongTermDrift();
this.testDriftDetection();
this.testAssessDrift();
console.log("\nTest Summary:");
console.log(`Total Tests: ${this.totalTests}`);
console.log(`Passed: ${this.passedTests}`);
console.log(`Failed: ${this.failedTests}`);
process.exit(this.failedTests > 0 ? 1 : 0);
}
}
// Run all tests
const tester = new ErrorMetricsTester();
tester.runAllTests().catch(console.error);

View File

@@ -0,0 +1,154 @@
//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;

View File

@@ -0,0 +1,138 @@
{
"general": {
"name": {
"default": "ErrorMetrics",
"rules": {
"type": "string",
"description": "A human-readable name for the configuration."
}
},
"id": {
"default": null,
"rules": {
"type": "string",
"nullable": true,
"description": "A unique identifier for this configuration, assigned dynamically when needed."
}
},
"unit": {
"default": "unitless",
"rules": {
"type": "string",
"description": "The unit used for the state values (e.g., 'meters', 'seconds', 'unitless')."
}
},
"logging": {
"logLevel": {
"default": "info",
"rules": {
"type": "enum",
"values": [
{
"value": "debug",
"description": "Log messages are printed for debugging purposes."
},
{
"value": "info",
"description": "Informational messages are printed."
},
{
"value": "warn",
"description": "Warning messages are printed."
},
{
"value": "error",
"description": "Error messages are printed."
}
]
}
},
"enabled": {
"default": true,
"rules": {
"type": "boolean",
"description": "Indicates whether logging is active. If true, log messages will be generated."
}
}
}
},
"functionality": {
"softwareType": {
"default": "errorMetrics",
"rules": {
"type": "string",
"description": "Logical name identifying the software type."
}
},
"role": {
"default": "error calculation",
"rules": {
"type": "string",
"description": "Functional role within the system."
}
}
},
"mode": {
"current": {
"default": "active",
"rules": {
"type": "enum",
"values": [
{
"value": "active",
"description": "The error metrics calculation is active."
},
{
"value": "inactive",
"description": "The error metrics calculation is inactive."
}
],
"description": "The operational mode of the error metrics calculation."
}
}
},
"thresholds": {
"NRMSE_LOW": {
"default": 0.05,
"rules": {
"type": "number",
"description": "Low threshold for normalized root mean squared error."
}
},
"NRMSE_MEDIUM": {
"default": 0.10,
"rules": {
"type": "number",
"description": "Medium threshold for normalized root mean squared error."
}
},
"NRMSE_HIGH": {
"default": 0.15,
"rules": {
"type": "number",
"description": "High threshold for normalized root mean squared error."
}
},
"LONG_TERM_LOW": {
"default": 0.02,
"rules": {
"type": "number",
"description": "Low threshold for long-term normalized root mean squared deviation."
}
},
"LONG_TERM_MEDIUM": {
"default": 0.04,
"rules": {
"type": "number",
"description": "Medium threshold for long-term normalized root mean squared deviation."
}
},
"LONG_TERM_HIGH": {
"default": 0.06,
"rules": {
"type": "number",
"description": "High threshold for long-term normalized root mean squared deviation."
}
}
}
}