/** * @file machine.js * * Permission is hereby granted to any person obtaining a copy of this software * and associated documentation files (the "Software"), to use it for personal * or non-commercial purposes, with the following restrictions: * * 1. **No Copying or Redistribution**: The Software or any of its parts may not * be copied, merged, distributed, sublicensed, or sold without explicit * prior written permission from the author. * * 2. **Commercial Use**: Any use of the Software for commercial purposes requires * a valid license, obtainable only with the explicit consent of the author. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, * OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. * * Ownership of this code remains solely with the original author. Unauthorized * use of this Software is strictly prohibited. * * @summary A class to interact and manipulate machines with a non-euclidian curve * @description A class to interact and manipulate machines with a non-euclidian curve * @module machineGroup * @exports machineGroup * @version 0.1.0 * @since 0.1.0 * * Author: * - Rene De Ren * Email: * - rene@thegoldenbasket.nl */ //load local dependencies const EventEmitter = require("events"); const Logger = require('../../../generalFunctions/helper/logger'); const Interpolation = require('../../../predict/dependencies/predict/interpolation'); const { MeasurementContainer } = require('../../../generalFunctions/helper/measurements/index'); //load all config modules const defaultConfig = require('./machineGroupConfig.json'); const ConfigUtils = require('../../../generalFunctions/helper/configUtils'); //load registration utility const ChildRegistrationUtils = require('../../../generalFunctions/helper/childRegistrationUtils'); class MachineGroup { constructor(machineGroupConfig = {}) { this.emitter = new EventEmitter(); // Own EventEmitter this.configUtils = new ConfigUtils(defaultConfig); this.config = this.configUtils.initConfig(machineGroupConfig); // 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 children 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."); } // when a child gets updated do something handleChildChange() { this.absoluteTotals = this.calcAbsoluteTotals(); //for reference and not to recalc these values continiously this.dynamicTotals = this.calcDynamicTotals(); } 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; }); return absoluteTotals; } //max and min current flow and power based on their actual pressure curve calcDynamicTotals() { const dynamicTotals = { flow: { min: Infinity, max: 0 }, power: { min: Infinity, max: 0 }, NCog : 0 }; Object.values(this.machines).forEach(machine => { //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; 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; //fetch total Normalized Cog over all machines dynamicTotals.NCog += machine.NCog; }); 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."); this.calcDynamicTotals(); const { maxEfficiency, lowestEfficiency } = this.calcGroupEfficiency(this.machines); const efficiency = this.measurements.type("efficiency").variant("predicted").position("downstream").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 => { machineId = parseInt(machineId); const state = machines[machineId].state.getCurrentState(); const validSourceForMode = machines[machineId].isValidSourceForMode("parent", "auto"); 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" || !validSourceForMode || !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 -------- // 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); } setScaling(scaling) { const scalingSet = new Set(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.`); } //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)); //set the pressures Object.entries(this.machines).forEach(([machineId, machine]) => { if(machine.state.getCurrentState() !== "operational" && machine.state.getCurrentState() !== "accelerating" && machine.state.getCurrentState() !== "decelerating"){ 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(); } }); //update dynamic totals const dynamicTotals = this.calcDynamicTotals(); 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("upstream").value(bestResult.bestPower); this.measurements.type("flow").variant("predicted").position("downstream").value(bestResult.bestFlow); this.measurements.type("efficiency").variant("predicted").position("downstream").value(bestResult.bestFlow / bestResult.bestPower); this.measurements.type("Ncog").variant("predicted").position("downstream").value(bestResult.bestCog); await Promise.all(Object.entries(this.machines).map(async ([machineId, machine]) => { 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 to 0`); flow = 0; } if( (flow <= 0 ) && ( machineStates[machineId] === "operational" || machineStates[machineId] === "accelerating" || machineStates[machineId] === "decelerating" ) ){ await machine.handleInput("parent", "execSequence", "shutdown"); } else if(machineStates[machineId] === "idle" && flow > 0){ await machine.handleInput("parent", "execSequence", "startup"); } else 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: parseInt(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 validSourceForMode = machine.isValidSourceForMode("parent", "auto"); const validActionForMode = machine.isValidActionForMode("execSequence", "auto"); return !(state === "off" || state === "coolingdown" || state === "stopping" || state === "emergencystop" || !validSourceForMode || !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(); // Handle zero demand by shutting down all machines early exit if (Qd <= 0) { await Promise.all(Object.entries(this.machines).map(async ([machineId, machine]) => { if (this.isMachineActive(machineId)) { await machine.handleInput("parent", "execSequence", "shutdown"); } })); return; } // 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("upstream").value(totalPower); this.measurements.type("flow").variant("predicted").position("downstream").value(totalFlow); this.measurements.type("efficiency").variant("predicted").position("downstream").value(totalFlow / totalPower); this.measurements.type("Ncog").variant("predicted").position("downstream").value(totalCog); // Apply the flow distribution to machines await Promise.all(flowDistribution.map(async ({ machineId, flow }) => { const machine = this.machines[machineId]; 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("upstream").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("upstream").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("downstream").value(totalFlow.reduce((a, b) => a + b, 0) / totalPower.reduce((a, b) => a + b, 0)); } } catch(err){ this.logger.error(err); } } async handleInput(source, Qd, powerCap = Infinity, priorityList = null) { if (!this.isValidSourceForMode(source, this.mode)) { this.logger.warn(`Invalid source ${source} for mode ${this.mode}`); return; } const scaling = this.scaling; const mode = this.mode; let rawInput = Qd; switch (scaling) { case "absolute": // No scaling needed but cap range if (Qd < this.absoluteTotals.flow.min) { this.logger.warn(`Flow demand ${Qd} is below minimum possible flow ${this.absoluteTotals.flow.min}. Capping to minimum flow.`); Qd = this.absoluteTotals.flow.min; } else if (Qd > this.absoluteTotals.flow.max) { this.logger.warn(`Flow demand ${Qd} is above maximum possible flow ${this.absoluteTotals.flow.max}. Capping to maximum flow.`); Qd = this.absoluteTotals.flow.max; } break; case "normalized": // Scale demand to 0-100% linear between min and max flow this is auto capped Qd = this.interpolation.interpolate_lin_single_point(Qd, 0, 100, this.dynamicTotals.flow.min, this.dynamicTotals.flow.max); break; } switch(mode) { case "prioritycontrol": await this.equalFlowControl(Qd,powerCap,priorityList); break; case "prioritypercentagecontrol": if(scaling !== "normalized"){ this.logger.warn("Priority percentage control is only valid with normalized scaling."); return; } await this.prioPercentageControl(rawInput,priorityList); break; case "optimalcontrol": await this.optimalControl(Qd,powerCap); 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); } setMode(source,mode) { this.isValidSourceForMode(source, mode) ? this.mode = mode : this.logger.warn(`Invalid source ${source} for 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 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["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/dependencies/machine/machine'); const Measurement = require('../../../measurement/dependencies/measurement/measurement'); const specs = require('../../../generalFunctions/datasets/assetData/pumps/hydrostal/centrifugal pumps/models.json'); const power = require("../../../convert/dependencies/definitions/power"); const { machine } = require("os"); function createBaseMachineConfig(name,specs) { return { general: { logging: { enabled: true, logLevel: "warn" }, name: name, unit: "m3/h" }, functionality: { softwareType: "machine", role: "RotationalDeviceController" }, asset: { type: "pump", subType: "Centrifugal", model: "TestModel", supplier: "Hydrostal", machineCurve: specs[0].machineCurve }, mode: { current: "auto", allowedActions: { auto: ["execSequence", "execMovement", "statusCheck"], virtualControl: ["execMovement", "statusCheck"], fysicalControl: ["statusCheck"] }, allowedSources: { auto: ["parent", "GUI"], virtualControl: ["GUI"], fysicalControl: ["fysical"] } }, sequences: { startup: ["starting", "warmingup", "operational"], shutdown: ["stopping", "coolingdown", "idle"], emergencystop: ["emergencystop", "off"], boot: ["idle", "starting", "warmingup", "operational"] } }; } 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 machineConfig = createBaseMachineConfig("TestMachine",specs); const ptConfig = { general: { logging: { enabled: true, logLevel: "debug" }, name: "TestPT", unit: "mbar", }, functionality: { softwareType: "measurement", role: "Sensor" }, asset: { type: "sensor", subType: "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 = 0; i < numofMachines; i++){ const machine = new Machine(machineConfig); //mg.machines[i] = machine; mg.childRegistrationUtils.registerChild(machine, "downstream"); } mg.machines[1].childRegistrationUtils.registerChild(pt1, "downstream"); mg.machines[2].childRegistrationUtils.registerChild(pt1, "downstream"); mg.setMode("parent","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); console.log("Waiting for 0.2 sec "); //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); console.log("Waiting for 0.2 sec "); //await new Promise(resolve => setTimeout(resolve, 200)); console.log("------------------------------------"); } *//* for(let demand = 0 ; demand <= 100 ; demand += 1){ //set pressure console.log("------------------------------------"); await mg.handleInput("parent",demand); pt1.calculateInput(1400); console.log("Waiting for 0.2 sec "); //await new Promise(resolve => setTimeout(resolve, 200)); console.log("------------------------------------"); } } catch(err){ console.log(err); } } makeMachines(); //*/