All KPIs added

This commit is contained in:
Woutverheijen
2025-12-15 08:33:00 +01:00
parent fa5083840b
commit e457975be8
2 changed files with 186 additions and 132 deletions

View File

@@ -89,15 +89,19 @@ class Machine {
// --- KPI tracking ---
this.kpi = {
failures: 0,
totalRuntimeHours: 0,
totalDowntimeHours: 0,
lastFailureTime: null,
lastRepairTime: null,
MTBF: 0,
MTTR: 0,
availability: 0
};
failures: 0,
totalRuntimeHours: 0,
totalDowntimeHours: 0,
lastFailureTime: null,
lastRepairTime: null,
MTBF: 0,
MTTR: 0,
availability: 1,
maintenanceMode: false,
lastFailureRuntime: 0,
lastRUL: null, // remember previous RUL value
lastFailureCause: null
};
this.assetHealth = {
index: 0 // 0 = optimal, 5 = failure
@@ -116,48 +120,7 @@ class Machine {
this._handleStateChangeForKPI(stateStr);
});
// --- KPI tracking ---
this.kpi = {
failures: 0,
totalRuntimeHours: 0,
totalDowntimeHours: 0,
lastFailureTime: null,
lastRepairTime: null,
MTBF: 0,
MTTR: 0,
availability: 0
};
this.assetHealth = {
index: 0 // 0 = optimal, 5 = failure
};
this.state.emitter.on('stateChange', (payload) => {
const stateStr = typeof payload === 'string'
? payload
: (payload?.state ?? payload?.newState ?? payload);
if (typeof stateStr !== 'string') {
this.logger.warn(`stateChange event without parsable state: ${JSON.stringify(payload)}`);
return;
}
this._handleStateChangeForKPI(stateStr);
});
}
_updateState(){
const isOperational = this._isOperationalState();
if(!isOperational){
//overrule the last prediction this should be 0 now
this.measurements.type("flow").variant("predicted").position("downstream").value(0);
}
}
}
_updateState(){
const isOperational = this._isOperationalState();
@@ -298,11 +261,9 @@ _callMeasurementHandler(measurementType, value, position, context) {
case "execmovement":
return await this.setpoint(parameter);
case "entermaintenance":
case "entermaintenance":
return await this.executeSequence(parameter);
case "exitmaintenance":
return await this.executeSequence(parameter);
@@ -649,12 +610,7 @@ _callMeasurementHandler(measurementType, value, position, context) {
}
}
/////////////////////////////
/**
* Compute a single drift score in [0..1] using predicted vs measured series.
* Uses min/max of the *predicted* window as normalization range.
* If no usable data -> returns 0 (neutral).
*/
///////////////////////////// Calculate Asset Health Index /////////////////////////////
_computeDriftScore() {
try {
const metrics = [
@@ -693,32 +649,23 @@ _callMeasurementHandler(measurementType, value, position, context) {
_calculateAssetHealthIndex() {
try {
// 1) Hard fail -> worst health
// if (this.state?.getCurrentState && this.state.getCurrentState() === "failed")
if (["off"].includes(this.state?.getCurrentState?.())){
if (["maintenance"].includes(this.state?.getCurrentState?.())){
this.assetHealth.index = 5;
return 5;
}
// 2) Inputs (clamped to 0..1)
const availability = typeof this.kpi?.availability === 'number' ? this.kpi.availability : 1;
const unavailability = 1 - Math.max(0, Math.min(1, availability));
const effPenalty = Math.max(0, Math.min(1, typeof this.relDistFromPeak === 'number' ? this.relDistFromPeak : 0));
const driftScore = this._computeDriftScore(); // 0..1
// 3) Blend (weights sum to 1.0)
// Tweak these if you like: e.g. make drift more/less important.
const wAvail = 0.4; // unavailability weight
const driftScore = this._computeDriftScore();
const wAvail = 0.2; // unavailability weight
const wDrift = 0.4; // drift weight
const wEff = 0.2; // efficiency distance weight
const wEff = 0.4; // efficiency distance weight
const score01 = (wAvail * unavailability) + (wDrift * driftScore) + (wEff * effPenalty);
// 4) Scale to 0..5 integer, clamp
const index = Math.max(0, Math.min(5, Math.round(score01 * 5)));
this.assetHealth.index = index;
return index;
} catch (err) {
@@ -728,56 +675,68 @@ _callMeasurementHandler(measurementType, value, position, context) {
}
}
_handleStateChangeForKPI(newState) {
const now = Date.now();
const runtime = this.state.getRunTimeHours();
const lastState = this.state.getPreviousState?.() || "unknown";
_registerFailure(cause, runtimeOverride) {
const runtime = runtimeOverride ?? this.state.getRunTimeHours();
const maintenance_time = this.state.getMaintenanceTimeHours();
// --- Treat OFF as failure and start of downtime ---
if (newState === "off") {
this.kpi.failures++; // always count a new failure when OFF
this.kpi.lastFailureTime = now; // mark the start of downtime
this.logger.warn(`Machine OFF (counted as failure). Total failures: ${this.kpi.failures}`);
}
this.kpi.failures += 1;
this.kpi.rulFailureTriggered = false;
this.kpi.lastFailureTime = Date.now();
this.kpi.lastFailureRuntime = runtime;
this.kpi.lastFailureCause = cause;
// --- When we leave OFF and become OPERATIONAL, book downtime ---
if (newState === "operational") {
// Only calculate downtime if we had an OFF period before
if (this.kpi.lastFailureTime != null) {
const downtimeHours = (now - this.kpi.lastFailureTime) / 3600000;
this.kpi.totalDowntimeHours += downtimeHours;
this.kpi.lastRepairTime = now; // moment of "repaired"
this.kpi.lastFailureTime = null; // close downtime window
this.logger.info(`OFF → OPERATIONAL. Added ${downtimeHours.toFixed(2)}h downtime.`);
const failures = this.kpi.failures;
// If no failures yet: MTBF = total runtime & MTTR = 0
this.kpi.MTBF = failures > 0 ? runtime / failures : runtime;
this.kpi.MTTR = failures > 0 ? maintenance_time / failures : 0;
const mtbf = this.kpi.MTBF ?? 0;
const mttr = this.kpi.MTTR ?? 0;
if (mtbf <= 0 && mttr <= 0) {
this.kpi.availability = 1;
} else {
const availability = mtbf / (mtbf + mttr);
this.kpi.availability = Math.min(1, Math.max(0, availability));
}
this.logger.warn(
`Failure registered (cause=${cause}). Total failures=${this.kpi.failures}, runtime=${runtime.toFixed(2)}h`
);
}
// --- Compute KPI Metrics ---
_handleStateChangeForKPI(newState) {
const runtime = this.state.getRunTimeHours();
if (newState === "maintenance") {
if (this.kpi.rulFailureTriggered) {
this.kpi.rulFailureTriggered = false;
this._registerFailure("MAINTENANCE", runtime);
} else {
this._registerFailure("MAINTENANCE", runtime);
}
this.kpi.maintenanceMode = true;
} else {
this.kpi.maintenanceMode = false;
}
// Recalculate KPIs after possibly registering a failure:
const failures = this.kpi.failures;
const downtime = this.kpi.totalDowntimeHours;
// If no failures yet: MTBF = total runtime; MTTR = 0
const maintenance_time = this.state.getMaintenanceTimeHours();
this.kpi.MTBF = failures > 0 ? runtime / failures : runtime;
this.kpi.MTTR = failures > 0 ? downtime / failures : 0;
// --- Compute Availability ---
this.kpi.MTTR = failures > 0 ? maintenance_time / failures : 0;
const mtbf = this.kpi.MTBF ?? 0;
const mttr = this.kpi.MTTR ?? 0;
if (mtbf <= 0 && mttr <= 0) {
this.kpi.availability = 1; // Default: 100% if no data
this.kpi.availability = 1;
} else {
const availability = mtbf / (mtbf + mttr);
this.kpi.availability = Math.min(1, Math.max(0, availability)); // clamp 01
this.kpi.availability = Math.min(1, Math.max(0, availability));
}
this.logger.debug(
`KPI updated — MTBF: ${this.kpi.MTBF.toFixed(2)}h, MTTR: ${this.kpi.MTTR.toFixed(2)}h, ` +
`Availability: ${(this.kpi.availability * 100).toFixed(2)}%`
);
}
//////////////////////////////////////////////
}
calcDistanceFromPeak(currentEfficiency,peakEfficiency){
return Math.abs(currentEfficiency - peakEfficiency);
@@ -871,6 +830,7 @@ _handleStateChangeForKPI(newState) {
if (power != 0 && flow != 0) {
// Calculate efficiency after measurements update
this.measurements.type("efficiency").variant(variant).position('atEquipment').value((flow / power));
} else {
this.measurements.type("efficiency").variant(variant).position('atEquipment').value(null);
}
@@ -879,6 +839,65 @@ _handleStateChangeForKPI(newState) {
}
calculateSimpleRUL() {
const mtbf = this.kpi?.MTBF ?? 0;
const healthIndex = typeof this.assetHealth?.index === "number"
? this.assetHealth.index
: 0;
if (this.kpi?.rulFailureTriggered) {
this.kpi.lastRUL = 0;
return 0;
}
if (mtbf <= 0) {
this.logger.debug("No MTBF available, RUL cannot be estimated.");
this.kpi.lastRUL = 0;
return 0;
}
const clampedHI = Math.min(5, Math.max(0, healthIndex));
const runtime = this.state?.getRunTimeHours?.() ?? 0;
const lastFailureRuntime = this.kpi?.lastFailureRuntime ?? 0;
const ageSinceLastFailure = Math.max(0, runtime - lastFailureRuntime);
const fractionByHealth = (5 - clampedHI) / 5;
const fractionByAge = Math.max(0, 1 - ageSinceLastFailure / mtbf);
const fractionLeft = Math.min(fractionByHealth, fractionByAge);
let rul = mtbf * fractionLeft;
if (rul < 0) rul = 0;
const prevRUL = this.kpi.lastRUL ?? null;
const EPS = 1e-3;
const state = this.state?.getCurrentState?.();
const isOperational = ["operational", "accelerating", "decelerating"].includes(state);
if (isOperational) {
const hadPrev = prevRUL !== null;
const wasPositive = hadPrev && prevRUL > EPS;
const isNowZeroish = rul <= EPS;
const isDecreasing = hadPrev && rul <= prevRUL + EPS;
if (wasPositive && isNowZeroish && isDecreasing) {
this.kpi.rulFailureTriggered = true;
this.kpi.lastFailureTime = Date.now();
this.kpi.lastFailureRuntime = runtime;
this.kpi.lastFailureCause = "RUL";
rul = 0;
}
}
this.kpi.lastRUL = rul;
return rul;
}
updateCurve(newCurve) {
this.logger.info(`Updating machine curve`);
const newConfig = { asset: { machineCurve: newCurve } };
@@ -940,14 +959,20 @@ _handleStateChangeForKPI(newState) {
output["cog"] = this.cog; // flow / power efficiency
output["NCog"] = this.NCog; // normalized cog
output["NCogPercent"] = Math.round(this.NCog * 100 * 100) / 100 ;
// KPIs
output["kpi_MTBF"] = this.kpi.MTBF;
output["kpi_MTTR"] = this.kpi.MTTR;
output["kpi_assetAvailability"] = Math.round(this.kpi.availability * 100 * 100) / 100;
output["kpi_totalFailuresCount"] = this.kpi.failures;
output["asset_tag_number"] = 'L001';
// output["asset_tag_number"] = this.assetTagNumber;
output["asset_tag_number"] = 'L001'; // hardcoded for now
output["maintenanceMode"] = this.kpi.maintenanceMode;
output["maintenanceTime"] = this.state.getMaintenanceTimeHours();
output["rul_hours"] = this.calculateSimpleRUL();
this._calculateAssetHealthIndex();
output["assetHealthIndex"] = this.assetHealth.index;
const healthColors = ["#006400", "#008000", "#FFFF00", "#FFA500", "#FF0000", "#8B0000"]; // 0 = darkgreen, 1 = green, 2 = yellow, 3 = orange, 4 = red, 5 = darkred
output["assetHealthColor"] = healthColors[this.assetHealth.index] || "unknown";
if(this.flowDrift != null){
const flowDrift = this.flowDrift;
@@ -962,21 +987,6 @@ _handleStateChangeForKPI(newState) {
output["effRelDistFromPeak"] = this.relDistFromPeak;
//this.logger.debug(`Output: ${JSON.stringify(output)}`);
/////////////////////////////////
// this._calculateAssetHealthIndex();
// output["assetHealthIndex"] = this.assetHealth.index;
this._calculateAssetHealthIndex();
output["assetHealthIndex"] = this.assetHealth.index;
// 0 = darkgreen, 1 = green, 2 = yellow, 3 = orange, 4 = red, 5 = darkred
// const healthColors = ["darkgreen", "green", "yellow", "orange", "red", "darkred"];
const healthColors = ["#006400", "#008000", "#FFFF00", "#FFA500", "#FF0000", "#8B0000"];
output["assetHealthColor"] = healthColors[this.assetHealth.index] || "unknown";
//////////////////////////
return output;
}