904 lines
31 KiB
JavaScript
904 lines
31 KiB
JavaScript
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');
|
|
|
|
class Machine {
|
|
|
|
/*------------------- Construct and set vars -------------------*/
|
|
constructor(machineConfig = {}, stateConfig = {}, errorMetricsConfig = {}) {
|
|
|
|
//basic setup
|
|
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.defaultConfig = this.configManager.getConfig('rotatingMachine'); // Load default config for rotating machine ( use software type name ? )
|
|
this.configUtils = new configUtils(this.defaultConfig);
|
|
|
|
// Load a specific curve
|
|
this.model = machineConfig.asset.model; // Get the model from the machineConfig
|
|
this.curve = this.model ? loadCurve(this.model) : null;
|
|
|
|
//Init config and check if it is valid
|
|
this.config = this.configUtils.initConfig(machineConfig);
|
|
|
|
//add unique name for this node.
|
|
this.config = this.configUtils.updateConfig(this.config, {general:{name: this.config.functionality?.softwareType + "_" + machineConfig.general.id}}); // add unique name if not present
|
|
|
|
if (!this.model || !this.curve) {
|
|
this.logger.error(`${!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;
|
|
this.config = this.configUtils.updateConfig(this.config, {
|
|
asset: { ...this.config.asset, machineCurve: this.curve }
|
|
});
|
|
machineConfig = { ...machineConfig, asset: { ...machineConfig.asset, machineCurve: this.curve } }; // Merge curve into 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.errorMetrics = new nrmse(errorMetricsConfig, this.logger);
|
|
|
|
// Initialize measurements
|
|
this.measurements = new MeasurementContainer();
|
|
this.interpolation = new interpolation();
|
|
|
|
this.flowDrift = null;
|
|
|
|
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;
|
|
|
|
// When position state changes, update position
|
|
this.state.emitter.on("positionChange", (data) => {
|
|
this.logger.debug(`Position change detected: ${data}`);
|
|
this.updatePosition();
|
|
});
|
|
|
|
// used for holding the source and sink unit operations or other object with setInfluent / getEffluent method for e.g. recirculation.
|
|
this.upstreamReactor = null;
|
|
this.downstreamReactor = null;
|
|
|
|
this.child = {}; // object to hold child information so we know on what to subscribe
|
|
this.childRegistrationUtils = new childRegistrationUtils(this); // Child registration utility
|
|
|
|
}
|
|
|
|
/*------------------- Register child events -------------------*/
|
|
registerChild(child, softwareType) {
|
|
switch (softwareType) {
|
|
case "measurement":
|
|
this.logger.debug(`Registering measurement child...`);
|
|
this._connectMeasurement(child);
|
|
break;
|
|
case "reactor":
|
|
this.logger.debug(`Registering reactor child...`);
|
|
this._connectReactor(child);
|
|
break;
|
|
|
|
default:
|
|
this.logger.error(`Unrecognized softwareType: ${softwareType}`);
|
|
}
|
|
}
|
|
|
|
_connectMeasurement(measurementChild) {
|
|
if (!measurementChild) {
|
|
this.logger.error("Invalid measurement provided.");
|
|
return;
|
|
}
|
|
|
|
const position = measurementChild.config.functionality.positionVsParent;
|
|
const distance = measurementChild.config.functionality.distanceVsParent || 0;
|
|
const measurementType = measurementChild.config.asset.type;
|
|
//rebuild to measurementype.variant no position and then switch based on values not strings or names.
|
|
const eventName = `${measurementType}.measured.${position}`;
|
|
|
|
this.logger.debug(`Setting up listener for ${eventName} from child ${measurementChild.config.general.name}`);
|
|
// Register event listener for measurement updates
|
|
measurementChild.measurements.emitter.on(eventName, (eventData) => {
|
|
this.logger.debug(`🔄 ${position} ${measurementType} from ${eventData.childName}: ${eventData.value} ${eventData.unit}`);
|
|
|
|
// Store directly in parent's measurement container
|
|
this.measurements
|
|
.type(measurementType)
|
|
.variant("measured")
|
|
.position(position)
|
|
.value(eventData.value, eventData.timestamp, eventData.unit);
|
|
|
|
// Call the appropriate handler
|
|
switch (measurementType) {
|
|
case 'pressure':
|
|
this.updateMeasuredPressure(eventData.value, position, eventData);
|
|
break;
|
|
|
|
case 'flow':
|
|
this.updateMeasuredFlow(eventData.value, position, eventData);
|
|
break;
|
|
|
|
default:
|
|
this.logger.warn(`No handler for measurement type: ${measurementType}`);
|
|
// Generic handler - just update position
|
|
this.updatePosition();
|
|
}
|
|
});
|
|
}
|
|
|
|
_connectReactor(reactorChild) {
|
|
if (!reactorChild) {
|
|
this.logger.error("Invalid measurement provided.");
|
|
return;
|
|
}
|
|
|
|
this.downstreamReactor = reactorChild; // downstream from the pumps perpective
|
|
}
|
|
|
|
//---------------- END child stuff -------------//
|
|
|
|
// 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":
|
|
return await this.executeSequence(parameter);
|
|
|
|
case "execMovement":
|
|
return await this.setpoint(parameter);
|
|
|
|
case "flowMovement":
|
|
// Calculate the control value for a desired flow
|
|
const pos = this.calcCtrl(parameter);
|
|
// Move to the desired setpoint
|
|
return await this.setpoint(pos);
|
|
|
|
case "emergencyStop":
|
|
this.logger.warn(`Emergency stop activated by '${source}'.`);
|
|
return await this.executeSequence("emergencyStop");
|
|
|
|
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}`);
|
|
}
|
|
|
|
}
|
|
|
|
abortMovement(reason = "group override") {
|
|
if (this.state?.abortCurrentMovement) {
|
|
this.state.abortCurrentMovement(reason);
|
|
}
|
|
}
|
|
|
|
setMode(newMode) {
|
|
const availableModes = this.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
|
|
}
|
|
}
|
|
|
|
//recalc flow and power
|
|
this.updatePosition();
|
|
}
|
|
|
|
async setpoint(setpoint) {
|
|
|
|
try {
|
|
// Validate setpoint
|
|
if (typeof setpoint !== 'number' || setpoint < 0) {
|
|
throw new Error("Invalid setpoint: Setpoint must be a non-negative number.");
|
|
}
|
|
|
|
this.logger.info(`Setting setpoint to ${setpoint}. Current position: ${this.state.getCurrentPosition()}`);
|
|
|
|
// 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) {
|
|
if(this.hasCurve) {
|
|
if (!this._isOperationalState()) {
|
|
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;
|
|
}
|
|
|
|
// 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
|
|
calcPower(x) {
|
|
if(this.hasCurve) {
|
|
if (!this._isOperationalState()) {
|
|
this.measurements.type("power").variant("predicted").position('atEquipment').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('atEquipment').value(cPower);
|
|
//this.logger.debug(`Calculated power: ${cPower} for pressure: ${this.getMeasuredPressure()} and position: ${x}`);
|
|
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('atEquipment').value(0);
|
|
return 0;
|
|
|
|
}
|
|
|
|
// calculate the power consumption using only flow and pressure
|
|
inputFlowCalcPower(flow) {
|
|
if(this.hasCurve) {
|
|
|
|
this.predictCtrl.currentX = flow;
|
|
const cCtrl = this.predictCtrl.y(flow);
|
|
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('atEquipment').value(0);
|
|
return 0;
|
|
|
|
}
|
|
|
|
// Function to predict control value for a desired flow
|
|
calcCtrl(x) {
|
|
if(this.hasCurve) {
|
|
this.predictCtrl.currentX = x;
|
|
const cCtrl = this.predictCtrl.y(x);
|
|
this.measurements.type("ctrl").variant("predicted").position('atEquipment').value(cCtrl);
|
|
//this.logger.debug(`Calculated ctrl: ${cCtrl} for pressure: ${this.getMeasuredPressure()} and position: ${x}`);
|
|
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('atEquipment').value(0);
|
|
return 0;
|
|
|
|
}
|
|
|
|
// returns the best available pressure measurement to use in the prediction calculation
|
|
// this will be either the differential pressure, downstream or upstream pressure
|
|
getMeasuredPressure() {
|
|
const pressureDiff = this.measurements.type('pressure').variant('measured').difference();
|
|
|
|
// Both upstream & downstream => differential
|
|
if (pressureDiff) {
|
|
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 is less acurate!!`);
|
|
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('flow').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('flow').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("atEquipment").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;
|
|
}
|
|
}
|
|
|
|
// context handler for pressure updates
|
|
updateMeasuredPressure(value, position, context = {}) {
|
|
|
|
this.logger.debug(`Pressure update: ${value} at ${position} from ${context.childName || 'child'} (${context.childId || 'unknown-id'})`);
|
|
|
|
// 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)
|
|
const pressure = this.getMeasuredPressure();
|
|
this.updatePosition();
|
|
|
|
this.logger.debug(`Using pressure: ${pressure} for calculations`);
|
|
}
|
|
|
|
// NEW: Flow handler
|
|
updateMeasuredFlow(value, position, context = {}) {
|
|
|
|
if (!this._isOperationalState()) {
|
|
this.logger.warn(`Machine not operational, skipping flow update from ${context.childName || 'unknown'}`);
|
|
return;
|
|
}
|
|
|
|
this.logger.debug(`Flow update: ${value} at ${position} from ${context.childName || 'child'}`);
|
|
|
|
if (this.upstreamReactor && this.downstreamReactor){
|
|
this._updateConnectedReactor();
|
|
}
|
|
|
|
// Store in parent's measurement container
|
|
this.measurements.type("flow").variant("measured").position(position).value(value, context.timestamp, context.unit);
|
|
|
|
// Update predicted flow if you have prediction capability
|
|
if (this.predictFlow) {
|
|
this.measurements.type("flow").variant("predicted").position("atEquipment").value(this.predictFlow.outputY || 0);
|
|
}
|
|
}
|
|
|
|
_updateConnectedReactor() {
|
|
this.downstreamReactor.setInfluent = this.upstreamReactor.getEffluent[1];
|
|
}
|
|
|
|
// Helper method for operational state check
|
|
_isOperationalState() {
|
|
const state = this.state.getCurrentState();
|
|
return ["operational", "accelerating", "decelerating"].includes(state);
|
|
}
|
|
|
|
//what is the internal functions that need updating when something changes that has influence on this.
|
|
updatePosition() {
|
|
|
|
if (this._isOperationalState()) {
|
|
|
|
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;
|
|
}
|
|
|
|
showWorkingCurves() {
|
|
// Show the current curves for debugging
|
|
const { powerCurve, flowCurve } = this.getCurrentCurves();
|
|
return {
|
|
powerCurve: powerCurve,
|
|
flowCurve: flowCurve,
|
|
cog: this.cog,
|
|
cogIndex: this.cogIndex,
|
|
NCog: this.NCog,
|
|
minEfficiency: this.minEfficiency,
|
|
currentEfficiencyCurve: this.currentEfficiencyCurve,
|
|
absDistFromPeak: this.absDistFromPeak,
|
|
relDistFromPeak: this.relDistFromPeak
|
|
};
|
|
}
|
|
|
|
|
|
// 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('atEquipment').value((flow / power));
|
|
} else {
|
|
this.measurements.type("efficiency").variant(variant).position('atEquipment').value(null);
|
|
}
|
|
|
|
return this.measurements.type("efficiency").variant(variant).position('atEquipment').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/src/specificClass');
|
|
|
|
console.log(`Creating child...`);
|
|
const PT1 = new Child(config={
|
|
general:{
|
|
name:"PT1",
|
|
logging:{
|
|
enabled:true,
|
|
logLevel:"debug",
|
|
},
|
|
},
|
|
functionality:{
|
|
softwareType:"measurement",
|
|
positionVsParent:"upstream",
|
|
},
|
|
asset:{
|
|
supplier:"Vega",
|
|
category:"sensor",
|
|
type:"pressure",
|
|
model:"Vegabar 82",
|
|
unit: "mbar"
|
|
},
|
|
|
|
});
|
|
|
|
const PT2 = new Child(config={
|
|
general:{
|
|
name:"PT2",
|
|
logging:{
|
|
enabled:true,
|
|
logLevel:"debug",
|
|
},
|
|
},
|
|
functionality:{
|
|
softwareType:"measurement",
|
|
positionVsParent:"upstream",
|
|
},
|
|
asset:{
|
|
supplier:"Vega",
|
|
category:"sensor",
|
|
type:"pressure",
|
|
model:"Vegabar 82",
|
|
unit: "mbar"
|
|
},
|
|
});
|
|
|
|
//create a machine
|
|
console.log(`Creating machine...`);
|
|
|
|
const machineConfig = {
|
|
general: {
|
|
name: "Hydrostal",
|
|
logging: {
|
|
enabled: true,
|
|
logLevel: "debug",
|
|
}
|
|
},
|
|
asset: {
|
|
supplier: "Hydrostal",
|
|
type: "pump",
|
|
category: "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}`);
|
|
}
|
|
}
|
|
|
|
|
|
//*/
|
|
|
|
|
|
|