forked from RnD/rotatingMachine
Compare commits
6 Commits
59dec46846
...
f450eab56e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f450eab56e | ||
|
|
9e7c951aa8 | ||
| 818c989a86 | |||
| 5a14f44fdd | |||
|
|
d2a0274eb3 | ||
|
|
2073207df1 |
24
README.md
24
README.md
@@ -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
|
||||
@@ -1,3 +1,13 @@
|
||||
<!--
|
||||
| S88-niveau | Primair (blokkleur) | Tekstkleur |
|
||||
| ---------------------- | ------------------- | ---------- |
|
||||
| **Area** | `#0f52a5` | wit |
|
||||
| **Process Cell** | `#0c99d9` | wit |
|
||||
| **Unit** | `#50a8d9` | zwart |
|
||||
| **Equipment (Module)** | `#86bbdd` | zwart |
|
||||
| **Control Module** | `#a9daee` | zwart |
|
||||
|
||||
-->
|
||||
<!-- Load the dynamic menu & config endpoints -->
|
||||
<script src="/rotatingMachine/menu.js"></script>
|
||||
<script src="/rotatingMachine/configData.js"></script>
|
||||
@@ -5,7 +15,7 @@
|
||||
<script>
|
||||
RED.nodes.registerType("rotatingMachine", {
|
||||
category: "EVOLV",
|
||||
color: "#4f8582",
|
||||
color: "#86bbdd",
|
||||
defaults: {
|
||||
|
||||
// Define specific properties
|
||||
@@ -41,7 +51,7 @@
|
||||
outputs: 3,
|
||||
inputLabels: ["Input"],
|
||||
outputLabels: ["process", "dbase", "parent"],
|
||||
icon: "font-awesome/fa-tachometer",
|
||||
icon: "font-awesome/fa-cog",
|
||||
|
||||
label: function () {
|
||||
return this.positionIcon + " " + this.category.slice(0, -1) || "Machine";
|
||||
|
||||
@@ -1,48 +1,3 @@
|
||||
/**
|
||||
* @file machine.js
|
||||
*
|
||||
* Permission is hereby granted to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to use it for personal
|
||||
* or non-commercial purposes, with the following restrictions:
|
||||
*
|
||||
* 1. **No Copying or Redistribution**: The Software or any of its parts may not
|
||||
* be copied, merged, distributed, sublicensed, or sold without explicit
|
||||
* prior written permission from the author.
|
||||
*
|
||||
* 2. **Commercial Use**: Any use of the Software for commercial purposes requires
|
||||
* a valid license, obtainable only with the explicit consent of the author.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
* Ownership of this code remainregisterChilds solely with the original author. Unauthorized
|
||||
* use of this Software is strictly prohibited.
|
||||
*
|
||||
* @summary A class to interact and manipulate machines with a non-euclidian curve
|
||||
* @description A class to interact and manipulate machines with a non-euclidian curve
|
||||
* @module machine
|
||||
* @exports machine
|
||||
* @version 0.1.0
|
||||
* @since 0.1.0
|
||||
*
|
||||
* Author:
|
||||
* - Rene De Ren
|
||||
* Email:
|
||||
* - r.de.ren@brabantsedelta.nl
|
||||
*
|
||||
* Add functionality later
|
||||
// -------- Operational Metrics -------- //
|
||||
maintenanceAlert: this.state.checkMaintenanceStatus()
|
||||
|
||||
|
||||
*/
|
||||
|
||||
//load local dependencies
|
||||
const EventEmitter = require('events');
|
||||
const {loadCurve,logger,configUtils,configManager,state, nrmse, MeasurementContainer, predict, interpolation , childRegistrationUtils} = require('generalFunctions');
|
||||
const { name } = require('../../generalFunctions/src/convert/lodash/lodash._shimkeys');
|
||||
@@ -116,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 -------------------*/
|
||||
@@ -513,13 +499,12 @@ _callMeasurementHandler(measurementType, value, position, context) {
|
||||
}
|
||||
}
|
||||
|
||||
// rich context handler for pressure updates
|
||||
// context handler for pressure updates
|
||||
updateMeasuredPressure(value, position, context = {}) {
|
||||
|
||||
// Enhanced logging with child context
|
||||
this.logger.debug(`Pressure update: ${value} at ${position} from ${context.childName || 'child'} (${context.childId || 'unknown-id'})`);
|
||||
|
||||
// Store in parent's measurement container (your existing logic)
|
||||
// Store in parent's measurement container
|
||||
this.measurements.type("pressure").variant("measured").position(position).value(value, context.timestamp, context.unit);
|
||||
|
||||
// Determine what kind of value to use as pressure (upstream , downstream or difference)
|
||||
@@ -574,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 0–1
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -748,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;
|
||||
@@ -762,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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user