/** * @summary A class to interact and manipulate machines with a non-euclidian curve * @description A class to interact and manipulate machines with a non-euclidian curve * @module machineGroup * @exports machineGroup * @version 0.1.0 * @since 0.1.0 * * Author: * - Rene De Ren * Email: * - r.de.ren@brabantsedelta.nl */ //load local dependencies const EventEmitter = require("events"); const {logger,configUtils,configManager, MeasurementContainer, interpolation , childRegistrationUtils} = require('generalFunctions'); class MachineGroup { constructor(machineGroupConfig = {}) { this.emitter = new EventEmitter(); // Own EventEmitter this.configManager = new configManager(); // Config manager to handle dynamic config loading this.defaultConfig = this.configManager.getConfig('machineGroupControl'); // Load default config for rotating machine ( use software type name ? ) this.configUtils = new configUtils(this.defaultConfig);// this will handle the config endpoints so we can load them dynamically this.config = this.configUtils.initConfig(machineGroupConfig); // verify and set the config for the machine group // Init after config is set this.logger = new logger(this.config.general.logging.enabled,this.config.general.logging.logLevel, this.config.general.name); // Initialize measurements this.measurements = new MeasurementContainer(); this.interpolation = new interpolation(); // Machines and child data this.machines = {}; this.child = {}; this.scaling = this.config.scaling.current; this.mode = this.config.mode.current; this.absDistFromPeak = 0 ; this.relDistFromPeak = 0; // Combination curve data this.dynamicTotals = { flow: { min: Infinity, max: 0 }, power: { min: Infinity, max: 0 } , NCog : 0}; this.absoluteTotals = { flow: { min: Infinity, max: 0 }, power: { min: Infinity, max: 0 }}; //this always last in the constructor this.childRegistrationUtils = new childRegistrationUtils(this); this.logger.info("MachineGroup initialized."); } registerChild(child,softwareType) { this.logger.debug('Setting up childs specific for this class'); if(softwareType == "machine"){ // Check if the machine is already registered this.machines[child.config.general.id] === undefined ? this.machines[child.config.general.id] = child : this.logger.warn(`Machine ${child.config.general.id} is already registered.`); //listen for machine pressure changes this.logger.debug(`Listening for pressure changes from machine ${child.config.general.id}`); child.measurements.emitter.on("pressure.measured.differential", (eventData) => { this.logger.debug(`Pressure update from ${child.config.general.id}: ${eventData.value} ${eventData.unit}`); this.handlePressureChange(); }); child.measurements.emitter.on("pressure.measured.downstream", (eventData) => { this.logger.debug(`Pressure update from ${child.config.general.id}: ${eventData.value} ${eventData.unit}`); this.handlePressureChange(); }); child.measurements.emitter.on("flow.predicted.downstream", (eventData) => { this.logger.debug(`Flow prediction update from ${child.config.general.id}: ${eventData.value} ${eventData.unit}`); //later change to this.handleFlowPredictionChange(); this.handlePressureChange(); }); } } calcAbsoluteTotals() { const absoluteTotals = { flow: { min: Infinity, max: 0 }, power: { min: Infinity, max: 0 } }; Object.values(this.machines).forEach(machine => { const totals = { flow: { min: Infinity, max: 0 }, power: { min: Infinity, max: 0 } }; //fetch min flow ever seen over all machines Object.entries(machine.predictFlow.inputCurve).forEach(([pressure, xyCurve], index) => { const minFlow = Math.min(...xyCurve.y); const maxFlow = Math.max(...xyCurve.y); const minPower = Math.min(...machine.predictPower.inputCurve[pressure].y); const maxPower = Math.max(...machine.predictPower.inputCurve[pressure].y); // min ever seen for 1 machine if (minFlow < totals.flow.min) { totals.flow.min = minFlow; } if (minPower < totals.power.min) { totals.power.min = minPower; } if( maxFlow > totals.flow.max ){ totals.flow.max = maxFlow; } if( maxPower > totals.power.max ){ totals.power.max = maxPower; } }); //surplus machines for max flow and power if( totals.flow.min < absoluteTotals.flow.min ){ absoluteTotals.flow.min = totals.flow.min; } if( totals.power.min < absoluteTotals.power.min ){ absoluteTotals.power.min = totals.power.min; } absoluteTotals.flow.max += totals.flow.max; absoluteTotals.power.max += totals.power.max; }); if(absoluteTotals.flow.min === Infinity) { this.logger.warn(`Flow min ${absoluteTotals.flow.min} is Infinity. Setting to 0.`); absoluteTotals.flow.min = 0; } if(absoluteTotals.power.min === Infinity) { this.logger.warn(`Power min ${absoluteTotals.power.min} is Infinity. Setting to 0.`); absoluteTotals.power.min = 0; } if(absoluteTotals.flow.max === -Infinity) { this.logger.warn(`Flow max ${absoluteTotals.flow.max} is -Infinity. Setting to 0.`); absoluteTotals.flow.max = 0; } if(absoluteTotals.power.max === -Infinity) { this.logger.warn(`Power max ${absoluteTotals.power.max} is -Infinity. Setting to 0.`); absoluteTotals.power.max = 0; } // Place data in object for external use this.absoluteTotals = absoluteTotals; return absoluteTotals; } //max and min current flow and power based on their actual pressure curve calcDynamicTotals() { const dynamicTotals = { flow: { min: Infinity, max: 0, act: 0 }, power: { min: Infinity, max: 0, act: 0 }, NCog : 0 }; this.logger.debug(`\n --------- Calculating dynamic totals for ${Object.keys(this.machines).length} machines. @ current pressure settings : ----------`); Object.values(this.machines).forEach(machine => { this.logger.debug(`Processing machine with id: ${machine.config.general.id}`); this.logger.debug(`Current pressure settings: ${JSON.stringify(machine.predictFlow.currentF)}`); //fetch min flow ever seen over all machines const minFlow = machine.predictFlow.currentFxyYMin; const maxFlow = machine.predictFlow.currentFxyYMax; const minPower = machine.predictPower.currentFxyYMin; const maxPower = machine.predictPower.currentFxyYMax; const actFlow = machine.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue(); const actPower = machine.measurements.type("power").variant("predicted").position("atEquipment").getCurrentValue(); this.logger.debug(`Machine ${machine.config.general.id} - Min Flow: ${minFlow}, Max Flow: ${maxFlow}, Min Power: ${minPower}, Max Power: ${maxPower}, NCog: ${machine.NCog}`); if( minFlow < dynamicTotals.flow.min ){ dynamicTotals.flow.min = minFlow; } if( minPower < dynamicTotals.power.min ){ dynamicTotals.power.min = minPower; } dynamicTotals.flow.max += maxFlow; dynamicTotals.power.max += maxPower; dynamicTotals.flow.act += actFlow; dynamicTotals.power.act += actPower; //fetch total Normalized Cog over all machines dynamicTotals.NCog += machine.NCog; }); // Place data in object for external use this.dynamicTotals = dynamicTotals; return dynamicTotals; } activeTotals() { const totals = { flow: { min: 0, max: 0 }, power: { min: 0, max: 0 }, countActiveMachines: 0 }; Object.entries(this.machines).forEach(([id, machine]) => { this.logger.debug(`Processing machine with id: ${id}`); if(this.isMachineActive(id)){ //fetch min flow ever seen over all machines const minFlow = machine.predictFlow.currentFxyYMin; const maxFlow = machine.predictFlow.currentFxyYMax; const minPower = machine.predictPower.currentFxyYMin; const maxPower = machine.predictPower.currentFxyYMax; totals.flow.min += minFlow; totals.flow.max += maxFlow; totals.power.min += minPower; totals.power.max += maxPower; totals.countActiveMachines++; } }); return totals; } handlePressureChange() { this.logger.info("---------------------->>>>>>>>>>>>>>>>>>>>>>>>>>>Pressure change detected."); // Recalculate totals const { flow, power } = this.calcDynamicTotals(); this.logger.debug(`Dynamic Totals after pressure change - Flow: Min ${flow.min}, Max ${flow.max}, Act ${flow.act} | Power: Min ${power.min}, Max ${power.max}, Act ${power.act}`); this.measurements.type("flow").variant("predicted").position("downstream").value(flow.act); this.measurements.type("power").variant("predicted").position("atEquipment").value(power.act); const { maxEfficiency, lowestEfficiency } = this.calcGroupEfficiency(this.machines); const efficiency = this.measurements.type("efficiency").variant("predicted").position("atEquipment").getCurrentValue(); this.calcDistanceBEP(efficiency,maxEfficiency,lowestEfficiency); } 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; } 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 }; } checkSpecialCases(machines, Qd) { Object.values(machines).forEach(machine => { const state = machine.state.getCurrentState(); const mode = machine.currentMode; //add special cases if( state === "operational" && ( mode == "virtualControl" || mode === "fysicalControl") ){ let flow = 0; if(machine.measurements.type("flow").variant("measured").position("downstream").getCurrentValue()){ flow = machine.measurements.type("flow").variant("measured").position("downstream").getCurrentValue(); } else if(machine.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue()){ flow = machine.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue(); } else{ this.logger.error("Dont perform calculation at all seeing that there is a machine working but we dont know the flow its producing"); //abort the calculation return false; } //Qd is less because we allready have machines delivering flow on manual control Qd = Qd - flow; } }); return Qd ; } validPumpCombinations(machines, Qd, PowerCap = Infinity) { let subsets = [[]]; // adjust demand flow when there are machines being controlled by a manual source Qd = this.checkSpecialCases(machines, Qd); // Generate all possible subsets of machines (power set) Object.keys(machines).forEach(machineId => { const state = machines[machineId].state.getCurrentState(); const validActionForMode = machines[machineId].isValidActionForMode("execSequence", "auto"); // Reasons why a machine is not valid for the combination if( state === "off" || state === "coolingdown" || state === "stopping" || state === "emergencystop" || !validActionForMode){ return; } // go through each machine and add it to the subsets let newSubsets = subsets.map(set => [...set, machineId]); subsets = subsets.concat(newSubsets); }); // Filter for non-empty subsets that can meet or exceed demand flow const combinations = subsets.filter(subset => { if (subset.length === 0) return false; // Calculate total and minimum flow for the subset in one pass const { maxFlow, minFlow, maxPower } = subset.reduce( (acc, machineId) => { const machine = machines[machineId]; const minFlow = machine.predictFlow.currentFxyYMin; const maxFlow = machine.predictFlow.currentFxyYMax; const maxPower = machine.predictPower.currentFxyYMax; return { maxFlow: acc.maxFlow + maxFlow, minFlow: acc.minFlow + minFlow, maxPower: acc.maxPower + maxPower }; }, { maxFlow: 0, minFlow: 0 , maxPower: 0 } ); // If total flow can deliver the demand if(maxFlow >= Qd && minFlow <= Qd && maxPower <= PowerCap){ return true; } else{ return false; } }); return combinations; } calcBestCombination(combinations, Qd) { let bestCombination = null; //keep track of totals let bestPower = Infinity; let bestFlow = 0; let bestCog = 0; combinations.forEach(combination => { let flowDistribution = []; // Stores the flow distribution for the best combination let totalCoG = 0; let totalPower = 0; let totalFlow = 0; // Calculate the total CoG for the current combination combination.forEach(machineId => { totalCoG += ( Math.round(this.machines[machineId].NCog * 100 ) /100 ) ; }); // Calculate the total power for the current combination combination.forEach(machineId => { let flow = 0; // Prevent division by zero if (totalCoG === 0) { // Distribute flow equally among all pumps flow = Qd / combination.length; } else { // Normal CoG-based distribution flow = (this.machines[machineId].NCog / totalCoG) * Qd ; this.logger.debug(`Machine Normalized CoG-based distribution ${machineId} flow: ${flow}`); } totalFlow += flow; totalPower += this.machines[machineId].inputFlowCalcPower(flow); flowDistribution.push({ machineId: machineId,flow: flow }); }); // Update the best combination if the current one is better if (totalPower < bestPower) { this.logger.debug(`New best combination found: ${totalPower} < ${bestPower}`); this.logger.debug(`combination ${JSON.stringify(flowDistribution)}`); bestPower = totalPower; bestFlow = totalFlow; bestCog = totalCoG; bestCombination = flowDistribution; } }); return { bestCombination, bestPower, bestFlow, bestCog }; } // -------- Mode and Input Management -------- // isValidActionForMode(action, mode) { const allowedActionsSet = this.config.mode.allowedActions[mode] || []; return allowedActionsSet.has(action); } setScaling(scaling) { const scalingSet = new Set(this.defaultConfig.scaling.current.rules.values.map( (value) => value.value)); scalingSet.has(scaling)? this.scaling = scaling : this.logger.warn(`${scaling} is not a valid scaling option.`); this.logger.debug(`Scaling set to: ${scaling}`); } async abortActiveMovements(reason = "new demand") { await Promise.all(Object.values(this.machines).map(async machine => { this.logger.warn(`Aborting active movements for machine ${machine.config.general.id} due to: ${reason}`); if (typeof machine.abortMovement === "function") { await machine.abortMovement(reason); } })); } //handle input from parent / user / UI async optimalControl(Qd, powerCap = Infinity) { try{ //we need to force the pressures of all machines to be equal to the highest pressure measured in the group // this is to ensure a correct evaluation of the flow and power consumption const pressures = Object.entries(this.machines).map(([machineId, machine]) => { return { downstream: machine.measurements.type("pressure").variant("measured").position("downstream").getCurrentValue(), upstream: machine.measurements.type("pressure").variant("measured").position("upstream").getCurrentValue() }; }); const maxDownstream = Math.max(...pressures.map(p => p.downstream)); const minUpstream = Math.min(...pressures.map(p => p.upstream)); this.logger.debug(`Max downstream pressure: ${maxDownstream}, Min upstream pressure: ${minUpstream}`); //set the pressures Object.entries(this.machines).forEach(([machineId, machine]) => { if(machine.state.getCurrentState() !== "operational" && machine.state.getCurrentState() !== "accelerating" && machine.state.getCurrentState() !== "decelerating"){ //Equilize pressures over all machines so we can make a proper calculation machine.measurements.type("pressure").variant("measured").position("downstream").value(maxDownstream); machine.measurements.type("pressure").variant("measured").position("upstream").value(minUpstream); // after updating the measurement directly we need to force the update of the value OLIFANT this is not so clear now in the code // we need to find a better way to do this but for now it works machine.getMeasuredPressure(); } }); //fetch dynamic totals const dynamicTotals = this.dynamicTotals; const machineStates = Object.entries(this.machines).reduce((acc, [machineId, machine]) => { acc[machineId] = machine.state.getCurrentState(); return acc; }, {}); if( Qd <= 0 ) { //if Qd is 0 turn all machines off and exit early } if( Qd < dynamicTotals.flow.min && Qd > 0 ){ //Capping Qd to lowest possible value this.logger.warn(`Flow demand ${Qd} is below minimum possible flow ${dynamicTotals.flow.min}. Capping to minimum flow.`); Qd = dynamicTotals.flow.min; } else if( Qd > dynamicTotals.flow.max ){ //Capping Qd to highest possible value this.logger.warn(`Flow demand ${Qd} is above maximum possible flow ${dynamicTotals.flow.max}. Capping to maximum flow.`); Qd = dynamicTotals.flow.max; } // fetch all valid combinations that meet expectations const combinations = this.validPumpCombinations(this.machines, Qd, powerCap); const bestResult = this.calcBestCombination(combinations, Qd); if(bestResult.bestCombination === null){ this.logger.warn(`Demand: ${Qd.toFixed(2)} -> No valid combination found => not updating control `); return; } const debugInfo = bestResult.bestCombination.map(({ machineId, flow }) => `${machineId}: ${flow.toFixed(2)} units`).join(" | "); this.logger.debug(`Moving to demand: ${Qd.toFixed(2)} -> Pumps: [${debugInfo}] => Total Power: ${bestResult.bestPower.toFixed(2)}`); //store the total delivered power this.measurements.type("power").variant("predicted").position("atEquipment").value(bestResult.bestPower); this.measurements.type("flow").variant("predicted").position("downstream").value(bestResult.bestFlow); this.measurements.type("efficiency").variant("predicted").position("atEquipment").value(bestResult.bestFlow / bestResult.bestPower); this.measurements.type("Ncog").variant("predicted").position("atEquipment").value(bestResult.bestCog); await Promise.all(Object.entries(this.machines).map(async ([machineId, machine]) => { // Find the flow for this machine in the best combination this.logger.debug(`Searching for machine ${machineId} with state ${machineStates[machineId]} in best combination.`); const pumpInfo = bestResult.bestCombination.find(item => item.machineId == machineId); let flow; if(pumpInfo !== undefined){ flow = pumpInfo.flow; } else { this.logger.debug(`Machine ${machineId} not in best combination, setting flow control to 0`); flow = 0; } if( (flow <= 0 ) && ( machineStates[machineId] === "operational" || machineStates[machineId] === "accelerating" || machineStates[machineId] === "decelerating" ) ){ await machine.handleInput("parent", "execSequence", "shutdown"); } if(machineStates[machineId] === "idle" && flow > 0){ await machine.handleInput("parent", "execSequence", "startup"); await machine.handleInput("parent", "flowMovement", flow); } if(machineStates[machineId] === "operational" && flow > 0 ){ await machine.handleInput("parent", "flowMovement", flow); } })); } catch(err){ this.logger.error(err); } } // Equalize pressure across all machines for machines that are not running. This is needed to ensure accurate flow and power predictions. equalizePressure(){ // Get current pressures from all machines const pressures = Object.entries(this.machines).map(([machineId, machine]) => { return { downstream: machine.measurements.type("pressure").variant("measured").position("downstream").getCurrentValue(), upstream: machine.measurements.type("pressure").variant("measured").position("upstream").getCurrentValue() }; }); // Find the highest downstream and lowest upstream pressure const maxDownstream = Math.max(...pressures.map(p => p.downstream)); const minUpstream = Math.min(...pressures.map(p => p.upstream)); // Set consistent pressures across machines Object.entries(this.machines).forEach(([machineId, machine]) => { if(!this.isMachineActive(machineId)){ machine.measurements.type("pressure").variant("measured").position("downstream").value(maxDownstream); machine.measurements.type("pressure").variant("measured").position("upstream").value(minUpstream); // Update the measured pressure value const pressure = machine.getMeasuredPressure(); this.logger.debug(`Setting pressure for machine ${machineId} to ${pressure}`); } }); } isMachineActive(machineId){ if(this.machines[machineId].state.getCurrentState() === "operational" || this.machines[machineId].state.getCurrentState() === "accelerating" || this.machines[machineId].state.getCurrentState() === "decelerating"){ return true; } return false; } capFlowDemand(Qd,dynamicTotals){ if (Qd < dynamicTotals.flow.min && Qd > 0) { this.logger.warn(`Flow demand ${Qd} is below minimum possible flow ${dynamicTotals.flow.min}. Capping to minimum flow.`); Qd = dynamicTotals.flow.min; } else if (Qd > dynamicTotals.flow.max) { this.logger.warn(`Flow demand ${Qd} is above maximum possible flow ${dynamicTotals.flow.max}. Capping to maximum flow.`); Qd = dynamicTotals.flow.max; } return Qd; } sortMachinesByPriority(priorityList) { let machinesInPriorityOrder; if (priorityList && Array.isArray(priorityList)) { machinesInPriorityOrder = priorityList .filter(id => this.machines[id]) .map(id => ({ id, machine: this.machines[id] })); } else { machinesInPriorityOrder = Object.entries(this.machines) .map(([id, machine]) => ({ id: id, machine })) .sort((a, b) => a.id - b.id); } return machinesInPriorityOrder; } filterOutUnavailableMachines(list) { const newList = list.filter(({ id, machine }) => { const state = machine.state.getCurrentState(); const validActionForMode = machine.isValidActionForMode("execSequence", "auto"); return !(state === "off" || state === "coolingdown" || state === "stopping" || state === "emergencystop" || !validActionForMode); }); return newList; } calcGroupEfficiency(machines){ let cumEfficiency = 0; let machineCount = 0; let lowestEfficiency = Infinity; // Calculate the average efficiency of all machines -> peak is the average of them all Object.entries(machines).forEach(([machineId, machine]) => { cumEfficiency += machine.cog; if(machine.cog < lowestEfficiency){ lowestEfficiency = machine.cog; } machineCount++; }); const maxEfficiency = cumEfficiency / machineCount; return { maxEfficiency, lowestEfficiency }; } //move machines assuming equal control in flow and a priority list async equalFlowControl(Qd, powerCap = Infinity, priorityList = null) { try { // equalize pressure across all machines this.equalizePressure(); // Update dynamic totals const dynamicTotals = this.calcDynamicTotals(); // Cap flow demand to min/max possible values Qd = this.capFlowDemand(Qd,dynamicTotals); // Get machines sorted by priority let machinesInPriorityOrder = this.sortMachinesByPriority(priorityList); // Filter out machines that are unavailable for control machinesInPriorityOrder = this.filterOutUnavailableMachines(machinesInPriorityOrder); // Initialize flow distribution let flowDistribution = []; let totalFlow = 0; let totalPower = 0; let totalCog = 0; const activeTotals = this.activeTotals(); // Distribute flow equally among all available machines switch (true) { case (Qd < activeTotals.flow.min && activeTotals.flow.min !== 0):{ let availableFlow = activeTotals.flow.min; for (let i = machinesInPriorityOrder.length - 1; i >= 0 && availableFlow > Qd; i--) { const machine = machinesInPriorityOrder[i]; if (this.isMachineActive(machine.id)) { flowDistribution.push({ machineId: machine.id, flow: 0 }); availableFlow -= machine.machine.predictFlow.currentFxyYMin; } } // Determine remaining active machines (not shut down). const remainingMachines = machinesInPriorityOrder.filter( ({ id }) => this.isMachineActive(id) && !flowDistribution.some(item => item.machineId === id) ); // Evenly distribute Qd among the remaining machines. const distributedFlow = Qd / remainingMachines.length; for (let machine of remainingMachines) { flowDistribution.push({ machineId: machine.id, flow: distributedFlow }); totalFlow += distributedFlow; totalPower += machine.machine.inputFlowCalcPower(distributedFlow); } break; } case (Qd > activeTotals.flow.max): // Case 2: Demand is above the maximum available flow. // Start the non-active machine with the highest priority and distribute Qd over all available machines. let i = 1; while (totalFlow < Qd && i <= machinesInPriorityOrder.length) { Qd = Qd / i; if(machinesInPriorityOrder[i-1].machine.predictFlow.currentFxyYMax >= Qd){ for ( let i2 = 0; i2 < i ; i2++){ if(! this.isMachineActive(machinesInPriorityOrder[i2].id)){ flowDistribution.push({ machineId: machinesInPriorityOrder[i2].id, flow: Qd }); totalFlow += Qd; totalPower += machinesInPriorityOrder[i2].machine.inputFlowCalcPower(Qd); } } } i++; } break; default: // Default case: Demand is within the active range. const countActiveMachines = machinesInPriorityOrder.filter(({ id }) => this.isMachineActive(id)).length; Qd /= countActiveMachines; // Simply distribute the demand equally among all available machines. for ( let i = 0 ; i < countActiveMachines ; i++){ flowDistribution.push({ machineId: machinesInPriorityOrder[i].id, flow: Qd}); totalFlow += Qd ; totalPower += machinesInPriorityOrder[i].machine.inputFlowCalcPower(Qd); } break; } // Log information about flow distribution const debugInfo = flowDistribution .filter(({ flow }) => flow > 0) .map(({ machineId, flow }) => `${machineId}: ${flow.toFixed(2)} units`) .join(" | "); this.logger.debug(`Priority control for demand: ${totalFlow.toFixed(2)} -> Active pumps: [${debugInfo}] => Total Power: ${totalPower.toFixed(2)}`); // Store measurements this.measurements.type("power").variant("predicted").position("atEquipment").value(totalPower); this.measurements.type("flow").variant("predicted").position("downstream").value(totalFlow); this.measurements.type("efficiency").variant("predicted").position("atEquipment").value(totalFlow / totalPower); this.measurements.type("Ncog").variant("predicted").position("atEquipment").value(totalCog); this.logger.debug(`Flow distribution: ${JSON.stringify(flowDistribution)}`); // Apply the flow distribution to machines await Promise.all(flowDistribution.map(async ({ machineId, flow }) => { const machine = this.machines[machineId]; this.logger.debug(this.machines[machineId].state); const currentState = this.machines[machineId].state.getCurrentState(); if (flow <= 0 && (currentState === "operational" || currentState === "accelerating" || currentState === "decelerating")) { await machine.handleInput("parent", "execSequence", "shutdown"); } else if (currentState === "idle" && flow > 0) { await machine.handleInput("parent", "execSequence", "startup"); } else if (currentState === "operational" && flow > 0) { await machine.handleInput("parent", "flowMovement", flow); } })); } catch (err) { this.logger.error(err); } } //only valid with equal machines async prioPercentageControl(input, priorityList = null) { try{ // stop all machines if input is negative if(input < 0 ){ //turn all machines off await Promise.all(Object.entries(this.machines).map(async ([machineId, machine]) => { if (this.isMachineActive(machineId)) { await machine.handleInput("parent", "execSequence", "shutdown"); } })); return; } //capp input to 100 input > 100 ? input = 100 : input = input; const numOfMachines = Object.keys(this.machines).length; const procentTotal = numOfMachines * input; const machinesNeeded = Math.ceil(procentTotal/100); const activeTotals = this.activeTotals(); const machinesActive = activeTotals.countActiveMachines; // Get machines sorted by priority let machinesInPriorityOrder = this.sortMachinesByPriority(priorityList); const ctrlDistribution = []; //{machineId : 0, flow : 0} push for each machine if(machinesNeeded > machinesActive){ //start extra machine and put all active machines at min control machinesInPriorityOrder.forEach(({ id, machine }, index) => { if(index < machinesNeeded){ ctrlDistribution.push({machineId : id, ctrl : 0}); } }); } if(machinesNeeded < machinesActive){ machinesInPriorityOrder.forEach(({ id, machine }, index) => { if(this.isMachineActive(id)){ if(index < machinesNeeded){ ctrlDistribution.push({machineId : id, ctrl : 100}); } else{ //turn machine off ctrlDistribution.push({machineId : id, ctrl : -1}); } } }); } if (machinesNeeded === machinesActive) { // distribute input equally among active machines (0 - 100%) const ctrlPerMachine = procentTotal / machinesActive; machinesInPriorityOrder.forEach(({ id, machine }) => { if (this.isMachineActive(id)) { // ensure ctrl is capped between 0 and 100% const ctrlValue = Math.max(0, Math.min(ctrlPerMachine, 100)); ctrlDistribution.push({ machineId: id, ctrl: ctrlValue }); } }); } const debugInfo = ctrlDistribution.map(({ machineId, ctrl }) => `${machineId}: ${ctrl.toFixed(2)}%`).join(" | "); this.logger.debug(`Priority control for input: ${input.toFixed(2)} -> Active pumps: [${debugInfo}]`); // Apply the ctrl distribution to machines await Promise.all(ctrlDistribution.map(async ({ machineId, ctrl }) => { const machine = this.machines[machineId]; const currentState = this.machines[machineId].state.getCurrentState(); if (ctrl < 0 && (currentState === "operational" || currentState === "accelerating" || currentState === "decelerating")) { await machine.handleInput("parent", "execSequence", "shutdown"); } else if (currentState === "idle" && ctrl >= 0) { await machine.handleInput("parent", "execSequence", "startup"); } else if (currentState === "operational" && ctrl > 0) { await machine.handleInput("parent", "execMovement", ctrl); } })); const totalPower = []; const totalFlow = []; // fetch and store measurements Object.entries(this.machines).forEach(([machineId, machine]) => { const powerValue = machine.measurements.type("power").variant("predicted").position("atEquipment").getCurrentValue(); const flowValue = machine.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue(); if (powerValue !== null) { totalPower.push(powerValue); } if (flowValue !== null) { totalFlow.push(flowValue); } }); this.measurements.type("power").variant("predicted").position("atEquipment").value(totalPower.reduce((a, b) => a + b, 0)); this.measurements.type("flow").variant("predicted").position("downstream").value(totalFlow.reduce((a, b) => a + b, 0)); if(totalPower.reduce((a, b) => a + b, 0) > 0){ this.measurements.type("efficiency").variant("predicted").position("atEquipment").value(totalFlow.reduce((a, b) => a + b, 0) / totalPower.reduce((a, b) => a + b, 0)); } } catch(err){ this.logger.error(err); } } async handleInput(source, demand, powerCap = Infinity, priorityList = null) { //abort current movements await this.abortActiveMovements("new demand received"); const scaling = this.scaling; const mode = this.mode; const dynamicTotals = this.calcDynamicTotals(); const demandQ = parseFloat(demand); let demandQout = 0; // keep output Q by default 0 for safety this.logger.debug(`Handling input from ${source}: Demand = ${demand}, Power Cap = ${powerCap}, Priority List = ${priorityList}`); switch (scaling) { case "absolute": if (isNaN(demandQ)) { this.logger.warn(`Invalid absolute flow demand: ${demand}. Must be a number.`); demandQout = 0; return; } if (demandQ < absoluteTotals.flow.min) { this.logger.warn(`Flow demand ${demandQ} is below minimum possible flow ${absoluteTotals.flow.min}. Capping to minimum flow.`); demandQout = this.absoluteTotals.flow.min; } else if (demandQout > absoluteTotals.flow.max) { this.logger.warn(`Flow demand ${demandQ} is above maximum possible flow ${absoluteTotals.flow.max}. Capping to maximum flow.`); demandQout = absoluteTotals.flow.max; }else if(demandQout <= 0){ this.logger.debug(`Turning machines off`); demandQout = 0; //return early and turn all machines off this.turnOffAllMachines(); return; } break; case "normalized": this.logger.debug(`Normalizing flow demand: ${demandQ} with min: ${dynamicTotals.flow.min} and max: ${dynamicTotals.flow.max}`); if(demand < 0){ this.logger.debug(`Turning machines off`); demandQout = 0; //return early and turn all machines off this.turnOffAllMachines(); return; } else{ // Scale demand to 0-100% linear between min and max flow this is auto capped demandQout = this.interpolation.interpolate_lin_single_point(demandQ, 0, 100, dynamicTotals.flow.min, dynamicTotals.flow.max ); this.logger.debug(`Normalized flow demand ${demandQ}% to: ${demandQout} Q units`); } break; } // Execute control based on mode switch(mode) { case "prioritycontrol": this.logger.debug(`Calculating prio control. Input flow demand: ${demandQ} scaling : ${scaling} -> ${demandQout}`); await this.equalFlowControl(demandQout,powerCap,priorityList); break; case "prioritypercentagecontrol": this.logger.debug(`Calculating prio percentage control. Input flow demand: ${demandQ} scaling : ${scaling} -> ${demandQout}`); if(scaling !== "normalized"){ this.logger.warn("Priority percentage control is only valid with normalized scaling."); return; } await this.prioPercentageControl(demandQout,priorityList); break; case "optimalcontrol": this.logger.debug(`Calculating optimal control. Input flow demand: ${demandQ} scaling : ${scaling} -> ${demandQout}`); await this.optimalControl(demandQout,powerCap); break; default: this.logger.warn(`${mode} is not a valid mode.`); break; } //recalc distance from BEP const { maxEfficiency, lowestEfficiency } = this.calcGroupEfficiency(this.machines); const efficiency = this.measurements.type("efficiency").variant("predicted").position("downstream").getCurrentValue(); this.calcDistanceBEP(efficiency,maxEfficiency,lowestEfficiency); } async turnOffAllMachines(){ await Promise.all(Object.entries(this.machines).map(async ([machineId, machine]) => { if (this.isMachineActive(machineId)) { await machine.handleInput("parent", "execSequence", "shutdown"); } })); } setMode(mode) { this.mode = mode; } 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 atEquipmentVal = this.measurements.type(type).variant(variant).position("atEquipment").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 (atEquipmentVal != null) { output[`atEquipment_${variant}_${type}`] = atEquipmentVal; } 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["mode"] = this.mode; output["scaling"] = this.scaling; output["flow"] = this.flow; output["power"] = this.power; output["NCog"] = this.NCog; // normalized cog output["absDistFromPeak"] = this.absDistFromPeak; output["relDistFromPeak"] = this.relDistFromPeak; //this.logger.debug(`Output: ${JSON.stringify(output)}`); return output; } } module.exports = MachineGroup; /* const Machine = require('../../rotatingMachine/src/specificClass'); const Measurement = require('../../measurement/src/specificClass'); const specs = require('../../generalFunctions/datasets/assetData/curves/hidrostal-H05K-S03R.json'); const { max } = require("mathjs"); function createBaseMachineConfig(machineNum, name,specs) { return { general: { logging: { enabled: true, logLevel: "debug" }, name: name, id: machineNum, unit: "m3/h" }, functionality: { softwareType: "machine", role: "rotationaldevicecontroller" }, asset: { category: "pump", type: "centrifugal", model: "hidrostal-h05k-s03r", supplier: "hydrostal", machineCurve: specs }, mode: { current: "auto", allowedActions: { auto: ["execSequence", "execMovement", "statusCheck"], virtualControl: ["execMovement", "statusCheck"], fysicalControl: ["statusCheck"] }, allowedSources: { auto: ["parent", "GUI"], virtualControl: ["GUI"], fysicalControl: ["fysical"] } }, sequences: { startup: ["starting", "warmingup", "operational"], shutdown: ["stopping", "coolingdown", "idle"], emergencystop: ["emergencystop", "off"], boot: ["idle", "starting", "warmingup", "operational"] } }; } function createStateConfig(){ return { time:{ starting: 1, stopping: 1, warmingup: 1, coolingdown: 1, emergencystop: 1 }, movement:{ mode:"dynspeed", speed:100, maxSpeed: 1000 } } }; function createBaseMachineGroupConfig(name) { return { general: { logging: { enabled: true, logLevel: "debug" }, name: name }, functionality: { softwareType: "machinegroup", role: "groupcontroller" }, scaling: { current: "normalized" }, mode: { current: "optimalControl" } }; } const machineGroupConfig = createBaseMachineGroupConfig("testmachinegroup"); const stateConfigs = {}; const machineConfigs = {}; stateConfigs[1] = createStateConfig(); stateConfigs[2] = createStateConfig(); machineConfigs[1]= createBaseMachineConfig("asdfkj;asdf","testmachine",specs); machineConfigs[2] = createBaseMachineConfig("asdfkj;asdf2","testmachine2",specs); const ptConfig = { general: { logging: { enabled: true, logLevel: "debug" }, name: "testpt", id: "0", unit: "mbar", }, functionality: { softwareType: "measurement", role: "sensor" }, asset: { category: "sensor", type: "pressure", model: "testmodel", supplier: "vega" }, scaling:{ absMin:0, absMax: 4000, } } async function makeMachines(){ const mg = new MachineGroup(machineGroupConfig); const pt1 = new Measurement(ptConfig); const numofMachines = 2; for(let i = 1; i <= numofMachines; i++){ const machine = new Machine(machineConfigs[i],stateConfigs[i]); //mg.machines[i] = machine; mg.childRegistrationUtils.registerChild(machine, "downstream"); } Object.keys(mg.machines).forEach(machineId => { mg.machines[machineId].childRegistrationUtils.registerChild(pt1, "downstream"); }); mg.setMode("prioritycontrol"); mg.setScaling("normalized"); const absMax = mg.dynamicTotals.flow.max; const absMin = mg.dynamicTotals.flow.min; const percMin = 0; const percMax = 100; try{ /* for(let demand = mg.dynamicTotals.flow.min ; demand <= mg.dynamicTotals.flow.max ; demand += 2){ //set pressure console.log("------------------------------------"); await mg.handleInput("parent",demand); pt1.calculateInput(1400); //await new Promise(resolve => setTimeout(resolve, 200)); console.log("------------------------------------"); } for(let demand = 240 ; demand >= mg.dynamicTotals.flow.min ; demand -= 40){ //set pressure console.log("------------------------------------"); await mg.handleInput("parent",demand); pt1.calculateInput(1400); //await new Promise(resolve => setTimeout(resolve, 200)); console.log("------------------------------------"); } //*//* for(let demand = 0 ; demand <= 50 ; demand += 1){ //set pressure console.log(`TESTING: processing demand of ${demand}`); await mg.handleInput("parent",demand); Object.keys(mg.machines).forEach(machineId => { console.log(mg.machines[machineId].state.getCurrentState()); }); console.log(`updating pressure to 1400 mbar`); pt1.calculateInput(1400); console.log("------------------------------------"); } } catch(err){ console.log(err); } } makeMachines(); //*/