2025-06-25 17:26:13 +02:00
const EventEmitter = require ( 'events' ) ;
2025-07-01 15:25:07 +02:00
const { loadCurve , logger , configUtils , configManager , state , nrmse , MeasurementContainer , predict , interpolation , childRegistrationUtils } = require ( 'generalFunctions' ) ;
2025-09-22 16:06:18 +02:00
const { name } = require ( '../../generalFunctions/src/convert/lodash/lodash._shimkeys' ) ;
2025-06-25 17:26:13 +02:00
class Machine {
/*------------------- Construct and set vars -------------------*/
constructor ( machineConfig = { } , stateConfig = { } , errorMetricsConfig = { } ) {
//basic setup
this . emitter = new EventEmitter ( ) ; // Own EventEmitter
2025-07-01 17:02:51 +02:00
2025-07-01 15:25:07 +02:00
this . logger = new logger ( machineConfig . general . logging . enabled , machineConfig . general . logging . logLevel , machineConfig . general . name ) ;
2025-06-25 17:26:13 +02:00
this . configManager = new configManager ( ) ;
2025-07-01 15:25:07 +02:00
this . defaultConfig = this . configManager . getConfig ( 'rotatingMachine' ) ; // Load default config for rotating machine ( use software type name ? )
2025-06-25 17:26:13 +02:00
this . configUtils = new configUtils ( this . defaultConfig ) ;
2025-07-01 15:25:07 +02:00
// Load a specific curve
this . model = machineConfig . asset . model ; // Get the model from the machineConfig
this . curve = this . model ? loadCurve ( this . model ) : null ;
2025-07-24 13:15:33 +02:00
//Init config and check if it is valid
2025-07-02 16:00:52 +02:00
this . config = this . configUtils . initConfig ( machineConfig ) ;
2025-09-22 16:06:18 +02:00
//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
2025-07-02 16:00:52 +02:00
2025-07-01 15:25:07 +02:00
if ( ! this . model || ! this . curve ) {
2025-09-23 15:11:06 +02:00
this . logger . error ( ` ${ ! this . model ? 'Model not specified' : 'Curve not found for model ' + this . model } in machineConfig. Cannot make predictions. ` ) ;
2025-07-01 15:25:07 +02:00
// 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 ;
2025-07-02 16:00:52 +02:00
this . config = this . configUtils . updateConfig ( this . config , {
asset : { ... this . config . asset , machineCurve : this . curve }
} ) ;
2025-07-01 15:25:07 +02:00
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)
}
2025-06-25 17:26:13 +02:00
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 ( ) ;
2025-08-07 13:52:06 +02:00
2025-06-25 17:26:13 +02:00
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 ;
2025-08-07 13:52:06 +02:00
// When position state changes, update position
2025-06-25 17:26:13 +02:00
this . state . emitter . on ( "positionChange" , ( data ) => {
this . logger . debug ( ` Position change detected: ${ data } ` ) ;
this . updatePosition ( ) ;
} ) ;
2025-10-22 14:41:35 +02:00
// 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 ;
2025-08-07 13:52:06 +02:00
this . child = { } ; // object to hold child information so we know on what to subscribe
2025-06-25 17:26:13 +02:00
this . childRegistrationUtils = new childRegistrationUtils ( this ) ; // Child registration utility
}
2025-08-07 13:52:06 +02:00
/*------------------- Register child events -------------------*/
2025-09-04 17:07:29 +02:00
registerChild ( child , softwareType ) {
2025-10-31 13:07:52 +01:00
if ( ! child ) {
this . logger . error ( ` Invalid ${ softwareType } child provided. ` ) ;
return ;
}
2025-10-16 16:32:20 +02:00
switch ( softwareType ) {
case "measurement" :
2025-10-17 13:38:05 +02:00
this . logger . debug ( ` Registering measurement child... ` ) ;
2025-10-16 16:32:20 +02:00
this . _connectMeasurement ( child ) ;
break ;
case "reactor" :
2025-10-17 13:38:05 +02:00
this . logger . debug ( ` Registering reactor child... ` ) ;
2025-10-16 16:32:20 +02:00
this . _connectReactor ( child ) ;
break ;
2025-08-07 13:52:06 +02:00
2025-10-16 16:32:20 +02:00
default :
this . logger . error ( ` Unrecognized softwareType: ${ softwareType } ` ) ;
2025-08-07 13:52:06 +02:00
}
2025-09-04 17:07:29 +02:00
}
2025-08-08 14:29:15 +02:00
2025-10-16 16:32:20 +02:00
_connectMeasurement ( measurementChild ) {
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 )
2025-10-22 14:41:35 +02:00
. value ( eventData . value , eventData . timestamp , eventData . unit ) ;
2025-08-08 14:29:15 +02:00
2025-10-16 16:32:20 +02:00
// 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 ) {
2025-10-22 14:41:35 +02:00
this . downstreamReactor = reactorChild ; // downstream from the pumps perpective
2025-08-08 14:29:15 +02:00
}
2025-10-22 14:41:35 +02:00
//---------------- END child stuff -------------//
2025-08-07 13:52:06 +02:00
2025-10-22 14:41:35 +02:00
// 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 ;
2025-06-25 17:26:13 +02:00
2025-10-22 14:41:35 +02:00
if ( ! predictedMeasurement || ! measuredMeasurement ) return null ;
return this . errorMetrics . assessDrift (
predictedMeasurement ,
measuredMeasurement ,
processMin ,
processMax
) ;
}
2025-06-25 17:26:13 +02:00
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 ) {
2025-10-02 17:09:24 +02:00
2025-06-25 17:26:13 +02:00
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 } '. ` ) ;
2025-10-02 17:09:24 +02:00
2025-06-25 17:26:13 +02:00
try {
switch ( action ) {
case "execSequence" :
2025-10-02 17:09:24 +02:00
return await this . executeSequence ( parameter ) ;
2025-06-25 17:26:13 +02:00
case "execMovement" :
2025-10-02 17:09:24 +02:00
return await this . setpoint ( parameter ) ;
2025-06-25 17:26:13 +02:00
case "flowMovement" :
// Calculate the control value for a desired flow
const pos = this . calcCtrl ( parameter ) ;
// Move to the desired setpoint
2025-10-02 17:09:24 +02:00
return await this . setpoint ( pos ) ;
2025-06-25 17:26:13 +02:00
case "emergencyStop" :
this . logger . warn ( ` Emergency stop activated by ' ${ source } '. ` ) ;
2025-10-02 17:09:24 +02:00
return await this . executeSequence ( "emergencyStop" ) ;
2025-06-25 17:26:13 +02:00
case "statusCheck" :
this . logger . info ( ` Status Check: Mode = ' ${ this . currentMode } ', Source = ' ${ source } '. ` ) ;
break ;
2025-10-02 17:09:24 +02:00
2025-06-25 17:26:13 +02:00
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 } ` ) ;
}
2025-10-02 17:09:24 +02:00
}
abortMovement ( reason = "group override" ) {
if ( this . state ? . abortCurrentMovement ) {
this . state . abortCurrentMovement ( reason ) ;
}
2025-06-25 17:26:13 +02:00
}
setMode ( newMode ) {
2025-07-24 13:15:33 +02:00
const availableModes = this . defaultConfig . mode . current . rules . values . map ( v => v . value ) ;
2025-06-25 17:26:13 +02:00
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
}
}
2025-10-02 17:09:24 +02:00
//recalc flow and power
this . updatePosition ( ) ;
2025-06-25 17:26:13 +02:00
}
async setpoint ( setpoint ) {
try {
// Validate setpoint
if ( typeof setpoint !== 'number' || setpoint < 0 ) {
throw new Error ( "Invalid setpoint: Setpoint must be a non-negative number." ) ;
}
2025-07-02 16:00:52 +02:00
this . logger . info ( ` Setting setpoint to ${ setpoint } . Current position: ${ this . state . getCurrentPosition ( ) } ` ) ;
2025-06-25 17:26:13 +02:00
// 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 ) {
2025-07-02 16:00:52 +02:00
if ( this . hasCurve ) {
2025-08-07 13:52:06 +02:00
if ( ! this . _isOperationalState ( ) ) {
2025-07-01 15:25:07 +02:00
this . measurements . type ( "flow" ) . variant ( "predicted" ) . position ( "downstream" ) . value ( 0 ) ;
this . logger . debug ( ` Machine is not operational. Setting predicted flow to 0. ` ) ;
return 0 ;
}
2025-06-25 17:26:13 +02:00
//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 ;
2025-07-01 15:25:07 +02:00
}
// 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 ;
2025-06-25 17:26:13 +02:00
}
// Calculate power based on current pressure and position
calcPower ( x ) {
2025-07-02 16:00:52 +02:00
if ( this . hasCurve ) {
2025-08-07 13:52:06 +02:00
if ( ! this . _isOperationalState ( ) ) {
2025-10-02 17:09:24 +02:00
this . measurements . type ( "power" ) . variant ( "predicted" ) . position ( 'atEquipment' ) . value ( 0 ) ;
2025-07-01 15:25:07 +02:00
this . logger . debug ( ` Machine is not operational. Setting predicted power to 0. ` ) ;
return 0 ;
}
2025-06-25 17:26:13 +02:00
//this.predictPower.currentX = x; Decrepated
const cPower = this . predictPower . y ( x ) ;
2025-10-02 17:09:24 +02:00
this . measurements . type ( "power" ) . variant ( "predicted" ) . position ( 'atEquipment' ) . value ( cPower ) ;
2025-06-25 17:26:13 +02:00
//this.logger.debug(`Calculated power: ${cPower} for pressure: ${this.getMeasuredPressure()} and position: ${x}`);
return cPower ;
2025-07-01 15:25:07 +02:00
}
// If no curve data is available, log a warning and return 0
this . logger . warn ( ` No curve data available for power calculation. Returning 0. ` ) ;
2025-10-02 17:09:24 +02:00
this . measurements . type ( "power" ) . variant ( "predicted" ) . position ( 'atEquipment' ) . value ( 0 ) ;
2025-07-01 15:25:07 +02:00
return 0 ;
2025-06-25 17:26:13 +02:00
}
// calculate the power consumption using only flow and pressure
inputFlowCalcPower ( flow ) {
2025-07-02 16:00:52 +02:00
if ( this . hasCurve ) {
2025-07-01 15:25:07 +02:00
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. ` ) ;
2025-10-02 17:09:24 +02:00
this . measurements . type ( "power" ) . variant ( "predicted" ) . position ( 'atEquipment' ) . value ( 0 ) ;
2025-07-01 15:25:07 +02:00
return 0 ;
2025-06-25 17:26:13 +02:00
}
// Function to predict control value for a desired flow
calcCtrl ( x ) {
2025-07-02 16:00:52 +02:00
if ( this . hasCurve ) {
2025-07-01 15:25:07 +02:00
this . predictCtrl . currentX = x ;
const cCtrl = this . predictCtrl . y ( x ) ;
2025-10-02 17:09:24 +02:00
this . measurements . type ( "ctrl" ) . variant ( "predicted" ) . position ( 'atEquipment' ) . value ( cCtrl ) ;
2025-07-01 15:25:07 +02:00
//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. ` ) ;
2025-10-02 17:09:24 +02:00
this . measurements . type ( "ctrl" ) . variant ( "predicted" ) . position ( 'atEquipment' ) . value ( 0 ) ;
2025-07-01 15:25:07 +02:00
return 0 ;
2025-06-25 17:26:13 +02:00
}
2025-08-07 13:52:06 +02:00
// returns the best available pressure measurement to use in the prediction calculation
// this will be either the differential pressure, downstream or upstream pressure
2025-06-25 17:26:13 +02:00
getMeasuredPressure ( ) {
const pressureDiff = this . measurements . type ( 'pressure' ) . variant ( 'measured' ) . difference ( ) ;
2025-10-02 17:09:24 +02:00
2025-06-25 17:26:13 +02:00
// Both upstream & downstream => differential
2025-10-02 17:09:24 +02:00
if ( pressureDiff ) {
2025-06-25 17:26:13 +02:00
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 ) {
2025-10-02 17:09:24 +02:00
this . logger . warn ( ` Using downstream pressure only for prediction: ${ downstreamPressure } This is less acurate!! ` ) ;
2025-06-25 17:26:13 +02:00
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
2025-07-02 16:00:52 +02:00
const upstreamFlow = this . measurements . type ( 'flow' ) . variant ( 'measured' ) . position ( 'upstream' ) . getCurrentValue ( ) ;
2025-06-25 17:26:13 +02:00
// 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
2025-07-02 16:00:52 +02:00
const downstreamFlow = this . measurements . type ( 'flow' ) . variant ( 'measured' ) . position ( 'downstream' ) . getCurrentValue ( ) ;
2025-06-25 17:26:13 +02:00
// 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 ( ) {
2025-08-07 13:52:06 +02:00
const power = this . measurements . type ( "power" ) . variant ( "measured" ) . position ( "atEquipment" ) . getCurrentValue ( ) ;
2025-06-25 17:26:13 +02:00
// 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 ;
}
}
2025-10-07 18:10:45 +02:00
// context handler for pressure updates
2025-08-07 13:52:06 +02:00
updateMeasuredPressure ( value , position , context = { } ) {
2025-10-02 17:09:24 +02:00
2025-08-07 13:52:06 +02:00
this . logger . debug ( ` Pressure update: ${ value } at ${ position } from ${ context . childName || 'child' } ( ${ context . childId || 'unknown-id' } ) ` ) ;
2025-10-07 18:10:45 +02:00
// Store in parent's measurement container
2025-08-07 13:52:06 +02:00
this . measurements . type ( "pressure" ) . variant ( "measured" ) . position ( position ) . value ( value , context . timestamp , context . unit ) ;
2025-08-08 14:29:15 +02:00
// Determine what kind of value to use as pressure (upstream , downstream or difference)
2025-08-07 13:52:06 +02:00
const pressure = this . getMeasuredPressure ( ) ;
this . updatePosition ( ) ;
this . logger . debug ( ` Using pressure: ${ pressure } for calculations ` ) ;
}
2025-06-25 17:26:13 +02:00
2025-09-23 15:51:16 +02:00
// NEW: Flow handler
2025-08-07 13:52:06 +02:00
updateMeasuredFlow ( value , position , context = { } ) {
2025-10-22 14:41:35 +02:00
2025-08-07 13:52:06 +02:00
if ( ! this . _isOperationalState ( ) ) {
this . logger . warn ( ` Machine not operational, skipping flow update from ${ context . childName || 'unknown' } ` ) ;
return ;
}
2025-06-25 17:26:13 +02:00
2025-08-07 13:52:06 +02:00
this . logger . debug ( ` Flow update: ${ value } at ${ position } from ${ context . childName || 'child' } ` ) ;
2025-10-22 14:41:35 +02:00
if ( this . upstreamReactor && this . downstreamReactor ) {
this . _updateConnectedReactor ( ) ;
}
2025-08-07 13:52:06 +02:00
// 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 ) ;
2025-06-25 17:26:13 +02:00
}
}
2025-10-22 14:41:35 +02:00
_updateConnectedReactor ( ) {
2025-10-31 14:16:00 +01:00
// Handles flow according to the configured "flow number"
this . downstreamReactor . setInfluent = this . upstreamReactor . getEffluent [ this . config . flowNumber ] ;
2025-10-22 14:41:35 +02:00
}
2025-08-07 13:52:06 +02:00
// Helper method for operational state check
_isOperationalState ( ) {
const state = this . state . getCurrentState ( ) ;
return [ "operational" , "accelerating" , "decelerating" ] . includes ( state ) ;
2025-06-25 17:26:13 +02:00
}
//what is the internal functions that need updating when something changes that has influence on this.
updatePosition ( ) {
2025-10-02 17:09:24 +02:00
2025-08-07 13:52:06 +02:00
if ( this . _isOperationalState ( ) ) {
2025-06-25 17:26:13 +02:00
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 ;
}
2025-07-24 13:15:33 +02:00
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
} ;
}
2025-06-25 17:26:13 +02:00
// 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
2025-10-02 17:09:24 +02:00
this . measurements . type ( "efficiency" ) . variant ( variant ) . position ( 'atEquipment' ) . value ( ( flow / power ) ) ;
2025-06-25 17:26:13 +02:00
} else {
2025-10-02 17:09:24 +02:00
this . measurements . type ( "efficiency" ) . variant ( variant ) . position ( 'atEquipment' ) . value ( null ) ;
2025-06-25 17:26:13 +02:00
}
2025-10-02 17:09:24 +02:00
return this . measurements . type ( "efficiency" ) . variant ( variant ) . position ( 'atEquipment' ) . getCurrentValue ( ) ;
2025-06-25 17:26:13 +02:00
}
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 -------------------*/
/ *
2025-08-07 13:52:06 +02:00
2025-06-25 17:26:13 +02:00
curve = require ( 'C:/Users/zn375/.node-red/public/fallbackData.json' ) ;
//import a child
2025-08-07 13:52:06 +02:00
const Child = require ( '../../measurement/src/specificClass' ) ;
2025-06-25 17:26:13 +02:00
console . log ( ` Creating child... ` ) ;
const PT1 = new Child ( config = {
general : {
name : "PT1" ,
logging : {
enabled : true ,
logLevel : "debug" ,
} ,
} ,
functionality : {
softwareType : "measurement" ,
2025-08-07 13:52:06 +02:00
positionVsParent : "upstream" ,
2025-06-25 17:26:13 +02:00
} ,
asset : {
2025-08-07 13:52:06 +02:00
supplier : "Vega" ,
category : "sensor" ,
type : "pressure" ,
model : "Vegabar 82" ,
unit : "mbar"
2025-06-25 17:26:13 +02:00
} ,
2025-08-07 13:52:06 +02:00
2025-06-25 17:26:13 +02:00
} ) ;
const PT2 = new Child ( config = {
general : {
name : "PT2" ,
logging : {
enabled : true ,
logLevel : "debug" ,
} ,
} ,
functionality : {
softwareType : "measurement" ,
2025-08-07 13:52:06 +02:00
positionVsParent : "upstream" ,
2025-06-25 17:26:13 +02:00
} ,
asset : {
2025-08-07 13:52:06 +02:00
supplier : "Vega" ,
category : "sensor" ,
type : "pressure" ,
model : "Vegabar 82" ,
unit : "mbar"
2025-06-25 17:26:13 +02:00
} ,
} ) ;
//create a machine
console . log ( ` Creating machine... ` ) ;
const machineConfig = {
general : {
name : "Hydrostal" ,
logging : {
enabled : true ,
logLevel : "debug" ,
}
} ,
asset : {
supplier : "Hydrostal" ,
type : "pump" ,
2025-08-07 13:52:06 +02:00
category : "centrifugal" ,
2025-06-25 17:26:13 +02:00
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 } ` ) ;
}
}
//*/