From de5652b73d292b9a9ae4229c8ffd6fd7146d2780 Mon Sep 17 00:00:00 2001 From: znetsixe <73483679+znetsixe@users.noreply.github.com> Date: Thu, 31 Jul 2025 09:10:34 +0200 Subject: [PATCH] small bug fixes --- dependencies/machineGroup/machineGroup.js | 1056 ----------------- .../machineGroup/machineGroup.test.js | 566 --------- .../machineGroup/machineGroupConfig.json | 188 --- dependencies/test.js | 137 --- mgc.html | 15 +- src/nodeClass.js | 4 +- src/specificClass.js | 2 +- 7 files changed, 7 insertions(+), 1961 deletions(-) delete mode 100644 dependencies/machineGroup/machineGroup.js delete mode 100644 dependencies/machineGroup/machineGroup.test.js delete mode 100644 dependencies/machineGroup/machineGroupConfig.json delete mode 100644 dependencies/test.js diff --git a/dependencies/machineGroup/machineGroup.js b/dependencies/machineGroup/machineGroup.js deleted file mode 100644 index 31edbd4..0000000 --- a/dependencies/machineGroup/machineGroup.js +++ /dev/null @@ -1,1056 +0,0 @@ -/** - * @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: - * - r.de.ren@brabantsedelta.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(); -//*/ diff --git a/dependencies/machineGroup/machineGroup.test.js b/dependencies/machineGroup/machineGroup.test.js deleted file mode 100644 index 80cc246..0000000 --- a/dependencies/machineGroup/machineGroup.test.js +++ /dev/null @@ -1,566 +0,0 @@ -const MachineGroup = require('./machineGroup'); -const Machine = require('../../../rotatingMachine/dependencies/machine/machine'); -const specs = require('../../../generalFunctions/datasets/assetData/pumps/hydrostal/centrifugal pumps/models.json'); - -class MachineGroupTester { - constructor() { - this.totalTests = 0; - this.passedTests = 0; - this.failedTests = 0; - this.machineCurve = specs[0].machineCurve; - } - - assert(condition, message) { - this.totalTests++; - if (condition) { - console.log(`✓ PASS: ${message}`); - this.passedTests++; - } else { - console.log(`✗ FAIL: ${message}`); - this.failedTests++; - } - } - - createBaseMachineConfig(name) { - return { - general: { - logging: { enabled: true, logLevel: "debug" }, - name: name, - unit: "m3/h" - }, - functionality: { - softwareType: "machine", - role: "RotationalDeviceController" - }, - asset: { - type: "pump", - subType: "Centrifugal", - model: "TestModel", - supplier: "Hydrostal", - machineCurve: this.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"] - }, - calculationMode: "medium" - }; - } - - createBaseMachineGroupConfig(name) { - return { - general: { - logging: { enabled: true, logLevel: "debug" }, - name: name - }, - functionality: { - softwareType: "machineGroup", - role: "GroupController" - }, - scaling: { - current: "normalized" - }, - mode: { - current: "optimalControl" - } - }; - } - - async testSingleMachineOperation() { - console.log('\nTesting Single Machine Operation...'); - - const machineGroupConfig = this.createBaseMachineGroupConfig("TestMachineGroup"); - const machineConfig = this.createBaseMachineConfig("TestMachine1"); - - try { - const mg = new MachineGroup(machineGroupConfig); - const machine = new Machine(machineConfig); - - // Register machine with group - mg.childRegistrationUtils.registerChild(machine, "downstream"); - machine.measurements.type("pressure").variant("measured").position("downstream").value(800); - await machine.state.transitionToState("idle"); - - // Test 1: Basic initialization - this.assert( - Object.keys(mg.machines).length === 0, - 'Machine group should have exactly zero machine' - ); - - // Test 2: Calculate demand with single machine - await machine.handleInput("parent", "execSequence", "startup"); - await mg.handleFlowInput(50); - this.assert( - mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() > 0, - 'Total flow should be greater than 0 for demand of 50' - ); - - // Test 3: Check machine mode handling - machine.setMode("virtualControl"); - const {single, machineNum} = mg.singleMachine(); - this.assert( - single === true, - 'Should identify as single machine when in virtual control' - ); - - // Test 4: Zero demand handling - await mg.handleFlowInput(0); - this.assert( - !mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() || - mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() === 0, - 'Total flow should be 0 for zero demand' - ); - - // Test 5: Max demand handling - await mg.handleFlowInput(100); - this.assert( - mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() > 0, - 'Total flow should be greater than 0 for max demand' - ); - - } catch (error) { - console.error('Test failed with error:', error); - this.failedTests++; - } - } - - async testMultipleMachineOperation() { - console.log('\nTesting Multiple Machine Operation...'); - - const machineGroupConfig = this.createBaseMachineGroupConfig("TestMachineGroup"); - - try { - const mg = new MachineGroup(machineGroupConfig); - const machine1 = new Machine(this.createBaseMachineConfig("Machine1")); - const machine2 = new Machine(this.createBaseMachineConfig("Machine2")); - - mg.childRegistrationUtils.registerChild(machine1, "downstream"); - mg.childRegistrationUtils.registerChild(machine2, "downstream"); - - machine1.measurements.type("pressure").variant("measured").position("downstream").value(800); - machine2.measurements.type("pressure").variant("measured").position("downstream").value(800); - - await machine1.state.transitionToState("idle"); - await machine2.state.transitionToState("idle"); - - await machine1.handleInput("parent", "execSequence", "startup"); - await machine2.handleInput("parent", "execSequence", "startup"); - - // Test 1: Multiple machine registration - this.assert( - Object.keys(mg.machines).length === 2, - 'Machine group should have exactly two machines' - ); - - // Test 1.1: Calculate demand with multiple machines - await mg.handleFlowInput(0); // Testing with higher demand for two machines - const machineOutputs = Object.keys(mg.machines).filter(id => - mg.machines[id].measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() > 0 - ); - - this.assert( - mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() > 0 && - machineOutputs.length > 0, - 'Should distribute load between machines' - ); - - // Test 1.2: Calculate demand with multiple machines with an increment of 10 - for(let i = 0; i < 100; i+=10){ - await mg.handleFlowInput(i); // Testing with incrementing demand - const flowValue = mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue(); - this.assert( - flowValue !== undefined && !isNaN(flowValue), - `Should handle demand of ${i} units properly` - ); - } - - // Test 2: Calculate nonsense demands with multiple machines - await mg.handleFlowInput(150); // Testing with higher demand for two machines - this.assert( - mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() > 0, - 'Should handle excessive demand gracefully' - ); - - // Test 3: Force single machine mode - machine2.setMode("maintenance"); - const {single} = mg.singleMachine(); - this.assert( - single === true, - 'Should identify as single machine when one machine is in maintenance' - ); - } catch (error) { - console.error('Test failed with error:', error); - this.failedTests++; - } - } - - async testDynamicTotals() { - console.log('\nTesting Dynamic Totals...'); - - const mg = new MachineGroup(this.createBaseMachineGroupConfig("TestMachineGroup")); - const machine = new Machine(this.createBaseMachineConfig("TestMachine")); - - try { - mg.childRegistrationUtils.registerChild(machine, "downstream"); - machine.measurements.type("pressure").variant("measured").position("downstream").value(800); - await machine.state.transitionToState("idle"); - await machine.handleInput("parent", "execSequence", "startup"); - - // Test 1: Dynamic totals initialization - const maxFlow = machine.predictFlow.currentFxyYMax; - const maxPower = machine.predictPower.currentFxyYMax; - - this.assert( - mg.dynamicTotals.flow.max === maxFlow && mg.dynamicTotals.power.max === maxPower, - 'Dynamic totals should reflect machine capabilities' - ); - - // Test 2: Demand scaling - await mg.handleFlowInput(50); // 50% of max - const actualFlow = mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue(); - this.assert( - actualFlow <= maxFlow * 0.6, // Allow some margin for interpolation - 'Scaled demand should be approximately 50% of max flow' - ); - } catch (error) { - console.error('Test failed with error:', error); - this.failedTests++; - } - } - - async testInterpolation() { - console.log('\nTesting Interpolation...'); - - const machineGroupConfig = this.createBaseMachineGroupConfig("TestMachineGroup"); - const machineConfig = this.createBaseMachineConfig("TestMachine"); - - try { - const mg = new MachineGroup(machineGroupConfig); - const machine = new Machine(machineConfig); - - // Register machine and set initial state - mg.childRegistrationUtils.registerChild(machine, "downstream"); - machine.measurements.type("pressure").variant("measured").position("downstream").value(1); - machine.state.transitionToState("idle"); - - // Test interpolation at different demand points - const testPoints = [0, 25, 50, 75, 100]; - for (const demand of testPoints) { - await mg.handleFlowInput(demand); - const flowValue = mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue(); - const powerValue = mg.measurements.type("power").variant("predicted").position("upstream").getCurrentValue(); - - this.assert( - flowValue !== undefined && !isNaN(flowValue), - `Interpolation should produce valid flow value for demand ${demand}` - ); - this.assert( - powerValue !== undefined && !isNaN(powerValue), - `Interpolation should produce valid power value for demand ${demand}` - ); - } - - // Test interpolation between curve points - const interpolatedPoint = 45; // Should interpolate between 40 and 60 - await mg.handleFlowInput(interpolatedPoint); - this.assert( - mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() > 0, - `Interpolation should handle non-exact point ${interpolatedPoint}` - ); - - } catch (error) { - console.error('Test failed with error:', error); - this.failedTests++; - } - } - - async testSingleMachineControlModes() { - console.log('\nTesting Single Machine Control Modes...'); - - const machineGroupConfig = this.createBaseMachineGroupConfig("TestMachineGroup"); - const machineConfig = this.createBaseMachineConfig("TestMachine1"); - - try { - const mg = new MachineGroup(machineGroupConfig); - const machine = new Machine(machineConfig); - - // Register machine and initialize - mg.childRegistrationUtils.registerChild(machine, "downstream"); - machine.measurements.type("pressure").variant("measured").position("downstream").value(800); - await machine.state.transitionToState("idle"); - await machine.handleInput("parent", "execSequence", "startup"); - - // Test 1: Virtual Control Mode - machine.setMode("virtualControl"); - await mg.handleFlowInput(50); - this.assert( - machine.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() !== undefined, - 'Should handle virtual control mode' - ); - - // Test 2: Physical Control Mode - machine.setMode("fysicalControl"); - await mg.handleFlowInput(75); - this.assert( - machine.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() !== undefined, - 'Should handle physical control mode' - ); - - // Test 3: Auto Mode Return - machine.setMode("auto"); - await mg.handleFlowInput(60); - this.assert( - mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() > 0, - 'Should return to normal operation in auto mode' - ); - - } catch (error) { - console.error('Test failed with error:', error); - this.failedTests++; - } - } - - async testMachinesOffNormalized() { - console.log('\nTesting Machines Off with Normalized Flow...'); - - const machineGroupConfig = this.createBaseMachineGroupConfig("TestMachineGroup_OffNormalized"); - // scaling is "normalized" by default - const mg = new MachineGroup(machineGroupConfig); - const machine = new Machine(this.createBaseMachineConfig("TestMachine_OffNormalized")); - - mg.childRegistrationUtils.registerChild(machine, "downstream"); - machine.measurements.type("pressure").variant("measured").position("downstream").value(800); - await machine.state.transitionToState("idle"); - await machine.handleInput("parent", "execSequence", "startup"); - - // Turn machines off by setting demand to 0 with normalized scaling - await mg.handleFlowInput(-1); - this.assert( - !mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() || - mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() === 0, - 'Total flow should be 0 when demand is < 0 in normalized scaling' - ); - } - - async testMachinesOffAbsolute() { - console.log('\nTesting Machines Off with Absolute Flow...'); - - const machineGroupConfig = this.createBaseMachineGroupConfig("TestMachineGroup_OffAbsolute"); - // Switch scaling to "absolute" - machineGroupConfig.scaling.current = "absolute"; - const mg = new MachineGroup(machineGroupConfig); - const machine = new Machine(this.createBaseMachineConfig("TestMachine_OffAbsolute")); - - mg.childRegistrationUtils.registerChild(machine, "downstream"); - machine.measurements.type("pressure").variant("measured").position("downstream").value(800); - await machine.state.transitionToState("idle"); - await machine.handleInput("parent", "execSequence", "startup"); - - // Turn machines off by setting demand to 0 with absolute scaling - await mg.handleFlowInput(0); - this.assert( - !mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() || - mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() === 0, - 'Total flow should be 0 when demand is 0 in absolute scaling' - ); - } - - async testPriorityControl() { - console.log('\nTesting Priority Control...'); - - const machineGroupConfig = this.createBaseMachineGroupConfig("TestMachineGroup_Priority"); - const mg = new MachineGroup(machineGroupConfig); - - try { - // Create 3 machines with different configurations for clearer testing - const machines = []; - for(let i = 1; i <= 3; i++) { - const machineConfig = this.createBaseMachineConfig(`Machine${i}`); - const machine = new Machine(machineConfig); - machines.push(machine); - mg.childRegistrationUtils.registerChild(machine, "downstream"); - - // Set different max flows to make priority visible - machine.predictFlow = { - currentFxyYMin: 10 * i, // Different min flows - currentFxyYMax: 50 * i // Different max flows - }; - - machine.measurements.type("pressure").variant("measured").position("downstream").value(800); - await machine.state.transitionToState("idle"); - await machine.handleInput("parent", "execSequence", "startup"); - - // Mock the inputFlowCalcPower method for testing - machine.inputFlowCalcPower = (flow) => flow * 2; // Simple mock function - } - - // Test 1: Default priority (by machine ID) - // Use handleInput which routes to equalControl in prioritycontrol mode - await mg.handleInput("parent", 80); - const flowAfterDefaultPriority = Object.values(mg.machines).map(machine => - machine.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() || 0 - ); - this.assert( - flowAfterDefaultPriority[0] > 0 && flowAfterDefaultPriority[1] > 0 && flowAfterDefaultPriority[2] === 0, - 'Default priority should use machines in ID order until demand is met' - ); - - // Test 2: Custom priority list - await mg.handleInput("parent", 120, Infinity, [3, 2, 1]); - await new Promise(resolve => setTimeout(resolve, 100)); - const flowAfterCustomPriority = Object.values(mg.machines).map(machine => - machine.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() || 0 - ); - this.assert( - flowAfterCustomPriority[2] > 0 && flowAfterCustomPriority[1] > 0 && flowAfterCustomPriority[0] === 0, - 'Custom priority should use machines in specified order until demand is met' - ); - - // Test 3: Zero demand should shut down all machines - await mg.handleInput("parent", 0); - const noFlowCondition = Object.values(mg.machines).every(machine => - !machine.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() || - machine.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() === 0 - ); - this.assert( - noFlowCondition, - 'Zero demand should result in no flow from any machine' - ); - - // Test 4: Handling excessive demand (more than total capacity) - const totalMaxFlow = machines.reduce((sum, machine) => sum + machine.predictFlow.currentFxyYMax, 0); - await mg.handleInput("parent", totalMaxFlow + 100); - const totalActualFlow = mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue(); - this.assert( - totalActualFlow <= totalMaxFlow && totalActualFlow > 0, - 'Excessive demand should be capped to maximum possible flow' - ); - - // Test 5: Check all measurements are updated correctly - this.assert( - mg.measurements.type("power").variant("predicted").position("upstream").getCurrentValue() > 0 && - mg.measurements.type("efficiency").variant("predicted").position("downstream").getCurrentValue() > 0, - 'All measurements should be updated after priority control' - ); - - } catch (error) { - console.error('Priority control test failed with error:', error); - this.failedTests++; - } - } - - async runAllTests() { - console.log('Starting MachineGroup Tests...\n'); - - await this.testSingleMachineOperation(); - await this.testMultipleMachineOperation(); - await this.testDynamicTotals(); - await this.testInterpolation(); - await this.testSingleMachineControlModes(); - await this.testMachinesOffNormalized(); - await this.testMachinesOffAbsolute(); - await this.testPriorityControl(); // Add the new test - await testCombinationIterations(); - - console.log('\nTest Summary:'); - console.log(`Total Tests: ${this.totalTests}`); - console.log(`Passed: ${this.passedTests}`); - console.log(`Failed: ${this.failedTests}`); - - // Return exit code based on test results - process.exit(this.failedTests > 0 ? 1 : 0); - } -} - -// Add a custom logger to capture debug logs during tests -class CapturingLogger { - constructor() { - this.logs = []; - } - debug(message) { - this.logs.push({ level: "debug", message }); - console.debug(message); - } - info(message) { - this.logs.push({ level: "info", message }); - console.info(message); - } - warn(message) { - this.logs.push({ level: "warn", message }); - console.warn(message); - } - error(message) { - this.logs.push({ level: "error", message }); - console.error(message); - } - getAll() { - return this.logs; - } - clear() { - this.logs = []; - } -} - -// Modify one of the test functions to override the machineGroup logger -async function testCombinationIterations() { - console.log('\nTesting Combination Iterations Logging...'); - - const machineGroupConfig = tester.createBaseMachineGroupConfig("TestCombinationIterations"); - const mg = new MachineGroup(machineGroupConfig); - - // Override logger with a capturing logger - const customLogger = new CapturingLogger(); - mg.logger = customLogger; - - // Create one machine for simplicity (or two if you like) - const machine = new Machine(tester.createBaseMachineConfig("TestMachineForCombo")); - mg.childRegistrationUtils.registerChild(machine, "downstream"); - machine.measurements.type("pressure").variant("measured").position("downstream").value(800); - await machine.state.transitionToState("idle"); - await machine.handleInput("parent", "execSequence", "startup"); - - // For testing, force dynamic totals so that combination search is exercised - mg.dynamicTotals.flow = { min: 0, max: 200 }; // example totalling - // Call handleFlowInput with a demand that requires iterations - await mg.handleFlowInput(120); - - // After running, output captured iteration debug logs - console.log("\n-- Captured Debug Logs for Combination Search Iterations --"); - customLogger.getAll().forEach(log => { - if(log.level === "debug") { - console.log(log.message); - } - }); - - // Also output best result details if any needed for further improvement - console.log("\n-- Final Output --"); - const totalFlow = mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue(); - console.log("Total Flow: ", totalFlow); - - // Get machine outputs by checking each machine's measurements - const machineOutputs = {}; - Object.entries(mg.machines).forEach(([id, machine]) => { - const flow = machine.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue(); - if (flow) machineOutputs[id] = flow; - }); - console.log("Machine Outputs: ", machineOutputs); -} - -// Run the tests -const tester = new MachineGroupTester(); -tester.runAllTests().catch(console.error); \ No newline at end of file diff --git a/dependencies/machineGroup/machineGroupConfig.json b/dependencies/machineGroup/machineGroupConfig.json deleted file mode 100644 index de10a57..0000000 --- a/dependencies/machineGroup/machineGroupConfig.json +++ /dev/null @@ -1,188 +0,0 @@ -{ - "general": { - "name": { - "default": "Machine Group Configuration", - "rules": { - "type": "string", - "description": "A human-readable name or label for this machine group configuration." - } - }, - "id": { - "default": null, - "rules": { - "type": "string", - "nullable": true, - "description": "A unique identifier for this configuration. If not provided, defaults to null." - } - }, - "logging": { - "logLevel": { - "default": "info", - "rules": { - "type": "enum", - "values": [ - { - "value": "debug", - "description": "Log messages are printed for debugging purposes." - }, - { - "value": "info", - "description": "Informational messages are printed." - }, - { - "value": "warn", - "description": "Warning messages are printed." - }, - { - "value": "error", - "description": "Error messages are printed." - } - ] - } - }, - "enabled": { - "default": true, - "rules": { - "type": "boolean", - "description": "Indicates whether logging is active. If true, log messages will be generated." - } - } - } - }, - "functionality": { - "softwareType": { - "default": "machineGroup", - "rules": { - "type": "string", - "description": "Logical name identifying the software type." - } - }, - "role": { - "default": "GroupController", - "rules": { - "type": "string", - "description": "Controls a group of machines within the system." - } - } - }, - "mode": { - "current": { - "default": "optimalControl", - "rules": { - "type": "enum", - "values": [ - { - "value": "optimalControl", - "description": "The group controller selects the most optimal combination of machines based on their real-time performance curves." - }, - { - "value": "priorityControl", - "description": "Machines are controlled sequentially from minimum to maximum output until each is maxed out, then additional machines are added." - }, - { - "value": "prioritypercentagecontrol", - "description": "Machines are controlled sequentially from minimum to maximum output until each is maxed out, then additional machines are added based on a percentage of the total demand." - }, - { - "value": "maintenance", - "description": "The group is in maintenance mode with limited actions (monitoring only)." - } - ], - "description": "The operational mode of the machine group controller." - } - }, - "allowedActions": { - "default": {}, - "rules": { - "type": "object", - "schema": { - "optimalControl": { - "default": ["statusCheck", "execOptimalCombination", "balanceLoad", "emergencyStop"], - "rules": { - "type": "set", - "itemType": "string", - "description": "Actions allowed in optimalControl mode." - } - }, - "priorityControl": { - "default": ["statusCheck", "execSequentialControl", "balanceLoad", "emergencyStop"], - "rules": { - "type": "set", - "itemType": "string", - "description": "Actions allowed in priorityControl mode." - } - }, - "prioritypercentagecontrol": { - "default": ["statusCheck", "execSequentialControl", "balanceLoad", "emergencyStop"], - "rules": { - "type": "set", - "itemType": "string", - "description": "Actions allowed in manualOverride mode." - } - }, - "maintenance": { - "default": ["statusCheck"], - "rules": { - "type": "set", - "itemType": "string", - "description": "Actions allowed in maintenance mode." - } - } - }, - "description": "Defines the actions available for each operational mode of the machine group controller." - } - }, - "allowedSources": { - "default": {}, - "rules": { - "type": "object", - "schema": { - "optimalcontrol": { - "default": ["parent", "GUI", "physical", "API"], - "rules": { - "type": "set", - "itemType": "string", - "description": "Command sources allowed in optimalControl mode." - } - }, - "prioritycontrol": { - "default": ["parent", "GUI", "physical", "API"], - "rules": { - "type": "set", - "itemType": "string", - "description": "Command sources allowed in priorityControl mode." - } - }, - "prioritypercentagecontrol": { - "default": ["parent", "GUI", "physical", "API"], - "rules": { - "type": "set", - "itemType": "string", - "description": "Command sources allowed " - } - } - }, - "description": "Specifies the valid command sources recognized by the machine group controller for each mode." - } - } - }, - "scaling": { - "current": { - "default": "normalized", - "rules": { - "type": "enum", - "values": [ - { - "value": "normalized", - "description": "Scales the demand between 0–100% of the total flow capacity, interpolating to calculate the effective demand." - }, - { - "value": "absolute", - "description": "Uses the absolute demand value directly, capped between the min and max machine flow capacities." - } - ], - "description": "The scaling mode for demand calculations." - } - } - } - } diff --git a/dependencies/test.js b/dependencies/test.js deleted file mode 100644 index 1f3bd2b..0000000 --- a/dependencies/test.js +++ /dev/null @@ -1,137 +0,0 @@ -/** - * This file implements a pump optimization algorithm that: - * 1. Models different pumps with efficiency characteristics - * 2. Determines all possible pump combinations that can meet a demand flow - * 3. Finds the optimal combination that minimizes power consumption - * 4. Tests the algorithm with different demand levels - */ - -/** - * Pump Class - * Represents a pump with specific operating characteristics including: - * - Maximum flow capacity - * - Center of Gravity (CoG) for efficiency - * - Efficiency curve mapping flow percentages to power consumption - */ -class Pump { - constructor(name, maxFlow, cog, efficiencyCurve) { - this.name = name; - this.maxFlow = maxFlow; // Maximum flow at a given pressure - this.CoG = cog; // Efficiency center of gravity percentage - this.efficiencyCurve = efficiencyCurve; // Flow % -> Power usage mapping - } - - /** - * Returns pump flow at a given pressure - * Currently assumes constant flow regardless of pressure - */ - getFlow(pressure) { - return this.maxFlow; // Assume constant flow at a given pressure - } - - /** - * Calculates power consumption based on flow and pressure - * Uses the efficiency curve when available, otherwise uses linear approximation - */ - getPowerConsumption(flow, pressure) { - let flowPercent = flow / this.maxFlow; - return this.efficiencyCurve[flowPercent] || (1.2 * flow); // Default linear approximation - } -} - -/** - * Test pump definitions - * Three pump models with different flow capacities and efficiency characteristics - */ -const pumps = [ - new Pump("Pump A", 100, 0.6, {0.6: 50, 0.8: 70, 1.0: 100}), - new Pump("Pump B", 120, 0.7, {0.6: 55, 0.8: 75, 1.0: 110}), - new Pump("Pump C", 90, 0.5, {0.5: 40, 0.7: 60, 1.0: 90}), -]; - -const pressure = 1.0; // Assume constant pressure - -/** - * Get all valid pump combinations that meet the required demand flow (Qd) - * - * @param {Array} pumps - Available pump array - * @param {Number} Qd - Required demand flow - * @param {Number} pressure - System pressure - * @returns {Array} Array of valid pump combinations that can meet or exceed the demand - * - * This function: - * 1. Generates all possible subsets of pumps (power set) - * 2. Filters for non-empty subsets that can meet or exceed demand flow - */ -function getValidPumpCombinations(pumps, Qd, pressure) { - let subsets = [[]]; - for (let pump of pumps) { - let newSubsets = subsets.map(set => [...set, pump]); - subsets = subsets.concat(newSubsets); - } - return subsets.filter(subset => subset.length > 0 && - subset.reduce((sum, p) => sum + p.getFlow(pressure), 0) >= Qd); -} - -/** - * Find the optimal pump combination that minimizes power consumption - * - * @param {Array} pumps - Available pump array - * @param {Number} Qd - Required demand flow - * @param {Number} pressure - System pressure - * @returns {Object} Object containing the best pump combination and its power consumption - * - * This function: - * 1. Gets all valid pump combinations that meet demand - * 2. For each combination, distributes flow based on CoG proportions - * 3. Calculates total power consumption for each distribution - * 4. Returns the combination with minimum power consumption - */ -function optimizePumpSelection(pumps, Qd, pressure) { - let validCombinations = getValidPumpCombinations(pumps, Qd, pressure); - let bestCombination = null; - let minPower = Infinity; - - validCombinations.forEach(combination => { - let totalFlow = combination.reduce((sum, pump) => sum + pump.getFlow(pressure), 0); - let totalCoG = combination.reduce((sum, pump) => sum + pump.CoG, 0); - - // Distribute flow based on CoG proportions - let flowDistribution = combination.map(pump => ({ - pump, - flow: (pump.CoG / totalCoG) * Qd - })); - - let totalPower = flowDistribution.reduce((sum, { pump, flow }) => - sum + pump.getPowerConsumption(flow, pressure), 0); - - if (totalPower < minPower) { - minPower = totalPower; - bestCombination = flowDistribution; - } - }); - - return { bestCombination, minPower }; -} - -/** - * Test function that runs optimization for different demand levels - * Tests from 0% to 100% of total available flow in 10% increments - * Outputs the selected pumps, flow allocation, and power consumption for each scenario - */ -console.log("Pump Optimization Results:"); -const totalAvailableFlow = pumps.reduce((sum, pump) => sum + pump.getFlow(pressure), 0); - -for (let i = 0; i <= 10; i++) { - let Qd = (i / 10) * totalAvailableFlow; // Incremental flow demand - let { bestCombination, minPower } = optimizePumpSelection(pumps, Qd, pressure); - - console.log(`\nTotal Demand Flow: ${Qd.toFixed(2)}`); - console.log("Selected Pumps and Allocated Flow:"); - - bestCombination.forEach(({ pump, flow }) => { - console.log(` ${pump.name}: ${flow.toFixed(2)} units`); - }); - - console.log(`Total Power Consumption: ${minPower.toFixed(2)} kW`); -} diff --git a/mgc.html b/mgc.html index 0d747ca..43c2a6b 100644 --- a/mgc.html +++ b/mgc.html @@ -57,13 +57,13 @@ const node = this; // Validate logger properties using the logger menu - if (window.EVOLV?.nodes?.measurement?.loggerMenu?.saveEditor) { - success = window.EVOLV.nodes.measurement.loggerMenu.saveEditor(node); + if (window.EVOLV?.nodes?.machineGroupControl?.loggerMenu?.saveEditor) { + success = window.EVOLV.nodes.machineGroupControl.loggerMenu.saveEditor(node); } // save position field - if (window.EVOLV?.nodes?.rotatingMachine?.positionMenu?.saveEditor) { - window.EVOLV.nodes.rotatingMachine.positionMenu.saveEditor(this); + if (window.EVOLV?.nodes?.machineGroupControl?.positionMenu?.saveEditor) { + window.EVOLV.nodes.machineGroupControl.positionMenu.saveEditor(this); } } @@ -78,13 +78,6 @@
- - -
- Tip: Ensure that the "Name" field is unique to easily identify the node. - Enable logging if you need detailed information for debugging purposes. - Choose the appropriate log level based on the verbosity required. - diff --git a/src/nodeClass.js b/src/nodeClass.js index b59fb12..00d5dcd 100644 --- a/src/nodeClass.js +++ b/src/nodeClass.js @@ -94,7 +94,7 @@ class nodeClass { // Determine overall status based on available machines const status = availableMachines.length > 0 - ? `${availableMachines.length} machines` + ? `${availableMachines.length} machine(s) connected` : "No machines"; let scalingSymbol = ""; @@ -197,7 +197,7 @@ class nodeClass { const RED = this.RED; switch (msg.topic) { case "registerChild": - console.log(`Registering child in mgc: ${msg.payload}`); + //console.log(`Registering child in mgc: ${msg.payload}`); const childId = msg.payload; const childObj = RED.nodes.getNode(childId); mg.childRegistrationUtils.registerChild( diff --git a/src/specificClass.js b/src/specificClass.js index e501a8b..dc17fea 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -56,7 +56,7 @@ class MachineGroup { this.measurements = new MeasurementContainer(); this.interpolation = new interpolation(); - // Machines and children data + // Machines and child data this.machines = {}; this.child = {}; this.scaling = this.config.scaling.current;