2025-06-25 17:26:13 +02:00
const EventEmitter = require ( 'events' ) ;
2025-11-12 17:40:38 +01:00
const { loadCurve , gravity , logger , configUtils , configManager , state , nrmse , MeasurementContainer , predict , interpolation , childRegistrationUtils , coolprop } = require ( 'generalFunctions' ) ;
2025-06-25 17:26:13 +02:00
2026-02-23 13:17:18 +01:00
/ * *
* Rotating machine domain model .
* Combines machine curves , state transitions and measurement reconciliation
* to produce flow / power / efficiency behavior for pumps and similar assets .
* /
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
2025-11-30 09:24:37 +01:00
this . curve = this . model ? loadCurve ( this . model ) : null ; // we need to convert the curve and add units to the curve information
2025-07-01 15:25:07 +02:00
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-11-13 19:39:05 +01:00
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
2025-07-01 15:25:07 +02:00
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
2025-10-31 18:35:40 +01:00
this . measurements = new MeasurementContainer ( {
autoConvert : true ,
windowSize : 50 ,
defaultUnits : {
pressure : 'mbar' ,
flow : this . config . general . unit ,
power : 'kW' ,
temperature : 'C'
}
} ) ;
2025-06-25 17:26:13 +02:00
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-31 18:35:40 +01:00
//When state changes look if we need to do other updates
this . state . emitter . on ( "stateChange" , ( newState ) => {
this . logger . debug ( ` State change detected: ${ newState } ` ) ;
this . _updateState ( ) ;
} ) ;
2025-11-12 17:40:38 +01:00
//perform init for certain values
this . _init ( ) ;
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
2026-02-19 17:36:44 +01:00
this . virtualPressureChildIds = {
upstream : "dashboard-sim-upstream" ,
downstream : "dashboard-sim-downstream" ,
} ;
this . virtualPressureChildren = { } ;
this . realPressureChildIds = {
upstream : new Set ( ) ,
downstream : new Set ( ) ,
} ;
this . _initVirtualPressureChildren ( ) ;
2025-06-25 17:26:13 +02:00
2025-11-12 17:40:38 +01:00
}
2026-02-19 17:36:44 +01:00
_initVirtualPressureChildren ( ) {
const createVirtualChild = ( position ) => {
const id = this . virtualPressureChildIds [ position ] ;
const name = ` dashboard-sim- ${ position } ` ;
const measurements = new MeasurementContainer ( {
autoConvert : true ,
defaultUnits : {
pressure : "mbar" ,
flow : this . config . general . unit ,
power : "kW" ,
temperature : "C" ,
} ,
} ) ;
measurements . setChildId ( id ) ;
measurements . setChildName ( name ) ;
measurements . setParentRef ( this ) ;
return {
config : {
general : { id , name } ,
functionality : {
softwareType : "measurement" ,
positionVsParent : position ,
} ,
asset : {
type : "pressure" ,
unit : "mbar" ,
} ,
} ,
measurements ,
} ;
} ;
const upstreamChild = createVirtualChild ( "upstream" ) ;
const downstreamChild = createVirtualChild ( "downstream" ) ;
this . virtualPressureChildren . upstream = upstreamChild ;
this . virtualPressureChildren . downstream = downstreamChild ;
this . registerChild ( upstreamChild , "measurement" ) ;
this . registerChild ( downstreamChild , "measurement" ) ;
}
2025-11-12 17:40:38 +01:00
_init ( ) {
//assume standard temperature is 20degrees
this . measurements . type ( 'temperature' ) . variant ( 'measured' ) . position ( 'atEquipment' ) . value ( 15 ) . unit ( 'C' ) ;
//assume standard atm pressure is at sea level
this . measurements . type ( 'atmPressure' ) . variant ( 'measured' ) . position ( 'atEquipment' ) . value ( 101325 ) . unit ( 'Pa' ) ;
2026-02-12 10:48:44 +01:00
//populate min and max when curve data is available
2025-11-28 09:59:51 +01:00
const flowunit = this . config . general . unit ;
2026-02-12 10:48:44 +01:00
if ( this . predictFlow ) {
this . measurements . type ( 'flow' ) . variant ( 'predicted' ) . position ( 'max' ) . value ( this . predictFlow . currentFxyYMax , Date . now ( ) , flowunit ) ;
this . measurements . type ( 'flow' ) . variant ( 'predicted' ) . position ( 'min' ) . value ( this . predictFlow . currentFxyYMin ) . unit ( this . config . general . unit ) ;
} else {
this . measurements . type ( 'flow' ) . variant ( 'predicted' ) . position ( 'max' ) . value ( 0 , Date . now ( ) , flowunit ) ;
this . measurements . type ( 'flow' ) . variant ( 'predicted' ) . position ( 'min' ) . value ( 0 , Date . now ( ) , flowunit ) ;
}
2025-06-25 17:26:13 +02:00
}
2025-10-31 18:35:40 +01:00
_updateState ( ) {
const isOperational = this . _isOperationalState ( ) ;
if ( ! isOperational ) {
//overrule the last prediction this should be 0 now
2025-11-30 09:24:37 +01:00
this . measurements . type ( "flow" ) . variant ( "predicted" ) . position ( "downstream" ) . value ( 0 , Date . now ( ) , this . config . general . unit ) ;
this . measurements . type ( "flow" ) . variant ( "predicted" ) . position ( "atEquipment" ) . value ( 0 , Date . now ( ) , this . config . general . unit ) ;
2025-10-31 18:35:40 +01:00
}
}
2025-08-07 13:52:06 +02:00
/*------------------- Register child events -------------------*/
2025-09-04 17:07:29 +02:00
registerChild ( child , softwareType ) {
2026-02-19 17:36:44 +01:00
const resolvedSoftwareType = softwareType || child ? . config ? . functionality ? . softwareType || "measurement" ;
this . logger . debug ( 'Setting up child event for softwaretype ' + resolvedSoftwareType ) ;
2025-09-04 17:07:29 +02:00
2026-02-19 17:36:44 +01:00
if ( resolvedSoftwareType === "measurement" ) {
const position = String ( child . config . functionality . positionVsParent || "atEquipment" ) . toLowerCase ( ) ;
2025-09-04 17:07:29 +02:00
const measurementType = child . config . asset . type ;
2026-02-19 17:36:44 +01:00
const childId = child . config ? . general ? . id || ` ${ measurementType } - ${ position } -unknown ` ;
const isVirtualPressureChild = Object . values ( this . virtualPressureChildIds ) . includes ( childId ) ;
if ( measurementType === "pressure" && ! isVirtualPressureChild ) {
this . realPressureChildIds [ position ] ? . add ( childId ) ;
}
2025-10-03 15:41:53 +02:00
//rebuild to measurementype.variant no position and then switch based on values not strings or names.
2025-09-04 17:07:29 +02:00
const eventName = ` ${ measurementType } .measured. ${ position } ` ;
2025-10-03 15:41:53 +02:00
this . logger . debug ( ` Setting up listener for ${ eventName } from child ${ child . config . general . name } ` ) ;
2025-09-04 17:07:29 +02:00
// Register event listener for measurement updates
child . measurements . emitter . on ( eventName , ( eventData ) => {
this . logger . debug ( ` 🔄 ${ position } ${ measurementType } from ${ eventData . childName } : ${ eventData . value } ${ eventData . unit } ` ) ;
2025-08-07 13:52:06 +02:00
2025-10-03 15:41:53 +02:00
2025-11-13 19:39:05 +01:00
this . logger . debug ( ` Emitting... ${ eventName } with data: ` ) ;
2025-09-04 17:07:29 +02:00
// Store directly in parent's measurement container
this . measurements
. type ( measurementType )
. variant ( "measured" )
. position ( position )
2026-02-19 17:36:44 +01:00
. child ( childId )
2025-09-04 17:07:29 +02:00
. value ( eventData . value , eventData . timestamp , eventData . unit ) ;
// Call the appropriate handler
this . _callMeasurementHandler ( measurementType , eventData . value , position , eventData ) ;
} ) ;
2025-08-07 13:52:06 +02:00
}
2025-09-04 17:07:29 +02:00
}
2025-08-08 14:29:15 +02:00
// Centralized handler dispatcher
_callMeasurementHandler ( measurementType , value , position , context ) {
switch ( measurementType ) {
case 'pressure' :
this . updateMeasuredPressure ( value , position , context ) ;
break ;
case 'flow' :
this . updateMeasuredFlow ( value , position , context ) ;
break ;
case 'temperature' :
this . updateMeasuredTemperature ( value , position , context ) ;
break ;
default :
this . logger . warn ( ` No handler for measurement type: ${ measurementType } ` ) ;
// Generic handler - just update position
this . updatePosition ( ) ;
break ;
}
}
//---------------- END child stuff -------------//
2025-08-07 13:52:06 +02:00
2025-06-25 17:26:13 +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 ;
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 ] || [ ] ;
2025-11-05 17:15:47 +01:00
const allowed = allowedSourcesSet . has ( source ) ;
allowed ?
this . logger . debug ( ` source is allowed proceeding with ${ source } for mode ${ mode } ` ) :
this . logger . warn ( ` ${ source } is not allowed in mode ${ mode } ` ) ;
return allowed ;
2025-06-25 17:26:13 +02:00
}
isValidActionForMode ( action , mode ) {
const allowedActionsSet = this . config . mode . allowedActions [ mode ] || [ ] ;
2025-11-05 17:15:47 +01:00
const allowed = allowedActionsSet . has ( action ) ;
allowed ?
this . logger . debug ( ` Action is allowed proceeding with ${ action } for mode ${ mode } ` ) :
this . logger . warn ( ` ${ action } is not allowed in mode ${ mode } ` ) ;
return allowed ;
2025-06-25 17:26:13 +02:00
}
async handleInput ( source , action , parameter ) {
2025-10-02 17:09:24 +02:00
2025-11-05 15:47:39 +01:00
//sanitize input
if ( typeof action !== 'string' ) { this . logger . error ( ` Action must be string ` ) ; return ; }
//convert to lower case to avoid to many mistakes in commands
action = action . toLowerCase ( ) ;
2025-11-05 17:15:47 +01:00
// check for validity of the request
if ( ! this . isValidActionForMode ( action , this . currentMode ) ) { return ; }
if ( ! this . isValidSourceForMode ( source , this . currentMode ) ) { return ; }
2025-06-25 17:26:13 +02:00
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 ) {
2025-11-05 15:47:39 +01:00
case "execsequence" :
2025-10-02 17:09:24 +02:00
return await this . executeSequence ( parameter ) ;
2025-11-05 15:47:39 +01:00
case "execmovement" :
2025-10-02 17:09:24 +02:00
return await this . setpoint ( parameter ) ;
2025-11-05 17:15:47 +01:00
case "entermaintenance" :
return await this . executeSequence ( parameter ) ;
case "exitmaintenance" :
return await this . executeSequence ( parameter ) ;
2025-11-05 15:47:39 +01:00
case "flowmovement" :
2025-06-25 17:26:13 +02:00
// 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-11-05 15:47:39 +01:00
case "emergencystop" :
2025-06-25 17:26:13 +02:00
this . logger . warn ( ` Emergency stop activated by ' ${ source } '. ` ) ;
2025-10-02 17:09:24 +02:00
return await this . executeSequence ( "emergencyStop" ) ;
2025-11-05 15:47:39 +01:00
case "statuscheck" :
2025-06-25 17:26:13 +02:00
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-11-28 09:59:51 +01:00
this . measurements . type ( "flow" ) . variant ( "predicted" ) . position ( "downstream" ) . value ( 0 , Date . now ( ) , this . config . general . unit ) ;
this . measurements . type ( "flow" ) . variant ( "predicted" ) . position ( "atEquipment" ) . value ( 0 , Date . now ( ) , this . config . general . unit ) ;
2025-07-01 15:25:07 +02:00
this . logger . debug ( ` Machine is not operational. Setting predicted flow to 0. ` ) ;
return 0 ;
}
2025-06-25 17:26:13 +02:00
const cFlow = this . predictFlow . y ( x ) ;
2025-11-28 09:59:51 +01:00
this . measurements . type ( "flow" ) . variant ( "predicted" ) . position ( "downstream" ) . value ( cFlow , Date . now ( ) , this . config . general . unit ) ;
this . measurements . type ( "flow" ) . variant ( "predicted" ) . position ( "atEquipment" ) . value ( cFlow , Date . now ( ) , this . config . general . unit ) ;
2025-06-25 17:26:13 +02:00
//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. ` ) ;
2025-11-30 09:24:37 +01:00
this . measurements . type ( "flow" ) . variant ( "predicted" ) . position ( "downstream" ) . value ( 0 , Date . now ( ) , this . config . general . unit ) ;
this . measurements . type ( "flow" ) . variant ( "predicted" ) . position ( "atEquipment" ) . value ( 0 , Date . now ( ) , this . config . general . unit ) ;
2025-07-01 15:25:07 +02:00
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 ( ) {
2025-11-13 19:39:05 +01:00
if ( this . hasCurve === false ) {
this . logger . error ( ` No valid curve available to calculate prediction using last known pressure ` ) ;
return 0 ;
}
2026-02-19 17:36:44 +01:00
const upstreamPressure = this . _getPreferredPressureValue ( "upstream" ) ;
const downstreamPressure = this . _getPreferredPressureValue ( "downstream" ) ;
2025-10-02 17:09:24 +02:00
2025-06-25 17:26:13 +02:00
// Both upstream & downstream => differential
2026-02-19 17:36:44 +01:00
if ( upstreamPressure != null && downstreamPressure != null ) {
const pressureDiffValue = downstreamPressure - upstreamPressure ;
this . logger . debug ( ` Pressure differential: ${ pressureDiffValue } ` ) ;
this . predictFlow . fDimension = pressureDiffValue ;
this . predictPower . fDimension = pressureDiffValue ;
this . predictCtrl . fDimension = pressureDiffValue ;
2025-06-25 17:26:13 +02:00
//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 ) ;
2026-02-19 17:36:44 +01:00
return pressureDiffValue ;
2025-06-25 17:26:13 +02:00
}
// 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 ;
}
2026-02-19 17:36:44 +01:00
// Only upstream => use it, warn that it's partial
if ( upstreamPressure != null ) {
this . logger . warn ( ` Using upstream pressure only for prediction: ${ upstreamPressure } This is less acurate!! ` ) ;
this . predictFlow . fDimension = upstreamPressure ;
this . predictPower . fDimension = upstreamPressure ;
this . predictCtrl . fDimension = upstreamPressure ;
//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 upstreamPressure ;
}
2025-06-25 17:26:13 +02:00
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 ) ;
2025-11-27 17:46:56 +01:00
//place min and max flow capabilities in containerthis.predictFlow.currentFxyYMax - this.predictFlow.currentFxyYMin
2025-11-28 09:59:51 +01:00
this . measurements . type ( 'flow' ) . variant ( 'predicted' ) . position ( 'max' ) . value ( this . predictFlow . currentFxyYMax ) . unit ( this . config . general . unit ) ;
this . measurements . type ( 'flow' ) . variant ( 'predicted' ) . position ( 'min' ) . value ( this . predictFlow . currentFxyYMin ) . unit ( this . config . general . unit ) ;
2025-06-25 17:26:13 +02:00
return 0 ;
}
2026-02-19 17:36:44 +01:00
_getPreferredPressureValue ( position ) {
const realIds = Array . from ( this . realPressureChildIds [ position ] || [ ] ) ;
for ( const childId of realIds ) {
const value = this . measurements
. type ( "pressure" )
. variant ( "measured" )
. position ( position )
. child ( childId )
. getCurrentValue ( ) ;
if ( value != null ) return value ;
}
const virtualId = this . virtualPressureChildIds [ position ] ;
if ( virtualId ) {
const simulatedValue = this . measurements
. type ( "pressure" )
. variant ( "measured" )
. position ( position )
. child ( virtualId )
. getCurrentValue ( ) ;
if ( simulatedValue != null ) return simulatedValue ;
}
return this . measurements
. type ( "pressure" )
. variant ( "measured" )
. position ( position )
. getCurrentValue ( ) ;
}
getPressureInitializationStatus ( ) {
const upstreamPressure = this . _getPreferredPressureValue ( "upstream" ) ;
const downstreamPressure = this . _getPreferredPressureValue ( "downstream" ) ;
const hasUpstream = upstreamPressure != null ;
const hasDownstream = downstreamPressure != null ;
const hasDifferential = hasUpstream && hasDownstream ;
return {
hasUpstream ,
hasDownstream ,
hasDifferential ,
initialized : hasUpstream || hasDownstream || hasDifferential ,
source : hasDifferential ? 'differential' : hasDownstream ? 'downstream' : hasUpstream ? 'upstream' : null ,
} ;
}
updateSimulatedMeasurement ( type , position , value , context = { } ) {
const normalizedType = String ( type || "" ) . toLowerCase ( ) ;
const normalizedPosition = String ( position || "atEquipment" ) . toLowerCase ( ) ;
if ( normalizedType !== "pressure" ) {
this . _callMeasurementHandler ( normalizedType , value , normalizedPosition , context ) ;
return ;
}
if ( ! this . virtualPressureChildIds [ normalizedPosition ] ) {
this . logger . warn ( ` Unsupported simulated pressure position ' ${ normalizedPosition } ' ` ) ;
return ;
}
const child = this . virtualPressureChildren [ normalizedPosition ] ;
if ( ! child ? . measurements ) {
this . logger . error ( ` Virtual pressure child ' ${ normalizedPosition } ' is missing ` ) ;
return ;
}
child . measurements
. type ( "pressure" )
. variant ( "measured" )
. position ( normalizedPosition )
. value ( value , context . timestamp || Date . now ( ) , context . unit || "mbar" ) ;
}
2025-06-25 17:26:13 +02:00
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 ;
}
}
2026-01-29 13:32:39 +01:00
updateMeasuredTemperature ( value , position , context = { } ) {
this . logger . debug ( ` Temperature update: ${ value } at ${ position } from ${ context . childName || 'child' } ( ${ context . childId || 'unknown-id' } ) ` ) ;
}
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 = { } ) {
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' } ` ) ;
// 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 ) {
2025-10-31 18:35:40 +01:00
this . measurements . type ( "flow" ) . variant ( "predicted" ) . position ( "downstream" ) . value ( this . predictFlow . outputY || 0 ) ;
2025-11-13 19:39:05 +01:00
this . measurements . type ( "flow" ) . variant ( "predicted" ) . position ( "atEquipment" ) . value ( this . predictFlow . outputY || 0 ) ;
2025-06-25 17:26:13 +02:00
}
}
2025-08-07 13:52:06 +02:00
// Helper method for operational state check
_isOperationalState ( ) {
const state = this . state . getCurrentState ( ) ;
2026-02-19 17:36:44 +01:00
const activeStates = [ "operational" , "warmingup" , "accelerating" , "decelerating" ] ;
this . logger . debug ( ` Checking operational state ${ this . state . getCurrentState ( ) } ? ${ activeStates . includes ( state ) } ` ) ;
return activeStates . 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 ) ;
}
2025-10-31 18:35:40 +01:00
2025-06-25 17:26:13 +02:00
}
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
2025-11-20 22:29:24 +01:00
const NCog = ( flowCurve . y [ peakIndex ] - this . predictFlow . currentFxyYMin ) / ( this . predictFlow . currentFxyYMax - this . predictFlow . currentFxyYMin ) ; //
2025-06-25 17:26:13 +02:00
//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 } ;
}
2025-11-12 17:40:38 +01:00
calcEfficiency ( power , flow , variant ) {
2026-02-23 13:17:18 +01:00
// Request a pressure differential explicitly in Pascal for hydraulic efficiency.
const pressureDiff = this . measurements
. type ( 'pressure' )
. variant ( 'measured' )
. difference ( { unit : 'Pa' } ) ;
2025-11-12 17:40:38 +01:00
const g = gravity . getStandardGravity ( ) ;
const temp = this . measurements . type ( 'temperature' ) . variant ( 'measured' ) . position ( 'atEquipment' ) . getCurrentValue ( 'K' ) ;
2025-11-28 09:59:51 +01:00
const atmPressure = this . measurements . type ( 'atmPressure' ) . variant ( 'measured' ) . position ( 'atEquipment' ) . getCurrentValue ( 'Pa' ) ;
2026-02-12 10:48:44 +01:00
let rho = null ;
try {
rho = coolprop . PropsSI ( 'D' , 'T' , temp , 'P' , atmPressure , 'WasteWater' ) ;
} catch ( error ) {
// coolprop can throw transient initialization errors; keep machine calculations running.
this . logger . warn ( ` CoolProp density lookup failed: ${ error . message } . Using fallback density. ` ) ;
rho = 1000 ; // kg/m3 fallback for water-like fluids
}
2025-11-12 17:40:38 +01:00
this . logger . debug ( ` temp: ${ temp } atmPressure : ${ atmPressure } rho : ${ rho } pressureDiff: ${ pressureDiff ? . value || 0 } ` ) ;
const flowM3s = this . measurements . type ( 'flow' ) . variant ( 'predicted' ) . position ( 'atEquipment' ) . getCurrentValue ( 'm3/s' ) ;
const powerWatt = this . measurements . type ( 'power' ) . variant ( 'predicted' ) . position ( 'atEquipment' ) . getCurrentValue ( 'W' ) ;
this . logger . debug ( ` Flow : ${ flowM3s } power: ${ powerWatt } ` ) ;
2025-06-25 17:26:13 +02:00
if ( power != 0 && flow != 0 ) {
2025-11-12 17:40:38 +01:00
const specificFlow = flow / power ;
const specificEnergyConsumption = power / flow ;
this . measurements . type ( "efficiency" ) . variant ( variant ) . position ( 'atEquipment' ) . value ( specificFlow ) ;
this . measurements . type ( "specificEnergyConsumption" ) . variant ( variant ) . position ( 'atEquipment' ) . value ( specificEnergyConsumption ) ;
if ( pressureDiff ? . value != null && flowM3s != null && powerWatt != null ) {
const meterPerBar = pressureDiff . value / rho * g ;
const nHydraulicEfficiency = rho * g * flowM3s * ( pressureDiff . value * meterPerBar ) / powerWatt ;
this . measurements . type ( "nHydraulicEfficiency" ) . variant ( variant ) . position ( 'atEquipment' ) . value ( nHydraulicEfficiency ) ;
}
}
2025-06-25 17:26:13 +02:00
2025-11-12 17:40:38 +01:00
//change this to nhydrefficiency ?
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
2025-11-05 15:47:39 +01:00
2025-11-28 09:59:51 +01:00
const output = this . measurements . getFlattenedOutput ( ) ;
2025-06-25 17:26:13 +02:00
//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 ;
2025-11-05 15:47:39 +01:00
output [ "maintenanceTime" ] = this . state . getMaintenanceTimeHours ( ) ;
2025-06-25 17:26:13 +02:00
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 ;