Compare commits

9 Commits

Author SHA1 Message Date
Woutverheijen
f450eab56e Merge branch 'main' of https://gitea.centraal.wbd-rd.nl/woutverheijen/rotatingMachine 2025-11-06 08:51:21 +01:00
Woutverheijen
9e7c951aa8 updates 2025-11-06 08:51:19 +01:00
818c989a86 merge upstream 2025-11-06 07:03:29 +00:00
Woutverheijen
59dec46846 test 2025-10-24 09:40:40 +02:00
Woutverheijen
133cadf225 remove test 2025-10-23 12:23:45 +02:00
Woutverheijen
60d7dcb1ac test 2025-10-23 12:20:46 +02:00
5a14f44fdd Merge pull request 'dev-Rene' (#2) from dev-Rene into main
Reviewed-on: #2
2025-10-16 13:21:38 +00:00
c081acae4e Remove non-implemented temperature handling function 2025-10-10 13:27:31 +02:00
08185243bc Merge pull request 'dev-Rene' (#1) from dev-Rene into main
Reviewed-on: #1
2025-10-06 14:16:18 +00:00
3 changed files with 219 additions and 22 deletions

View File

@@ -1 +1,23 @@
# rotating machine
# rotating machine
---Explanation KPIs---
Mean Time Between Failures: MTBF is a key indicator of asset reliability. It represents the average amount of time between two consecutive failures during normal operation. The higher the value, the better.
- Formula: MTBF = runtime / failures
Mean Time to Repair: MTTR focuses on the speed of failure recovery. It indicates how long it takes on average to repair a failure and restore the system to operational condition. The lower the value, the better.
- Formula: MTTR = downtime / failures
Asset Availability: Asset availability indicates how often machines are available for use. Higher availability means that the equipment experiences fewer breakdowns and remains operational for a greater amount of time.
- Formula: availability = MTBF / (MTBF + MTTR) * 100%
Asset Health Index: A score ranging from 0 (optimal condition) to 5 (worst case) that reflects the overall health status of an asset.
---KPI message---
The message consists of the following components:
- asset tagnumber:
- asset availability
- mean time between failures
- mean time to repair
- asset health index
- asset health color: Gives a color based on the asset health index (0 = Darkgreen, 1 = Green, 2 = Yellow, 3 = Orange, 4 = Red, 5 = Darkred.)
- total failures: the total number of failures that have occured for a particular asset

View File

@@ -115,7 +115,7 @@ class nodeClass {
const mode = m.currentMode;
const state = m.state.getCurrentState();
const flow = Math.round(m.measurements.type("flow").variant("predicted").position('downstream').getCurrentValue());
const power = Math.round(m.measurements.type("power").variant("predicted").position('upstream').getCurrentValue());
const power = Math.round(m.measurements.type("power").variant("predicted").position('atEquipment').getCurrentValue());
let symbolState;
switch(state){
case "off":

View File

@@ -71,6 +71,37 @@ class Machine {
this.child = {}; // object to hold child information so we know on what to subscribe
this.childRegistrationUtils = new childRegistrationUtils(this); // Child registration utility
// --- 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);
});
}
/*------------------- Register child events -------------------*/
@@ -116,10 +147,6 @@ _callMeasurementHandler(measurementType, value, position, context) {
this.updateMeasuredFlow(value, position, context);
break;
case 'temperature':
this.updateMeasuredTemperature(value, position, context);
break;
default:
this.logger.warn(`No handler for measurement type: ${measurementType}`);
// Generic handler - just update position
@@ -532,6 +559,136 @@ _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).
*/
_computeDriftScore() {
try {
const metrics = [
{ key: "pressure", pos: "downstream" },
{ key: "flow", pos: "downstream" },
{ key: "power", pos: "atEquipment" }
];
const values = [];
for (const m of metrics) {
const pred = this.measurements.type(m.key).variant("predicted").position(m.pos).getAllValues()?.values;
const meas = this.measurements.type(m.key).variant("measured").position(m.pos).getAllValues()?.values;
if (!Array.isArray(pred) || !Array.isArray(meas) || pred.length < 2 || meas.length < 2) continue;
const expectedMin = Math.min(...pred);
const expectedMax = Math.max(...pred);
if (!Number.isFinite(expectedMin) || !Number.isFinite(expectedMax) || expectedMax === expectedMin) continue;
const drift = this.errorMetrics.assessDrift(pred, meas, expectedMin, expectedMax);
if (Number.isFinite(drift)) {
// assessDrift is already normalized; keep it in [0..1]
values.push(Math.max(0, Math.min(1, Math.abs(drift))));
}
}
if (values.length === 0) return 0; // neutral if no data
const avg = values.reduce((s, v) => s + v, 0) / values.length;
return Math.max(0, Math.min(1, avg));
} catch (e) {
this.logger?.warn?.(`Drift score error: ${e.message}`);
return 0;
}
}
_calculateAssetHealthIndex() {
try {
// 1) Hard fail -> worst health
// if (this.state?.getCurrentState && this.state.getCurrentState() === "failed")
if (["off"].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 wDrift = 0.4; // drift weight
const wEff = 0.2; // 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) {
this.logger?.error?.(`AHI calc error: ${err.message}`);
this.assetHealth.index = 0;
return 0;
}
}
_handleStateChangeForKPI(newState) {
const now = Date.now();
const runtime = this.state.getRunTimeHours();
const lastState = this.state.getPreviousState?.() || "unknown";
// --- 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}`);
}
// --- 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.`);
}
}
// --- Compute KPI Metrics ---
const failures = this.kpi.failures;
const downtime = this.kpi.totalDowntimeHours;
// If no failures yet: MTBF = total runtime; MTTR = 0
this.kpi.MTBF = failures > 0 ? runtime / failures : runtime;
this.kpi.MTTR = failures > 0 ? downtime / failures : 0;
// --- Compute Availability ---
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
} else {
const availability = mtbf / (mtbf + mttr);
this.kpi.availability = Math.min(1, Math.max(0, availability)); // clamp 01
}
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);
@@ -706,6 +863,13 @@ _callMeasurementHandler(measurementType, value, position, context) {
output["cog"] = this.cog; // flow / power efficiency
output["NCog"] = this.NCog; // normalized cog
output["NCogPercent"] = Math.round(this.NCog * 100 * 100) / 100 ;
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;
if(this.flowDrift != null){
const flowDrift = this.flowDrift;
@@ -720,6 +884,21 @@ _callMeasurementHandler(measurementType, value, position, context) {
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;
}
@@ -729,9 +908,9 @@ _callMeasurementHandler(measurementType, value, position, context) {
module.exports = Machine;
/*------------------- Testing -------------------*/
/*
curve = require('C:/Users/zn375/.node-red/public/fallbackData.json');
/*
//curve = require('C:/Users/zn375/.node-red/public/fallbackData.json');
//import a child
const Child = require('../../measurement/src/specificClass');
@@ -747,7 +926,6 @@ const PT1 = new Child(config={
},
functionality:{
softwareType:"measurement",
positionVsParent:"upstream",
},
asset:{
supplier:"Vega",
@@ -769,7 +947,6 @@ const PT2 = new Child(config={
},
functionality:{
softwareType:"measurement",
positionVsParent:"upstream",
},
asset:{
supplier:"Vega",
@@ -785,18 +962,17 @@ console.log(`Creating machine...`);
const machineConfig = {
general: {
name: "Hydrostal",
name: "Hidrostal",
logging: {
enabled: true,
logLevel: "debug",
}
},
asset: {
supplier: "Hydrostal",
supplier: "Hidrostal",
type: "pump",
category: "centrifugal",
model: "H05K-S03R+HGM1X-X280KO", // Ensure this field is present.
machineCurve: curve["machineCurves"]["Hydrostal"]["H05K-S03R+HGM1X-X280KO"],
model: "hidrostal-H05K-S03R", // Ensure this field is present.
}
}
@@ -825,18 +1001,17 @@ const machine = new Machine(machineConfig, stateConfig);
machine.logger.info(`Registering child...`);
machine.childRegistrationUtils.registerChild(PT1, "upstream");
machine.childRegistrationUtils.registerChild(PT2, "downstream");
//feed curve to the machine class
//machine.updateCurve(curve["machineCurves"]["Hydrostal"]["H05K-S03R+HGM1X-X280KO"]);
/*
PT1.logger.info(`Enable sim...`);
PT1.toggleSimulation();
PT2.logger.info(`Enable sim...`);
PT2.toggleSimulation();
machine.getOutput();
*/
//manual test
//machine.handleInput("parent", "execSequence", "startup");
/*
machine.measurements.type("pressure").variant("measured").position('upstream').value(-200);
machine.measurements.type("pressure").variant("measured").position('downstream').value(1000);
@@ -846,8 +1021,8 @@ const tickLoop = setInterval(changeInput,1000);
function changeInput(){
PT1.logger.info(`tick...`);
PT1.tick();
PT2.tick();
//PT1.tick();
//PT2.tick();
}
async function testingSequences(){