generic updates completed for now
This commit is contained in:
814
dependencies/machine/machine.js
vendored
814
dependencies/machine/machine.js
vendored
@@ -1,814 +0,0 @@
|
|||||||
/**
|
|
||||||
* @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 remains 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:
|
|
||||||
* - rene@thegoldenbasket.nl
|
|
||||||
*
|
|
||||||
* Add functionality later
|
|
||||||
// -------- Operational Metrics -------- //
|
|
||||||
maintenanceAlert: this.state.checkMaintenanceStatus()
|
|
||||||
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
//load local dependencies
|
|
||||||
const EventEmitter = require('events');
|
|
||||||
const Logger = require('../../../generalFunctions/helper/logger');
|
|
||||||
const State = require('../../../generalFunctions/helper/state/state');
|
|
||||||
const Predict = require('../../../predict/dependencies/predict/predict_class');
|
|
||||||
const { MeasurementContainer } = require('../../../generalFunctions/helper/measurements/index');
|
|
||||||
const Interpolation = require('../../../predict/dependencies/predict/interpolation');
|
|
||||||
|
|
||||||
//load all config modules
|
|
||||||
const defaultConfig = require('../rotatingMachine/rotatingMachineConfig.json');
|
|
||||||
const ConfigUtils = require('../../../generalFunctions/helper/configUtils');
|
|
||||||
|
|
||||||
//load registration utility
|
|
||||||
const ChildRegistrationUtils = require('../../../generalFunctions/helper/childRegistrationUtils');
|
|
||||||
const ErrorMetrics = require('../../../generalFunctions/helper/nrmse/errorMetrics');
|
|
||||||
|
|
||||||
class Machine {
|
|
||||||
|
|
||||||
/*------------------- Construct and set vars -------------------*/
|
|
||||||
constructor(machineConfig = {}, stateConfig = {}, errorMetricsConfig = {}) {
|
|
||||||
|
|
||||||
//basic setup
|
|
||||||
this.emitter = new EventEmitter(); // Own EventEmitter
|
|
||||||
this.configUtils = new ConfigUtils(defaultConfig);
|
|
||||||
this.config = this.configUtils.initConfig(machineConfig);
|
|
||||||
|
|
||||||
// Initialize measurements
|
|
||||||
this.measurements = new MeasurementContainer();
|
|
||||||
this.interpolation = new Interpolation();
|
|
||||||
this.child = {}; // object to hold child information so we know on what to subscribe
|
|
||||||
|
|
||||||
this.flowDrift = null;
|
|
||||||
|
|
||||||
// Init after config is set
|
|
||||||
this.logger = new Logger(this.config.general.logging.enabled, this.config.general.logging.logLevel, this.config.general.name);
|
|
||||||
this.state = new State(stateConfig, this.logger); // Init State manager and pass logger
|
|
||||||
this.errorMetrics = new ErrorMetrics(errorMetricsConfig, this.logger);
|
|
||||||
|
|
||||||
this.predictFlow = new Predict({ curve: this.config.asset.machineCurve.nq }); // load nq (x : ctrl , y : flow relationship)
|
|
||||||
this.predictPower = new Predict({ curve: this.config.asset.machineCurve.np }); // load np (x : ctrl , y : power relationship)
|
|
||||||
this.predictCtrl = new Predict({ curve: this.reverseCurve(this.config.asset.machineCurve.nq) }); // load reversed nq (x: flow, y: ctrl relationship)
|
|
||||||
|
|
||||||
this.currentMode = this.config.mode.current;
|
|
||||||
this.currentEfficiencyCurve = {};
|
|
||||||
this.cog = 0;
|
|
||||||
this.NCog = 0;
|
|
||||||
this.cogIndex = 0;
|
|
||||||
this.minEfficiency = 0;
|
|
||||||
this.absDistFromPeak = 0;
|
|
||||||
this.relDistFromPeak = 0;
|
|
||||||
|
|
||||||
this.state.emitter.on("positionChange", (data) => {
|
|
||||||
this.logger.debug(`Position change detected: ${data}`);
|
|
||||||
this.updatePosition();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//this.calcCog();
|
|
||||||
|
|
||||||
|
|
||||||
this.childRegistrationUtils = new ChildRegistrationUtils(this); // Child registration utility
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method to assess drift using errorMetrics
|
|
||||||
assessDrift(measurement, processMin, processMax) {
|
|
||||||
this.logger.debug(`Assessing drift for measurement: ${measurement} processMin: ${processMin} processMax: ${processMax}`);
|
|
||||||
const predictedMeasurement = this.measurements.type(measurement).variant("predicted").position("downstream").getAllValues().values;
|
|
||||||
const measuredMeasurement = this.measurements.type(measurement).variant("measured").position("downstream").getAllValues().values;
|
|
||||||
|
|
||||||
if (!predictedMeasurement || !measuredMeasurement) return null;
|
|
||||||
|
|
||||||
return this.errorMetrics.assessDrift(
|
|
||||||
predictedMeasurement,
|
|
||||||
measuredMeasurement,
|
|
||||||
processMin,
|
|
||||||
processMax
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
reverseCurve(curve) {
|
|
||||||
const reversedCurve = {};
|
|
||||||
for (const [pressure, values] of Object.entries(curve)) {
|
|
||||||
reversedCurve[pressure] = {
|
|
||||||
x: [...values.y], // Previous y becomes new x
|
|
||||||
y: [...values.x] // Previous x becomes new y
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return reversedCurve;
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------- Config -------- //
|
|
||||||
updateConfig(newConfig) {
|
|
||||||
this.config = this.configUtils.updateConfig(this.config, newConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------- Mode and Input Management -------- //
|
|
||||||
isValidSourceForMode(source, mode) {
|
|
||||||
const allowedSourcesSet = this.config.mode.allowedSources[mode] || [];
|
|
||||||
return allowedSourcesSet.has(source);
|
|
||||||
}
|
|
||||||
|
|
||||||
isValidActionForMode(action, mode) {
|
|
||||||
const allowedActionsSet = this.config.mode.allowedActions[mode] || [];
|
|
||||||
return allowedActionsSet.has(action);
|
|
||||||
}
|
|
||||||
|
|
||||||
async handleInput(source, action, parameter) {
|
|
||||||
if (!this.isValidSourceForMode(source, this.currentMode)) {
|
|
||||||
let warningTxt = `Source '${source}' is not valid for mode '${this.currentMode}'.`;
|
|
||||||
this.logger.warn(warningTxt);
|
|
||||||
return {status : false , feedback: warningTxt};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
this.logger.info(`Handling input from source '${source}' with action '${action}' in mode '${this.currentMode}'.`);
|
|
||||||
try {
|
|
||||||
switch (action) {
|
|
||||||
case "execSequence":
|
|
||||||
await this.executeSequence(parameter);
|
|
||||||
//recalc flow and power
|
|
||||||
this.updatePosition();
|
|
||||||
break;
|
|
||||||
case "execMovement":
|
|
||||||
await this.setpoint(parameter);
|
|
||||||
break;
|
|
||||||
case "flowMovement":
|
|
||||||
// Calculate the control value for a desired flow
|
|
||||||
const pos = this.calcCtrl(parameter);
|
|
||||||
// Move to the desired setpoint
|
|
||||||
await this.setpoint(pos);
|
|
||||||
break;
|
|
||||||
case "emergencyStop":
|
|
||||||
this.logger.warn(`Emergency stop activated by '${source}'.`);
|
|
||||||
await this.executeSequence("emergencyStop");
|
|
||||||
break;
|
|
||||||
case "statusCheck":
|
|
||||||
this.logger.info(`Status Check: Mode = '${this.currentMode}', Source = '${source}'.`);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
this.logger.warn(`Action '${action}' is not implemented.`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
this.logger.debug(`Action '${action}' successfully executed`);
|
|
||||||
return {status : true , feedback: `Action '${action}' successfully executed.`};
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`Error handling input: ${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setMode(newMode) {
|
|
||||||
const availableModes = defaultConfig.mode.current.rules.values.map(v => v.value);
|
|
||||||
if (!availableModes.includes(newMode)) {
|
|
||||||
this.logger.warn(`Invalid mode '${newMode}'. Allowed modes are: ${availableModes.join(', ')}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.currentMode = newMode;
|
|
||||||
this.logger.info(`Mode successfully changed to '${newMode}'.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------- Sequence Handlers -------- //
|
|
||||||
async executeSequence(sequenceName) {
|
|
||||||
|
|
||||||
const sequence = this.config.sequences[sequenceName];
|
|
||||||
|
|
||||||
if (!sequence || sequence.size === 0) {
|
|
||||||
this.logger.warn(`Sequence '${sequenceName}' not defined.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.state.getCurrentState() == "operational" && sequenceName == "shutdown") {
|
|
||||||
this.logger.info(`Machine will ramp down to position 0 before performing ${sequenceName} sequence`);
|
|
||||||
await this.setpoint(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.info(` --------- Executing sequence: ${sequenceName} -------------`);
|
|
||||||
|
|
||||||
for (const state of sequence) {
|
|
||||||
try {
|
|
||||||
await this.state.transitionToState(state);
|
|
||||||
// Update measurements after state change
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`Error during sequence '${sequenceName}': ${error}`);
|
|
||||||
break; // Exit sequence execution on error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async setpoint(setpoint) {
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Validate setpoint
|
|
||||||
if (typeof setpoint !== 'number' || setpoint < 0) {
|
|
||||||
throw new Error("Invalid setpoint: Setpoint must be a non-negative number.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move to the desired setpoint
|
|
||||||
await this.state.moveTo(setpoint);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error setting setpoint: ${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate flow based on current pressure and position
|
|
||||||
calcFlow(x) {
|
|
||||||
const state = this.state.getCurrentState();
|
|
||||||
|
|
||||||
if (!["operational", "accelerating", "decelerating"].includes(state)) {
|
|
||||||
this.measurements.type("flow").variant("predicted").position("downstream").value(0);
|
|
||||||
this.logger.debug(`Machine is not operational. Setting predicted flow to 0.`);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
//this.predictFlow.currentX = x; Decrepated
|
|
||||||
const cFlow = this.predictFlow.y(x);
|
|
||||||
this.measurements.type("flow").variant("predicted").position("downstream").value(cFlow);
|
|
||||||
//this.logger.debug(`Calculated flow: ${cFlow} for pressure: ${this.getMeasuredPressure()} and position: ${x}`);
|
|
||||||
return cFlow;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate power based on current pressure and position
|
|
||||||
calcPower(x) {
|
|
||||||
const state = this.state.getCurrentState();
|
|
||||||
if (!["operational", "accelerating", "decelerating"].includes(state)) {
|
|
||||||
this.measurements.type("power").variant("predicted").position('upstream').value(0);
|
|
||||||
this.logger.debug(`Machine is not operational. Setting predicted power to 0.`);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
//this.predictPower.currentX = x; Decrepated
|
|
||||||
const cPower = this.predictPower.y(x);
|
|
||||||
this.measurements.type("power").variant("predicted").position('upstream').value(cPower);
|
|
||||||
//this.logger.debug(`Calculated power: ${cPower} for pressure: ${this.getMeasuredPressure()} and position: ${x}`);
|
|
||||||
return cPower;
|
|
||||||
}
|
|
||||||
|
|
||||||
// calculate the power consumption using only flow and pressure
|
|
||||||
inputFlowCalcPower(flow) {
|
|
||||||
this.predictCtrl.currentX = flow;
|
|
||||||
const cCtrl = this.predictCtrl.y(flow);
|
|
||||||
this.predictPower.currentX = cCtrl;
|
|
||||||
const cPower = this.predictPower.y(cCtrl);
|
|
||||||
return cPower;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to predict control value for a desired flow
|
|
||||||
calcCtrl(x) {
|
|
||||||
|
|
||||||
this.predictCtrl.currentX = x;
|
|
||||||
const cCtrl = this.predictCtrl.y(x);
|
|
||||||
this.measurements.type("ctrl").variant("predicted").position('upstream').value(cCtrl);
|
|
||||||
//this.logger.debug(`Calculated ctrl: ${cCtrl} for pressure: ${this.getMeasuredPressure()} and position: ${x}`);
|
|
||||||
return cCtrl;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// this function returns the pressure for calculations
|
|
||||||
getMeasuredPressure() {
|
|
||||||
const pressureDiff = this.measurements.type('pressure').variant('measured').difference();
|
|
||||||
|
|
||||||
// Both upstream & downstream => differential
|
|
||||||
if (pressureDiff != null) {
|
|
||||||
this.logger.debug(`Pressure differential: ${pressureDiff.value}`);
|
|
||||||
this.predictFlow.fDimension = pressureDiff.value;
|
|
||||||
this.predictPower.fDimension = pressureDiff.value;
|
|
||||||
this.predictCtrl.fDimension = pressureDiff.value;
|
|
||||||
//update the cog
|
|
||||||
const { cog, minEfficiency } = this.calcCog();
|
|
||||||
// calc efficiency
|
|
||||||
const efficiency = this.calcEfficiency(this.predictPower.outputY, this.predictFlow.outputY, "predicted");
|
|
||||||
//update the distance from peak
|
|
||||||
this.calcDistanceBEP(efficiency,cog,minEfficiency);
|
|
||||||
|
|
||||||
return pressureDiff.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get downstream
|
|
||||||
const downstreamPressure = this.measurements.type('pressure').variant('measured').position('downstream').getCurrentValue();
|
|
||||||
|
|
||||||
// Only downstream => use it, warn that it's partial
|
|
||||||
if (downstreamPressure != null) {
|
|
||||||
this.logger.warn(`Using downstream pressure only for prediction: ${downstreamPressure} `);
|
|
||||||
this.predictFlow.fDimension = downstreamPressure;
|
|
||||||
this.predictPower.fDimension = downstreamPressure;
|
|
||||||
this.predictCtrl.fDimension = downstreamPressure;
|
|
||||||
//update the cog
|
|
||||||
const { cog, minEfficiency } = this.calcCog();
|
|
||||||
// calc efficiency
|
|
||||||
const efficiency = this.calcEfficiency(this.predictPower.outputY, this.predictFlow.outputY, "predicted");
|
|
||||||
//update the distance from peak
|
|
||||||
this.calcDistanceBEP(efficiency,cog,minEfficiency);
|
|
||||||
return downstreamPressure;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.error(`No valid pressure measurements available to calculate prediction using last known pressure`);
|
|
||||||
|
|
||||||
//set default at 0 => lowest pressure possible
|
|
||||||
this.predictFlow.fDimension = 0;
|
|
||||||
this.predictPower.fDimension = 0;
|
|
||||||
this.predictCtrl.fDimension = 0;
|
|
||||||
//update the cog
|
|
||||||
const { cog, minEfficiency } = this.calcCog();
|
|
||||||
// calc efficiency
|
|
||||||
const efficiency = this.calcEfficiency(this.predictPower.outputY, this.predictFlow.outputY, "predicted");
|
|
||||||
//update the distance from peak
|
|
||||||
this.calcDistanceBEP(efficiency,cog,minEfficiency);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleMeasuredFlow() {
|
|
||||||
const flowDiff = this.measurements.type('flow').variant('measured').difference();
|
|
||||||
|
|
||||||
// If both are present
|
|
||||||
if (flowDiff != null) {
|
|
||||||
// In theory, mass flow in = mass flow out, so they should match or be close.
|
|
||||||
if (flowDiff.value < 0.001) {
|
|
||||||
// flows match within tolerance
|
|
||||||
this.logger.debug(`Flow match: ${flowDiff.value}`);
|
|
||||||
return flowDiff.value;
|
|
||||||
} else {
|
|
||||||
// Mismatch => decide how to handle. Maybe take the average?
|
|
||||||
// Or bail out with an error. Example: we bail out here.
|
|
||||||
this.logger.error(`Something wrong with down or upstream flow measurement. Bailing out!`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// get
|
|
||||||
const upstreamFlow = this.measurements.type('pressure').variant('measured').position('upstream').getCurrentValue();
|
|
||||||
|
|
||||||
// Only upstream => might still accept it, but warn
|
|
||||||
if (upstreamFlow != null) {
|
|
||||||
this.logger.warn(`Only upstream flow is present. Using it but results may be incomplete!`);
|
|
||||||
return upstreamFlow;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get
|
|
||||||
const downstreamFlow = this.measurements.type('pressure').variant('measured').position('downstream').getCurrentValue();
|
|
||||||
|
|
||||||
// Only downstream => might still accept it, but warn
|
|
||||||
if (downstreamFlow != null) {
|
|
||||||
this.logger.warn(`Only downstream flow is present. Using it but results may be incomplete!`);
|
|
||||||
return downstreamFlow;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Neither => error
|
|
||||||
this.logger.error(`No upstream or downstream flow measurement. Bailing out!`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleMeasuredPower() {
|
|
||||||
const power = this.measurements.type("power").variant("measured").position("upstream").getCurrentValue();
|
|
||||||
// If your system calls it "upstream" or just a single "value", adjust accordingly
|
|
||||||
|
|
||||||
if (power != null) {
|
|
||||||
this.logger.debug(`Measured power: ${power}`);
|
|
||||||
return power;
|
|
||||||
} else {
|
|
||||||
this.logger.error(`No measured power found. Bailing out!`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePressure(variant,value,position) {
|
|
||||||
|
|
||||||
switch (variant) {
|
|
||||||
case ("measured"):
|
|
||||||
//only update when machine is in a state where it can be used
|
|
||||||
if (this.state.getCurrentState() == "operational" || this.state.getCurrentState() == "accelerating" || this.state.getCurrentState() == "decelerating") {
|
|
||||||
// put value in measurements
|
|
||||||
this.measurements.type("pressure").variant("measured").position(position).value(value);
|
|
||||||
//when measured pressure gets updated we need some logic to fetch the relevant value which could be downstream or differential pressure
|
|
||||||
const pressure = this.getMeasuredPressure();
|
|
||||||
//update the flow power and cog
|
|
||||||
this.updatePosition();
|
|
||||||
this.logger.debug(`Measured pressure: ${pressure}`);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
this.logger.warn(`Unrecognized variant '${variant}' for pressure update.`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateFlow(variant,value,position) {
|
|
||||||
|
|
||||||
switch (variant) {
|
|
||||||
case ("measured"):
|
|
||||||
// put value in measurements
|
|
||||||
this.measurements.type("flow").variant("measured").position(position).value(value);
|
|
||||||
//when measured flow gets updated we need to push the last known value in the prediction measurements to keep them synced
|
|
||||||
this.measurements.type("flow").variant("predicted").position("downstream").value(this.predictFlow.outputY);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ("predicted"):
|
|
||||||
this.logger.debug('not doing anythin yet');
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
this.logger.warn(`Unrecognized variant '${variant}' for flow update.`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateMeasurement(variant, subType, value, position) {
|
|
||||||
this.logger.debug(`---------------------- updating ${subType} ------------------ `);
|
|
||||||
switch (subType) {
|
|
||||||
case "pressure":
|
|
||||||
// Update pressure measurement
|
|
||||||
this.updatePressure(variant,value,position);
|
|
||||||
break;
|
|
||||||
case "flow":
|
|
||||||
this.updateFlow(variant,value,position);
|
|
||||||
// Update flow measurement
|
|
||||||
this.flowDrift = this.assessDrift("flow", this.predictFlow.currentFxyYMin , this.predictFlow.currentFxyYMax);
|
|
||||||
this.logger.debug(`---------------------------------------- `);
|
|
||||||
break;
|
|
||||||
case "power":
|
|
||||||
// Update power measurement
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
this.logger.error(`Type '${type}' not recognized for measured update.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//what is the internal functions that need updating when something changes that has influence on this.
|
|
||||||
updatePosition() {
|
|
||||||
if (this.state.getCurrentState() == "operational" || this.state.getCurrentState() == "accelerating" || this.state.getCurrentState() == "decelerating") {
|
|
||||||
|
|
||||||
const currentPosition = this.state.getCurrentPosition();
|
|
||||||
|
|
||||||
// Update the predicted values based on the new position
|
|
||||||
const { cPower, cFlow } = this.calcFlowPower(currentPosition);
|
|
||||||
|
|
||||||
// Calc predicted efficiency
|
|
||||||
const efficiency = this.calcEfficiency(cPower, cFlow, "predicted");
|
|
||||||
|
|
||||||
//update the cog
|
|
||||||
const { cog, minEfficiency } = this.calcCog();
|
|
||||||
|
|
||||||
//update the distance from peak
|
|
||||||
this.calcDistanceBEP(efficiency,cog,minEfficiency);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
calcDistanceFromPeak(currentEfficiency,peakEfficiency){
|
|
||||||
return Math.abs(currentEfficiency - peakEfficiency);
|
|
||||||
}
|
|
||||||
|
|
||||||
calcRelativeDistanceFromPeak(currentEfficiency,maxEfficiency,minEfficiency){
|
|
||||||
let distance = 1;
|
|
||||||
if(currentEfficiency != null){
|
|
||||||
distance = this.interpolation.interpolate_lin_single_point(currentEfficiency,maxEfficiency, minEfficiency, 0, 1);
|
|
||||||
}
|
|
||||||
return distance;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate the center of gravity for current pressure
|
|
||||||
calcCog() {
|
|
||||||
|
|
||||||
//fetch current curve data for power and flow
|
|
||||||
const { powerCurve, flowCurve } = this.getCurrentCurves();
|
|
||||||
|
|
||||||
const {efficiencyCurve, peak, peakIndex, minEfficiency } = this.calcEfficiencyCurve(powerCurve, flowCurve);
|
|
||||||
|
|
||||||
// Calculate the normalized center of gravity
|
|
||||||
const NCog = (flowCurve.y[peakIndex] - this.predictFlow.currentFxyYMin) / (this.predictFlow.currentFxyYMax - this.predictFlow.currentFxyYMin);
|
|
||||||
|
|
||||||
//store in object for later retrieval
|
|
||||||
this.currentEfficiencyCurve = efficiencyCurve;
|
|
||||||
this.cog = peak;
|
|
||||||
this.cogIndex = peakIndex;
|
|
||||||
this.NCog = NCog;
|
|
||||||
this.minEfficiency = minEfficiency;
|
|
||||||
|
|
||||||
return { cog: peak, cogIndex: peakIndex, NCog: NCog, minEfficiency: minEfficiency };
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
calcEfficiencyCurve(powerCurve, flowCurve) {
|
|
||||||
|
|
||||||
const efficiencyCurve = [];
|
|
||||||
let peak = 0;
|
|
||||||
let peakIndex = 0;
|
|
||||||
let minEfficiency = 0;
|
|
||||||
|
|
||||||
// Calculate efficiency curve based on power and flow curves
|
|
||||||
powerCurve.y.forEach((power, index) => {
|
|
||||||
|
|
||||||
// Get flow for the current power
|
|
||||||
const flow = flowCurve.y[index];
|
|
||||||
|
|
||||||
// higher efficiency is better
|
|
||||||
efficiencyCurve.push( Math.round( ( flow / power ) * 100 ) / 100);
|
|
||||||
|
|
||||||
// Keep track of peak efficiency
|
|
||||||
peak = Math.max(peak, efficiencyCurve[index]);
|
|
||||||
peakIndex = peak == efficiencyCurve[index] ? index : peakIndex;
|
|
||||||
minEfficiency = Math.min(...efficiencyCurve);
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
return { efficiencyCurve, peak, peakIndex, minEfficiency };
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
//calc flow power based on pressure and current position
|
|
||||||
calcFlowPower(x) {
|
|
||||||
|
|
||||||
// Calculate flow and power
|
|
||||||
const cFlow = this.calcFlow(x);
|
|
||||||
const cPower = this.calcPower(x);
|
|
||||||
|
|
||||||
return { cPower, cFlow };
|
|
||||||
}
|
|
||||||
|
|
||||||
calcEfficiency(power, flow, variant) {
|
|
||||||
|
|
||||||
if (power != 0 && flow != 0) {
|
|
||||||
// Calculate efficiency after measurements update
|
|
||||||
this.measurements.type("efficiency").variant(variant).position('downstream').value((flow / power));
|
|
||||||
} else {
|
|
||||||
this.measurements.type("efficiency").variant(variant).position('downstream').value(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.measurements.type("efficiency").variant(variant).position('downstream').getCurrentValue();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
updateCurve(newCurve) {
|
|
||||||
this.logger.info(`Updating machine curve`);
|
|
||||||
const newConfig = { asset: { machineCurve: newCurve } };
|
|
||||||
|
|
||||||
//validate input of new curve fed to the machine
|
|
||||||
this.config = this.configUtils.updateConfig(this.config, newConfig);
|
|
||||||
|
|
||||||
//After we passed validation load the curves into their predictors
|
|
||||||
this.predictFlow.updateCurve(this.config.asset.machineCurve.nq);
|
|
||||||
this.predictPower.updateCurve(this.config.asset.machineCurve.np);
|
|
||||||
this.predictCtrl.updateCurve(this.reverseCurve(this.config.asset.machineCurve.nq));
|
|
||||||
}
|
|
||||||
|
|
||||||
getCompleteCurve() {
|
|
||||||
const powerCurve = this.predictPower.inputCurveData;
|
|
||||||
const flowCurve = this.predictFlow.inputCurveData;
|
|
||||||
return { powerCurve, flowCurve };
|
|
||||||
}
|
|
||||||
|
|
||||||
getCurrentCurves() {
|
|
||||||
const powerCurve = this.predictPower.currentFxyCurve[this.predictPower.currentF];
|
|
||||||
const flowCurve = this.predictFlow.currentFxyCurve[this.predictFlow.currentF];
|
|
||||||
|
|
||||||
return { powerCurve, flowCurve };
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
calcDistanceBEP(efficiency,maxEfficiency,minEfficiency) {
|
|
||||||
|
|
||||||
const absDistFromPeak = this.calcDistanceFromPeak(efficiency,maxEfficiency);
|
|
||||||
const relDistFromPeak = this.calcRelativeDistanceFromPeak(efficiency,maxEfficiency,minEfficiency);
|
|
||||||
|
|
||||||
//store internally
|
|
||||||
this.absDistFromPeak = absDistFromPeak ;
|
|
||||||
this.relDistFromPeak = relDistFromPeak;
|
|
||||||
|
|
||||||
return { absDistFromPeak: absDistFromPeak, relDistFromPeak: relDistFromPeak };
|
|
||||||
}
|
|
||||||
|
|
||||||
getOutput() {
|
|
||||||
|
|
||||||
// Improved output object generation
|
|
||||||
const output = {};
|
|
||||||
//build the output object
|
|
||||||
this.measurements.getTypes().forEach(type => {
|
|
||||||
this.measurements.getVariants(type).forEach(variant => {
|
|
||||||
|
|
||||||
const downstreamVal = this.measurements.type(type).variant(variant).position("downstream").getCurrentValue();
|
|
||||||
const upstreamVal = this.measurements.type(type).variant(variant).position("upstream").getCurrentValue();
|
|
||||||
|
|
||||||
if (downstreamVal != null) {
|
|
||||||
output[`downstream_${variant}_${type}`] = downstreamVal;
|
|
||||||
}
|
|
||||||
if (upstreamVal != null) {
|
|
||||||
output[`upstream_${variant}_${type}`] = upstreamVal;
|
|
||||||
}
|
|
||||||
if (downstreamVal != null && upstreamVal != null) {
|
|
||||||
const diffVal = this.measurements.type(type).variant(variant).difference().value;
|
|
||||||
output[`differential_${variant}_${type}`] = diffVal;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
//fill in the rest of the output object
|
|
||||||
output["state"] = this.state.getCurrentState();
|
|
||||||
output["runtime"] = this.state.getRunTimeHours();
|
|
||||||
output["ctrl"] = this.state.getCurrentPosition();
|
|
||||||
output["moveTimeleft"] = this.state.getMoveTimeLeft();
|
|
||||||
output["mode"] = this.currentMode;
|
|
||||||
output["cog"] = this.cog; // flow / power efficiency
|
|
||||||
output["NCog"] = this.NCog; // normalized cog
|
|
||||||
output["NCogPercent"] = Math.round(this.NCog * 100 * 100) / 100 ;
|
|
||||||
|
|
||||||
if(this.flowDrift != null){
|
|
||||||
const flowDrift = this.flowDrift;
|
|
||||||
output["flowNrmse"] = flowDrift.nrmse;
|
|
||||||
output["flowLongterNRMSD"] = flowDrift.longTermNRMSD;
|
|
||||||
output["flowImmediateLevel"] = flowDrift.immediateLevel;
|
|
||||||
output["flowLongTermLevel"] = flowDrift.longTermLevel;
|
|
||||||
}
|
|
||||||
|
|
||||||
//should this all go in the container of measurements?
|
|
||||||
output["effDistFromPeak"] = this.absDistFromPeak;
|
|
||||||
output["effRelDistFromPeak"] = this.relDistFromPeak;
|
|
||||||
//this.logger.debug(`Output: ${JSON.stringify(output)}`);
|
|
||||||
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
} // end of class
|
|
||||||
|
|
||||||
module.exports = Machine;
|
|
||||||
|
|
||||||
/*------------------- Testing -------------------*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
curve = require('C:/Users/zn375/.node-red/public/fallbackData.json');
|
|
||||||
|
|
||||||
//import a child
|
|
||||||
const Child = require('../../../measurement/dependencies/measurement/measurement');
|
|
||||||
|
|
||||||
console.log(`Creating child...`);
|
|
||||||
const PT1 = new Child(config={
|
|
||||||
general:{
|
|
||||||
name:"PT1",
|
|
||||||
logging:{
|
|
||||||
enabled:true,
|
|
||||||
logLevel:"debug",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
functionality:{
|
|
||||||
softwareType:"measurement",
|
|
||||||
},
|
|
||||||
asset:{
|
|
||||||
type:"sensor",
|
|
||||||
subType:"pressure",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const PT2 = new Child(config={
|
|
||||||
general:{
|
|
||||||
name:"PT2",
|
|
||||||
logging:{
|
|
||||||
enabled:true,
|
|
||||||
logLevel:"debug",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
functionality:{
|
|
||||||
softwareType:"measurement",
|
|
||||||
},
|
|
||||||
asset:{
|
|
||||||
type:"sensor",
|
|
||||||
subType:"pressure",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
//create a machine
|
|
||||||
console.log(`Creating machine...`);
|
|
||||||
|
|
||||||
const machineConfig = {
|
|
||||||
general: {
|
|
||||||
name: "Hydrostal",
|
|
||||||
logging: {
|
|
||||||
enabled: true,
|
|
||||||
logLevel: "debug",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
asset: {
|
|
||||||
supplier: "Hydrostal",
|
|
||||||
type: "pump",
|
|
||||||
subType: "centrifugal",
|
|
||||||
model: "H05K-S03R+HGM1X-X280KO", // Ensure this field is present.
|
|
||||||
machineCurve: curve["machineCurves"]["Hydrostal"]["H05K-S03R+HGM1X-X280KO"],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const stateConfig = {
|
|
||||||
general: {
|
|
||||||
logging: {
|
|
||||||
enabled: true,
|
|
||||||
logLevel: "debug",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// Your custom config here (or leave empty for defaults)
|
|
||||||
movement: {
|
|
||||||
speed: 1,
|
|
||||||
},
|
|
||||||
time: {
|
|
||||||
starting: 2,
|
|
||||||
warmingup: 3,
|
|
||||||
stopping: 2,
|
|
||||||
coolingdown: 3,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const machine = new Machine(machineConfig, stateConfig);
|
|
||||||
|
|
||||||
//machine.logger.info(JSON.stringify(curve["machineCurves"]["Hydrostal"]["H05K-S03R+HGM1X-X280KO"]));
|
|
||||||
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);
|
|
||||||
|
|
||||||
testingSequences();
|
|
||||||
|
|
||||||
const tickLoop = setInterval(changeInput,1000);
|
|
||||||
|
|
||||||
function changeInput(){
|
|
||||||
PT1.logger.info(`tick...`);
|
|
||||||
PT1.tick();
|
|
||||||
PT2.tick();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function testingSequences(){
|
|
||||||
try{
|
|
||||||
console.log(` ********** Testing sequence startup... **********`);
|
|
||||||
await machine.handleInput("parent", "execSequence", "startup");
|
|
||||||
console.log(` ********** Testing movement to 15... **********`);
|
|
||||||
await machine.handleInput("parent", "execMovement", 15);
|
|
||||||
machine.getOutput();
|
|
||||||
console.log(` ********** Testing sequence shutdown... **********`);
|
|
||||||
await machine.handleInput("parent", "execSequence", "shutdown");
|
|
||||||
console.log(`********** Testing moving to setpoint 10... while in idle **********`);
|
|
||||||
await machine.handleInput("parent", "execMovement", 10);
|
|
||||||
console.log(` ********** Testing sequence emergencyStop... **********`);
|
|
||||||
await machine.handleInput("parent", "execSequence", "emergencystop");
|
|
||||||
console.log(`********** Testing sequence boot... **********`);
|
|
||||||
await machine.handleInput("parent", "execSequence", "boot");
|
|
||||||
}catch(error){
|
|
||||||
console.error(`Error: ${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//*/
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
297
dependencies/machine/machine.test.js
vendored
297
dependencies/machine/machine.test.js
vendored
@@ -1,297 +0,0 @@
|
|||||||
const Machine = require('./machine');
|
|
||||||
const specs = require('../../../generalFunctions/datasets/assetData/pumps/hydrostal/centrifugal pumps/models.json');
|
|
||||||
|
|
||||||
class MachineTester {
|
|
||||||
constructor() {
|
|
||||||
this.totalTests = 0;
|
|
||||||
this.passedTests = 0;
|
|
||||||
this.failedTests = 0;
|
|
||||||
this.machineCurve = specs[0].machineCurve;
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(condition, message) {
|
|
||||||
this.totalTests++;
|
|
||||||
if (condition) {
|
|
||||||
console.log(`✓ PASS: ${message}`);
|
|
||||||
this.passedTests++;
|
|
||||||
} else {
|
|
||||||
console.log(`✗ FAIL: ${message}`);
|
|
||||||
this.failedTests++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
createBaseMachineConfig(name) {
|
|
||||||
return {
|
|
||||||
general: {
|
|
||||||
logging: { enabled: true, logLevel: "debug" },
|
|
||||||
name: name,
|
|
||||||
unit: "m3/h"
|
|
||||||
},
|
|
||||||
functionality: {
|
|
||||||
softwareType: "machine",
|
|
||||||
role: "RotationalDeviceController"
|
|
||||||
},
|
|
||||||
asset: {
|
|
||||||
type: "pump",
|
|
||||||
subType: "Centrifugal",
|
|
||||||
model: "TestModel",
|
|
||||||
supplier: "Hydrostal",
|
|
||||||
machineCurve: this.machineCurve
|
|
||||||
},
|
|
||||||
mode: {
|
|
||||||
current: "auto",
|
|
||||||
allowedActions: {
|
|
||||||
auto: ["execSequence", "execMovement", "statusCheck"],
|
|
||||||
virtualControl: ["execMovement", "statusCheck"],
|
|
||||||
fysicalControl: ["statusCheck"]
|
|
||||||
},
|
|
||||||
allowedSources: {
|
|
||||||
auto: ["parent", "GUI"],
|
|
||||||
virtualControl: ["GUI"],
|
|
||||||
fysicalControl: ["fysical"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sequences: {
|
|
||||||
startup: ["starting", "warmingup", "operational"],
|
|
||||||
shutdown: ["stopping", "coolingdown", "idle"],
|
|
||||||
emergencystop: ["emergencystop", "off"],
|
|
||||||
boot: ["idle", "starting", "warmingup", "operational"]
|
|
||||||
},
|
|
||||||
calculationMode: "medium"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async testBasicOperations() {
|
|
||||||
console.log('\nTesting Basic Machine Operations...');
|
|
||||||
|
|
||||||
const machine = new Machine(this.createBaseMachineConfig("TestMachine"));
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Test 1: Initialization
|
|
||||||
this.assert(
|
|
||||||
machine.currentMode === "auto",
|
|
||||||
'Machine should initialize in auto mode'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 2: Set pressure measurement
|
|
||||||
machine.measurements.type("pressure").variant("measured").position("downstream").value(800);
|
|
||||||
const pressure = machine.handleMeasuredPressure();
|
|
||||||
this.assert(
|
|
||||||
pressure === 800,
|
|
||||||
'Should correctly handle pressure measurement'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 3: State transition
|
|
||||||
await machine.state.transitionToState("idle");
|
|
||||||
this.assert(
|
|
||||||
machine.state.getCurrentState() === "idle",
|
|
||||||
'Should transition to idle state'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 4: Mode change
|
|
||||||
machine.setMode("virtualControl");
|
|
||||||
this.assert(
|
|
||||||
machine.currentMode === "virtualControl",
|
|
||||||
'Should change mode to virtual control'
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Test failed with error:', error);
|
|
||||||
this.failedTests++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async testPredictions() {
|
|
||||||
console.log('\nTesting Machine Predictions...');
|
|
||||||
|
|
||||||
const machine = new Machine(this.createBaseMachineConfig("TestMachine"));
|
|
||||||
machine.measurements.type("pressure").variant("measured").position("downstream").value(800);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Test 1: Flow prediction
|
|
||||||
const flow = machine.calcFlow(50);
|
|
||||||
this.assert(
|
|
||||||
flow > 0 && !isNaN(flow),
|
|
||||||
'Should calculate valid flow for control value'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 2: Power prediction
|
|
||||||
const power = machine.calcPower(50);
|
|
||||||
this.assert(
|
|
||||||
power > 0 && !isNaN(power),
|
|
||||||
'Should calculate valid power for control value'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 3: Control prediction
|
|
||||||
const ctrl = machine.calcCtrl(100);
|
|
||||||
this.assert(
|
|
||||||
ctrl >= 0 && ctrl <= 100,
|
|
||||||
'Should calculate valid control value for desired flow'
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Test failed with error:', error);
|
|
||||||
this.failedTests++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async testSequenceExecution() {
|
|
||||||
console.log('\nTesting Machine Sequences...');
|
|
||||||
|
|
||||||
const machine = new Machine(this.createBaseMachineConfig("TestMachine"));
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Test 1: Startup sequence
|
|
||||||
await machine.handleInput("parent", "execSequence", "startup");
|
|
||||||
this.assert(
|
|
||||||
machine.state.getCurrentState() === "operational",
|
|
||||||
'Should complete startup sequence'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 2: Movement after startup
|
|
||||||
await machine.handleInput("parent", "execMovement", 50);
|
|
||||||
this.assert(
|
|
||||||
machine.state.getCurrentPosition() === 50,
|
|
||||||
'Should move to specified position'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 3: Shutdown sequence
|
|
||||||
await machine.handleInput("parent", "execSequence", "shutdown");
|
|
||||||
this.assert(
|
|
||||||
machine.state.getCurrentState() === "idle",
|
|
||||||
'Should complete shutdown sequence'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 4: Emergency stop
|
|
||||||
await machine.handleInput("parent", "execSequence", "emergencystop");
|
|
||||||
this.assert(
|
|
||||||
machine.state.getCurrentState() === "off",
|
|
||||||
'Should execute emergency stop'
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Test failed with error:', error);
|
|
||||||
this.failedTests++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async testMeasurementHandling() {
|
|
||||||
console.log('\nTesting Measurement Handling...');
|
|
||||||
|
|
||||||
const machine = new Machine(this.createBaseMachineConfig("TestMachine"));
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Test 1: Pressure measurement
|
|
||||||
machine.measurements.type("pressure").variant("measured").position("downstream").value(800);
|
|
||||||
machine.measurements.type("pressure").variant("measured").setUpstream(1000);
|
|
||||||
const pressure = machine.handleMeasuredPressure();
|
|
||||||
this.assert(
|
|
||||||
pressure === 200,
|
|
||||||
'Should calculate correct differential pressure'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 2: Flow measurement
|
|
||||||
machine.measurements.type("flow").variant("measured").position("downstream").value(100);
|
|
||||||
const flow = machine.handleMeasuredFlow();
|
|
||||||
this.assert(
|
|
||||||
flow === 100,
|
|
||||||
'Should handle flow measurement correctly'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 3: Power measurement
|
|
||||||
machine.measurements.type("power").variant("measured").setUpstream(75);
|
|
||||||
const power = machine.handleMeasuredPower();
|
|
||||||
this.assert(
|
|
||||||
power === 75,
|
|
||||||
'Should handle power measurement correctly'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 4: Efficiency calculation
|
|
||||||
machine.calcEfficiency();
|
|
||||||
const efficiency = machine.measurements.type("efficiency").variant("measured").getDownstream();
|
|
||||||
this.assert(
|
|
||||||
efficiency > 0 && !isNaN(efficiency),
|
|
||||||
'Should calculate valid efficiency'
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Test failed with error:', error);
|
|
||||||
this.failedTests++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async testCurveHandling() {
|
|
||||||
console.log('\nTesting Machine Curve Handling...');
|
|
||||||
|
|
||||||
const machine = new Machine(this.createBaseMachineConfig("TestMachine"));
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Test 1: Curve initialization
|
|
||||||
const curves = machine.showCurve();
|
|
||||||
this.assert(
|
|
||||||
curves.powerCurve && curves.flowCurve,
|
|
||||||
'Should properly initialize power and flow curves'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 2: Test reverse curve creation
|
|
||||||
const reversedCurve = machine.reverseCurve(this.machineCurve.nq);
|
|
||||||
this.assert(
|
|
||||||
reversedCurve["1"].x[0] === this.machineCurve.nq["1"].y[0] &&
|
|
||||||
reversedCurve["1"].y[0] === this.machineCurve.nq["1"].x[0],
|
|
||||||
'Should correctly reverse x and y values in curve'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 3: Update curve dynamically
|
|
||||||
const newCurve = {
|
|
||||||
nq: {
|
|
||||||
"1": {
|
|
||||||
x: [0, 25, 50, 75, 100],
|
|
||||||
y: [0, 125, 250, 375, 500]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
np: {
|
|
||||||
"1": {
|
|
||||||
x: [0, 25, 50, 75, 100],
|
|
||||||
y: [0, 75, 150, 225, 300]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
machine.updateCurve(newCurve);
|
|
||||||
const updatedCurves = machine.showCurve();
|
|
||||||
this.assert(
|
|
||||||
updatedCurves.flowCurve["1"].y[2] === 250,
|
|
||||||
'Should update curve with new values'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test 4: Verify curve interpolation
|
|
||||||
machine.measurements.type("pressure").variant("measured").position("downstream").value(800);
|
|
||||||
const midpointCtrl = machine.calcCtrl(250); // Should interpolate between points
|
|
||||||
const calculatedFlow = machine.calcFlow(midpointCtrl);
|
|
||||||
this.assert(
|
|
||||||
Math.abs(calculatedFlow - 250) < 1, // Allow small numerical error
|
|
||||||
'Should accurately interpolate between curve points'
|
|
||||||
);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Test failed with error:', error);
|
|
||||||
this.failedTests++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async runAllTests() {
|
|
||||||
console.log('Starting Machine Tests...\n');
|
|
||||||
|
|
||||||
await this.testBasicOperations();
|
|
||||||
await this.testPredictions();
|
|
||||||
await this.testSequenceExecution();
|
|
||||||
await this.testMeasurementHandling();
|
|
||||||
await this.testCurveHandling();
|
|
||||||
|
|
||||||
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 the tests
|
|
||||||
const tester = new MachineTester();
|
|
||||||
tester.runAllTests().catch(console.error);
|
|
||||||
@@ -1,247 +0,0 @@
|
|||||||
module.exports = function (RED) {
|
|
||||||
function rotatingMachine(config) {
|
|
||||||
RED.nodes.createNode(this, config);
|
|
||||||
var node = this;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Load Machine class and curve data
|
|
||||||
const Machine = require("./dependencies/machine/machine");
|
|
||||||
const OutputUtils = require("../generalFunctions/helper/outputUtils");
|
|
||||||
|
|
||||||
const machineConfig = {
|
|
||||||
general: {
|
|
||||||
name: config.name || "Default Machine",
|
|
||||||
id: node.id,
|
|
||||||
logging: {
|
|
||||||
enabled: config.eneableLog,
|
|
||||||
logLevel: config.logLevel
|
|
||||||
}
|
|
||||||
},
|
|
||||||
asset: {
|
|
||||||
supplier: config.supplier || "Unknown",
|
|
||||||
type: config.machineType || "generic",
|
|
||||||
subType: config.subType || "generic",
|
|
||||||
model: config.model || "generic",
|
|
||||||
machineCurve: config.machineCurve
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const stateConfig = {
|
|
||||||
general: {
|
|
||||||
logging: {
|
|
||||||
enabled: config.eneableLog,
|
|
||||||
logLevel: config.logLevel
|
|
||||||
}
|
|
||||||
},
|
|
||||||
movement: {
|
|
||||||
speed: Number(config.speed)
|
|
||||||
},
|
|
||||||
time: {
|
|
||||||
starting: Number(config.startup),
|
|
||||||
warmingup: Number(config.warmup),
|
|
||||||
stopping: Number(config.shutdown),
|
|
||||||
coolingdown: Number(config.cooldown)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create machine instance
|
|
||||||
const m = new Machine(machineConfig, stateConfig);
|
|
||||||
|
|
||||||
// put m on node memory as source
|
|
||||||
node.source = m;
|
|
||||||
|
|
||||||
//load output utils
|
|
||||||
const output = new OutputUtils();
|
|
||||||
|
|
||||||
function updateNodeStatus() {
|
|
||||||
try {
|
|
||||||
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());
|
|
||||||
let symbolState;
|
|
||||||
switch(state){
|
|
||||||
case "off":
|
|
||||||
symbolState = "⬛";
|
|
||||||
break;
|
|
||||||
case "idle":
|
|
||||||
symbolState = "⏸️";
|
|
||||||
break;
|
|
||||||
case "operational":
|
|
||||||
symbolState = "⏵️";
|
|
||||||
break;
|
|
||||||
case "starting":
|
|
||||||
symbolState = "⏯️";
|
|
||||||
break;
|
|
||||||
case "warmingup":
|
|
||||||
symbolState = "🔄";
|
|
||||||
break;
|
|
||||||
case "accelerating":
|
|
||||||
symbolState = "⏩";
|
|
||||||
break;
|
|
||||||
case "stopping":
|
|
||||||
symbolState = "⏹️";
|
|
||||||
break;
|
|
||||||
case "coolingdown":
|
|
||||||
symbolState = "❄️";
|
|
||||||
break;
|
|
||||||
case "decelerating":
|
|
||||||
symbolState = "⏪";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
const position = m.state.getCurrentPosition();
|
|
||||||
const roundedPosition = Math.round(position * 100) / 100;
|
|
||||||
|
|
||||||
let status;
|
|
||||||
switch (state) {
|
|
||||||
case "off":
|
|
||||||
status = { fill: "red", shape: "dot", text: `${mode}: OFF` };
|
|
||||||
break;
|
|
||||||
case "idle":
|
|
||||||
status = { fill: "blue", shape: "dot", text: `${mode}: ${symbolState}` };
|
|
||||||
break;
|
|
||||||
case "operational":
|
|
||||||
status = { fill: "green", shape: "dot", text: `${mode}: ${symbolState} | ${roundedPosition}% | 💨${flow}m³/h | ⚡${power}kW` };
|
|
||||||
break;
|
|
||||||
case "starting":
|
|
||||||
status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState}` };
|
|
||||||
break;
|
|
||||||
case "warmingup":
|
|
||||||
status = { fill: "green", shape: "dot", text: `${mode}: ${symbolState} | ${roundedPosition}% | 💨${flow}m³/h | ⚡${power}kW` };
|
|
||||||
break;
|
|
||||||
case "accelerating":
|
|
||||||
status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState} | ${roundedPosition}%| 💨${flow}m³/h | ⚡${power}kW` };
|
|
||||||
break;
|
|
||||||
case "stopping":
|
|
||||||
status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState}` };
|
|
||||||
break;
|
|
||||||
case "coolingdown":
|
|
||||||
status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState}` };
|
|
||||||
break;
|
|
||||||
case "decelerating":
|
|
||||||
status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState} - ${roundedPosition}% | 💨${flow}m³/h | ⚡${power}kW` };
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
status = { fill: "grey", shape: "dot", text: `${mode}: ${symbolState}` };
|
|
||||||
}
|
|
||||||
return status;
|
|
||||||
} catch (error) {
|
|
||||||
node.error("Error in updateNodeStatus: " + error.message);
|
|
||||||
return { fill: "red", shape: "ring", text: "Status Error" };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function tick() {
|
|
||||||
try {
|
|
||||||
const status = updateNodeStatus();
|
|
||||||
node.status(status);
|
|
||||||
|
|
||||||
//get output
|
|
||||||
const classOutput = m.getOutput();
|
|
||||||
const dbOutput = output.formatMsg(classOutput, m.config, "influxdb");
|
|
||||||
const pOutput = output.formatMsg(classOutput, m.config, "process");
|
|
||||||
|
|
||||||
//console.log(pOutput);
|
|
||||||
|
|
||||||
//only send output on values that changed
|
|
||||||
let msgs = [];
|
|
||||||
msgs[0] = pOutput;
|
|
||||||
msgs[1] = dbOutput;
|
|
||||||
|
|
||||||
node.send(msgs);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
node.error("Error in tick function: " + error);
|
|
||||||
node.status({ fill: "red", shape: "ring", text: "Tick Error" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// register child on first output this timeout is needed because of node - red stuff
|
|
||||||
setTimeout(
|
|
||||||
() => {
|
|
||||||
|
|
||||||
/*---execute code on first start----*/
|
|
||||||
let msgs = [];
|
|
||||||
|
|
||||||
msgs[2] = { topic : "registerChild" , payload: node.id, positionVsParent: "upstream" };
|
|
||||||
msgs[3] = { topic : "registerChild" , payload: node.id, positionVsParent: "downstream" };
|
|
||||||
|
|
||||||
//send msg
|
|
||||||
this.send(msgs);
|
|
||||||
},
|
|
||||||
100
|
|
||||||
);
|
|
||||||
|
|
||||||
//declare refresh interval internal node
|
|
||||||
|
|
||||||
setTimeout(
|
|
||||||
() => {
|
|
||||||
//---execute code on first start----
|
|
||||||
this.interval_id = setInterval(function(){ tick() },1000)
|
|
||||||
},
|
|
||||||
1000
|
|
||||||
);
|
|
||||||
|
|
||||||
node.on("input", function(msg, send, done) {
|
|
||||||
try {
|
|
||||||
|
|
||||||
/* Update to complete event based node by putting the tick function after an input event */
|
|
||||||
|
|
||||||
|
|
||||||
switch(msg.topic) {
|
|
||||||
case 'registerChild':
|
|
||||||
const childId = msg.payload;
|
|
||||||
const childObj = RED.nodes.getNode(childId);
|
|
||||||
m.childRegistrationUtils.registerChild(childObj.source ,msg.positionVsParent);
|
|
||||||
break;
|
|
||||||
case 'setMode':
|
|
||||||
m.setMode(msg.payload);
|
|
||||||
break;
|
|
||||||
case 'execSequence':
|
|
||||||
const { source, action, parameter } = msg.payload;
|
|
||||||
m.handleInput(source, action, parameter);
|
|
||||||
break;
|
|
||||||
case 'execMovement':
|
|
||||||
const { source: mvSource, action: mvAction, setpoint } = msg.payload;
|
|
||||||
m.handleInput(mvSource, mvAction, Number(setpoint));
|
|
||||||
break;
|
|
||||||
case 'flowMovement':
|
|
||||||
const { source: fmSource, action: fmAction, setpoint: fmSetpoint } = msg.payload;
|
|
||||||
m.handleInput(fmSource, fmAction, Number(fmSetpoint));
|
|
||||||
|
|
||||||
break;
|
|
||||||
case 'emergencystop':
|
|
||||||
const { source: esSource, action: esAction } = msg.payload;
|
|
||||||
m.handleInput(esSource, esAction);
|
|
||||||
break;
|
|
||||||
case 'showCompleteCurve':
|
|
||||||
m.showCompleteCurve();
|
|
||||||
send({ topic : "Showing curve" , payload: m.showCompleteCurve() });
|
|
||||||
break;
|
|
||||||
case 'CoG':
|
|
||||||
m.showCoG();
|
|
||||||
send({ topic : "Showing CoG" , payload: m.showCoG() });
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (done) done();
|
|
||||||
} catch (error) {
|
|
||||||
node.error("Error processing input: " + error.message);
|
|
||||||
if (done) done(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
node.on('close', function(done) {
|
|
||||||
if (node.interval_id) clearTimeout(node.interval_id);
|
|
||||||
if (node.tick_interval) clearInterval(node.tick_interval);
|
|
||||||
if (done) done();
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
node.error("Fatal error in node initialization: " + error.stack);
|
|
||||||
node.status({fill: "red", shape: "ring", text: "Fatal Error"});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RED.nodes.registerType("rotatingMachine", rotatingMachine);
|
|
||||||
};
|
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
color: "#4f8582",
|
color: "#4f8582",
|
||||||
defaults: {
|
defaults: {
|
||||||
// Define default properties
|
// Define default properties
|
||||||
name: { value: "", required: true }, // use asset category as name ?
|
name: { value: ""}, // use asset category as name ?
|
||||||
|
|
||||||
// Define specific properties
|
// Define specific properties
|
||||||
speed: { value: 1, required: true },
|
speed: { value: 1, required: true },
|
||||||
@@ -31,6 +31,7 @@
|
|||||||
|
|
||||||
//physicalAspect
|
//physicalAspect
|
||||||
positionVsParent: { value: "" },
|
positionVsParent: { value: "" },
|
||||||
|
positionIcon: { value: "" },
|
||||||
|
|
||||||
},
|
},
|
||||||
inputs: 1,
|
inputs: 1,
|
||||||
@@ -39,8 +40,8 @@
|
|||||||
outputLabels: ["process", "dbase", "parent"],
|
outputLabels: ["process", "dbase", "parent"],
|
||||||
icon: "font-awesome/fa-tachometer",
|
icon: "font-awesome/fa-tachometer",
|
||||||
|
|
||||||
label: function() {
|
label: function () {
|
||||||
return this.name || "rotatingMachine";
|
return this.positionIcon + " " + this.category.slice(0, -1) || "Machine";
|
||||||
},
|
},
|
||||||
|
|
||||||
oneditprepare: function() {
|
oneditprepare: function() {
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
const nameOfNode = 'rotatingMachine';
|
const nameOfNode = 'rotatingMachine';
|
||||||
const NodeClass = require('./src/nodeClass.js');
|
const nodeClass = require('./src/nodeClass.js');
|
||||||
const { MenuManager, configManager } = require('generalFunctions');
|
const { MenuManager, configManager } = require('generalFunctions');
|
||||||
|
|
||||||
module.exports = function(RED) {
|
module.exports = function(RED) {
|
||||||
// 1) Register the node type and delegate to your class
|
// 1) Register the node type and delegate to your class
|
||||||
RED.nodes.registerType(nameOfNode, function(config) {
|
RED.nodes.registerType(nameOfNode, function(config) {
|
||||||
RED.nodes.createNode(this, config);
|
RED.nodes.createNode(this, config);
|
||||||
this.nodeClass = new NodeClass(config, RED, this, nameOfNode);
|
this.nodeClass = new nodeClass(config, RED, this, nameOfNode);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2) Setup the dynamic menu & config endpoints
|
// 2) Setup the dynamic menu & config endpoints
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* measurement.class.js
|
* node class.js
|
||||||
*
|
*
|
||||||
* Encapsulates all node logic in a reusable class. In future updates we can split this into multiple generic classes and use the config to specifiy which ones to use.
|
* Encapsulates all node logic in a reusable class. In future updates we can split this into multiple generic classes and use the config to specifiy which ones to use.
|
||||||
* This allows us to keep the Node-RED node clean and focused on wiring up the UI and event handlers.
|
* This allows us to keep the Node-RED node clean and focused on wiring up the UI and event handlers.
|
||||||
@@ -7,9 +7,6 @@
|
|||||||
const { outputUtils, configManager } = require('generalFunctions');
|
const { outputUtils, configManager } = require('generalFunctions');
|
||||||
const Specific = require("./specificClass");
|
const Specific = require("./specificClass");
|
||||||
|
|
||||||
/**
|
|
||||||
* Class representing a Node-RED node.
|
|
||||||
*/
|
|
||||||
class nodeClass {
|
class nodeClass {
|
||||||
/**
|
/**
|
||||||
* Create a Node.
|
* Create a Node.
|
||||||
@@ -79,27 +76,32 @@ class nodeClass {
|
|||||||
* Instantiate the core Measurement logic and store as source.
|
* Instantiate the core Measurement logic and store as source.
|
||||||
*/
|
*/
|
||||||
_setupSpecificClass() {
|
_setupSpecificClass() {
|
||||||
|
const machineConfig = this.config;
|
||||||
|
|
||||||
// need extra state for this
|
// need extra state for this
|
||||||
const stateConfig = {
|
const stateConfig = {
|
||||||
general: {
|
general: {
|
||||||
logging: {
|
logging: {
|
||||||
enabled: config.eneableLog,
|
enabled: machineConfig.eneableLog,
|
||||||
logLevel: config.logLevel
|
logLevel: machineConfig.logLevel
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
movement: {
|
movement: {
|
||||||
speed: Number(config.speed)
|
speed: Number(machineConfig.speed)
|
||||||
},
|
},
|
||||||
time: {
|
time: {
|
||||||
starting: Number(config.startup),
|
starting: Number(machineConfig.startup),
|
||||||
warmingup: Number(config.warmup),
|
warmingup: Number(machineConfig.warmup),
|
||||||
stopping: Number(config.shutdown),
|
stopping: Number(machineConfig.shutdown),
|
||||||
coolingdown: Number(config.cooldown)
|
coolingdown: Number(machineConfig.cooldown)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.source = new Specific(this.config, stateConfig);
|
this.source = new Specific(machineConfig, stateConfig);
|
||||||
|
|
||||||
|
//store in node
|
||||||
|
this.node.source = this.source; // Store the source in the node instance for easy access
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -223,7 +225,7 @@ class nodeClass {
|
|||||||
* Execute a single tick: update measurement, format and send outputs.
|
* Execute a single tick: update measurement, format and send outputs.
|
||||||
*/
|
*/
|
||||||
_tick() {
|
_tick() {
|
||||||
this.source.tick();
|
//this.source.tick();
|
||||||
|
|
||||||
const raw = this.source.getOutput();
|
const raw = this.source.getOutput();
|
||||||
const processMsg = this._output.formatMsg(raw, this.config, 'process');
|
const processMsg = this._output.formatMsg(raw, this.config, 'process');
|
||||||
@@ -242,6 +244,7 @@ class nodeClass {
|
|||||||
const m = this.source;
|
const m = this.source;
|
||||||
switch(msg.topic) {
|
switch(msg.topic) {
|
||||||
case 'registerChild':
|
case 'registerChild':
|
||||||
|
// Register this node as a child of the parent node
|
||||||
const childId = msg.payload;
|
const childId = msg.payload;
|
||||||
const childObj = this.RED.nodes.getNode(childId);
|
const childObj = this.RED.nodes.getNode(childId);
|
||||||
m.childRegistrationUtils.registerChild(childObj.source ,msg.positionVsParent);
|
m.childRegistrationUtils.registerChild(childObj.source ,msg.positionVsParent);
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ maintenanceAlert: this.state.checkMaintenanceStatus()
|
|||||||
|
|
||||||
//load local dependencies
|
//load local dependencies
|
||||||
const EventEmitter = require('events');
|
const EventEmitter = require('events');
|
||||||
const {logger,configUtils,configManager,state, nrmse, MeasurementContainer, predict, interpolation , childRegistrationUtils} = require('generalFunctions');
|
const {loadCurve,logger,configUtils,configManager,state, nrmse, MeasurementContainer, predict, interpolation , childRegistrationUtils} = require('generalFunctions');
|
||||||
|
|
||||||
class Machine {
|
class Machine {
|
||||||
|
|
||||||
@@ -53,13 +53,31 @@ class Machine {
|
|||||||
|
|
||||||
//basic setup
|
//basic setup
|
||||||
this.emitter = new EventEmitter(); // Own EventEmitter
|
this.emitter = new EventEmitter(); // Own EventEmitter
|
||||||
|
this.logger = new logger(machineConfig.general.logging.enabled,machineConfig.general.logging.logLevel, machineConfig.general.name);
|
||||||
this.configManager = new configManager();
|
this.configManager = new configManager();
|
||||||
this.defaultConfig = this.configManager.getConfig('rotatingMachine'); // Load default config for rotating machine
|
this.defaultConfig = this.configManager.getConfig('rotatingMachine'); // Load default config for rotating machine ( use software type name ? )
|
||||||
this.configUtils = new configUtils(this.defaultConfig);
|
this.configUtils = new configUtils(this.defaultConfig);
|
||||||
this.config = this.configUtils.initConfig(machineConfig);
|
|
||||||
|
|
||||||
// Init after config is set
|
// Load a specific curve
|
||||||
this.logger = new logger(this.config.general.logging.enabled,this.config.general.logging.logLevel, this.config.general.name);
|
this.model = machineConfig.asset.model; // Get the model from the machineConfig
|
||||||
|
this.curve = this.model ? loadCurve(this.model) : null;
|
||||||
|
|
||||||
|
if (!this.model || !this.curve) {
|
||||||
|
this.logger.warning(`${!this.model ? 'Model not specified' : 'Curve not found for model ' + this.model} in machineConfig. Cannot make predictions.`);
|
||||||
|
// Set prediction objects to null to prevent method calls
|
||||||
|
this.predictFlow = null;
|
||||||
|
this.predictPower = null;
|
||||||
|
this.predictCtrl = null;
|
||||||
|
this.hasCurve = false;
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
this.hasCurve = true;
|
||||||
|
machineConfig = { ...machineConfig, asset: { ...machineConfig.asset, machineCurve: this.curve } }; // Merge curve into machineConfig
|
||||||
|
this.config = this.configUtils.initConfig(machineConfig);
|
||||||
|
this.predictFlow = new predict({ curve: this.config.asset.machineCurve.nq }); // load nq (x : ctrl , y : flow relationship)
|
||||||
|
this.predictPower = new predict({ curve: this.config.asset.machineCurve.np }); // load np (x : ctrl , y : power relationship)
|
||||||
|
this.predictCtrl = new predict({ curve: this.reverseCurve(this.config.asset.machineCurve.nq) }); // load reversed nq (x: flow, y: ctrl relationship)
|
||||||
|
}
|
||||||
|
|
||||||
this.state = new state(stateConfig, this.logger); // Init State manager and pass logger
|
this.state = new state(stateConfig, this.logger); // Init State manager and pass logger
|
||||||
this.errorMetrics = new nrmse(errorMetricsConfig, this.logger);
|
this.errorMetrics = new nrmse(errorMetricsConfig, this.logger);
|
||||||
@@ -71,10 +89,6 @@ class Machine {
|
|||||||
|
|
||||||
this.flowDrift = null;
|
this.flowDrift = null;
|
||||||
|
|
||||||
this.predictFlow = new predict({ curve: this.config.asset.machineCurve.nq }); // load nq (x : ctrl , y : flow relationship)
|
|
||||||
this.predictPower = new predict({ curve: this.config.asset.machineCurve.np }); // load np (x : ctrl , y : power relationship)
|
|
||||||
this.predictCtrl = new predict({ curve: this.reverseCurve(this.config.asset.machineCurve.nq) }); // load reversed nq (x: flow, y: ctrl relationship)
|
|
||||||
|
|
||||||
this.currentMode = this.config.mode.current;
|
this.currentMode = this.config.mode.current;
|
||||||
this.currentEfficiencyCurve = {};
|
this.currentEfficiencyCurve = {};
|
||||||
this.cog = 0;
|
this.cog = 0;
|
||||||
@@ -239,55 +253,84 @@ class Machine {
|
|||||||
|
|
||||||
// Calculate flow based on current pressure and position
|
// Calculate flow based on current pressure and position
|
||||||
calcFlow(x) {
|
calcFlow(x) {
|
||||||
const state = this.state.getCurrentState();
|
if(!this.hasCurve) {
|
||||||
|
const state = this.state.getCurrentState();
|
||||||
|
|
||||||
if (!["operational", "accelerating", "decelerating"].includes(state)) {
|
if (!["operational", "accelerating", "decelerating"].includes(state)) {
|
||||||
this.measurements.type("flow").variant("predicted").position("downstream").value(0);
|
this.measurements.type("flow").variant("predicted").position("downstream").value(0);
|
||||||
this.logger.debug(`Machine is not operational. Setting predicted flow to 0.`);
|
this.logger.debug(`Machine is not operational. Setting predicted flow to 0.`);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
//this.predictFlow.currentX = x; Decrepated
|
//this.predictFlow.currentX = x; Decrepated
|
||||||
const cFlow = this.predictFlow.y(x);
|
const cFlow = this.predictFlow.y(x);
|
||||||
this.measurements.type("flow").variant("predicted").position("downstream").value(cFlow);
|
this.measurements.type("flow").variant("predicted").position("downstream").value(cFlow);
|
||||||
//this.logger.debug(`Calculated flow: ${cFlow} for pressure: ${this.getMeasuredPressure()} and position: ${x}`);
|
//this.logger.debug(`Calculated flow: ${cFlow} for pressure: ${this.getMeasuredPressure()} and position: ${x}`);
|
||||||
return cFlow;
|
return cFlow;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no curve data is available, log a warning and return 0
|
||||||
|
this.logger.warn(`No curve data available for flow calculation. Returning 0.`);
|
||||||
|
this.measurements.type("flow").variant("predicted").position("downstream").value(0);
|
||||||
|
return 0;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate power based on current pressure and position
|
// Calculate power based on current pressure and position
|
||||||
calcPower(x) {
|
calcPower(x) {
|
||||||
const state = this.state.getCurrentState();
|
if(!this.hasCurve) {
|
||||||
if (!["operational", "accelerating", "decelerating"].includes(state)) {
|
const state = this.state.getCurrentState();
|
||||||
this.measurements.type("power").variant("predicted").position('upstream').value(0);
|
if (!["operational", "accelerating", "decelerating"].includes(state)) {
|
||||||
this.logger.debug(`Machine is not operational. Setting predicted power to 0.`);
|
this.measurements.type("power").variant("predicted").position('upstream').value(0);
|
||||||
return 0;
|
this.logger.debug(`Machine is not operational. Setting predicted power to 0.`);
|
||||||
}
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
//this.predictPower.currentX = x; Decrepated
|
//this.predictPower.currentX = x; Decrepated
|
||||||
const cPower = this.predictPower.y(x);
|
const cPower = this.predictPower.y(x);
|
||||||
this.measurements.type("power").variant("predicted").position('upstream').value(cPower);
|
this.measurements.type("power").variant("predicted").position('upstream').value(cPower);
|
||||||
//this.logger.debug(`Calculated power: ${cPower} for pressure: ${this.getMeasuredPressure()} and position: ${x}`);
|
//this.logger.debug(`Calculated power: ${cPower} for pressure: ${this.getMeasuredPressure()} and position: ${x}`);
|
||||||
return cPower;
|
return cPower;
|
||||||
|
}
|
||||||
|
// If no curve data is available, log a warning and return 0
|
||||||
|
this.logger.warn(`No curve data available for power calculation. Returning 0.`);
|
||||||
|
this.measurements.type("power").variant("predicted").position('upstream').value(0);
|
||||||
|
return 0;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// calculate the power consumption using only flow and pressure
|
// calculate the power consumption using only flow and pressure
|
||||||
inputFlowCalcPower(flow) {
|
inputFlowCalcPower(flow) {
|
||||||
this.predictCtrl.currentX = flow;
|
if(!this.hasCurve) {
|
||||||
const cCtrl = this.predictCtrl.y(flow);
|
|
||||||
this.predictPower.currentX = cCtrl;
|
this.predictCtrl.currentX = flow;
|
||||||
const cPower = this.predictPower.y(cCtrl);
|
const cCtrl = this.predictCtrl.y(flow);
|
||||||
return cPower;
|
this.predictPower.currentX = cCtrl;
|
||||||
|
const cPower = this.predictPower.y(cCtrl);
|
||||||
|
return cPower;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no curve data is available, log a warning and return 0
|
||||||
|
this.logger.warn(`No curve data available for power calculation. Returning 0.`);
|
||||||
|
this.measurements.type("power").variant("predicted").position('upstream').value(0);
|
||||||
|
return 0;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to predict control value for a desired flow
|
// Function to predict control value for a desired flow
|
||||||
calcCtrl(x) {
|
calcCtrl(x) {
|
||||||
|
if(!this.hasCurve) {
|
||||||
this.predictCtrl.currentX = x;
|
this.predictCtrl.currentX = x;
|
||||||
const cCtrl = this.predictCtrl.y(x);
|
const cCtrl = this.predictCtrl.y(x);
|
||||||
this.measurements.type("ctrl").variant("predicted").position('upstream').value(cCtrl);
|
this.measurements.type("ctrl").variant("predicted").position('upstream').value(cCtrl);
|
||||||
//this.logger.debug(`Calculated ctrl: ${cCtrl} for pressure: ${this.getMeasuredPressure()} and position: ${x}`);
|
//this.logger.debug(`Calculated ctrl: ${cCtrl} for pressure: ${this.getMeasuredPressure()} and position: ${x}`);
|
||||||
return cCtrl;
|
return cCtrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no curve data is available, log a warning and return 0
|
||||||
|
this.logger.warn(`No curve data available for control calculation. Returning 0.`);
|
||||||
|
this.measurements.type("ctrl").variant("predicted").position('upstream').value(0);
|
||||||
|
return 0;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user