Compare commits

...

10 Commits

Author SHA1 Message Date
681856104d Merge pull request 'changed colours, description based on s88' (#2) from dev-Rene into main
Reviewed-on: RnD/machineGroupControl#2
2025-10-16 13:22:56 +00:00
znetsixe
e0526250c2 changed colours, description based on s88 2025-10-14 13:52:18 +02:00
c0e4539b50 Merge pull request 'dev-Rene' (#1) from dev-Rene into main
Reviewed-on: RnD/machineGroupControl#1
2025-10-06 14:15:42 +00:00
znetsixe
426d45890f ok 2025-10-05 07:56:35 +02:00
znetsixe
8c59a921d5 syncing 2025-10-05 07:55:23 +02:00
Rene De ren
15501e8b1d updates from laptop 2025-10-03 15:33:37 +02:00
znetsixe
b4364094c6 Stable version of machinegroup control 2025-10-02 17:08:41 +02:00
znetsixe
a55c6bdbea fixed pressure updates from machines. Everything seems to be working again. 2025-09-23 15:50:40 +02:00
znetsixe
ac9d1b4fdd added test file 2025-09-23 15:12:01 +02:00
znetsixe
cbc0840f0c added testfile fixing bugs 2025-09-23 15:03:57 +02:00
4 changed files with 527 additions and 158 deletions

View File

@@ -1,15 +1,12 @@
<!--
brabantse delta kleuren:
#eaf4f1
#86bbdd
#bad33b
#0c99d9
#a9daee
#0f52a5
#50a8d9
#cade63
#4f8582
#c4cce0
| S88-niveau | Primair (blokkleur) | Tekstkleur |
| ---------------------- | ------------------- | ---------- |
| **Area** | `#0f52a5` | wit |
| **Process Cell** | `#0c99d9` | wit |
| **Unit** | `#50a8d9` | zwart |
| **Equipment (Module)** | `#86bbdd` | zwart |
| **Control Module** | `#a9daee` | zwart |
-->
<script src="/machineGroupControl/menu.js"></script> <!-- Load the menu script for dynamic dropdowns -->
<script src="/machineGroupControl/configData.js"></script> <!-- Load the config script for node information -->
@@ -17,7 +14,7 @@
<script>
RED.nodes.registerType('machineGroupControl',{
category: "EVOLV",
color: "#eaf4f1",
color: "#50a8d9",
defaults: {
// Define default properties
name: { value: "" },
@@ -39,7 +36,7 @@
outputs:3,
inputLabels: ["Input"],
outputLabels: ["process", "dbase", "parent"],
icon: "font-awesome/fa-tachometer",
icon: "font-awesome/fa-cogs",
label: function () {
return this.positionIcon + " " + "machineGroup";

288
src/groupcontrol.test.js Normal file
View File

@@ -0,0 +1,288 @@
// ...existing code...
const MachineGroup = require('./specificClass.js');
const Machine = require('../../rotatingMachine/src/specificClass');
const Measurement = require('../../measurement/src/specificClass');
const specs = require('../../generalFunctions/datasets/assetData/curves/hidrostal-H05K-S03R.json');
const stateConfig = { time:{starting:0,warmingup:0,stopping:0,coolingdown:0}, movement:{speed:1000,mode:"staticspeed"} };
const ptConfig = {
general:{ logging:{enabled:false,logLevel:"warn"}, name:"testpt", id:"pt-1", unit:"mbar" },
functionality:{ softwareType:"measurement", role:"sensor" },
asset:{ category:"sensor", type:"pressure", model:"testmodel", supplier:"vega", unit:"mbar" },
scaling:{ absMin:0, absMax:4000 }
};
const testSuite = [];
const efficiencyComparisons = [];
function logPass(name, details="") {
const entry = { name, status:"PASS", details };
testSuite.push(entry);
console.log(`${name}${details ? `${details}` : ""}`);
}
function logFail(name, error) {
const entry = { name, status:"FAIL", details:error?.message || error };
testSuite.push(entry);
console.error(`${name}${entry.details}`);
}
function approxEqual(actual, expected, tolerancePct=1) {
const tolerance = (expected * tolerancePct) / 100;
return actual >= expected - tolerance && actual <= expected + tolerance;
}
async function sleep(ms){ return new Promise(resolve => setTimeout(resolve, ms)); }
function createMachineConfig(id,label) {
return {
general:{ logging:{enabled:false,logLevel:"warn"}, name:label, id, unit:"m3/h" },
functionality:{ softwareType:"machine", role:"rotationaldevicecontroller" },
asset:{ category:"pump", type:"centrifugal", model:"hidrostal-h05k-s03r", supplier:"hydrostal", machineCurve:specs },
mode:{
current:"auto",
allowedActions:{
auto:["execSequence","execMovement","flowMovement","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"]
}
};
}
async function bootstrapGroup() {
const groupCfg = {
general:{ logging:{enabled:false,logLevel:"warn"}, name:"testmachinegroup" },
functionality:{ softwareType:"machinegroup", role:"groupcontroller" },
scaling:{ current:"normalized" },
mode:{ current:"optimalcontrol" }
};
const mg = new MachineGroup(groupCfg);
const pt = new Measurement(ptConfig);
for (let idx=1; idx<=2; idx++){
const machine = new Machine(createMachineConfig(String(idx),`machine-${idx}`), stateConfig);
mg.childRegistrationUtils.registerChild(machine,"downstream");
machine.childRegistrationUtils.registerChild(pt,"downstream");
}
pt.calculateInput(1000);
await sleep(10);
return { mg, pt };
}
function captureState(mg,label){
return {
label,
machines: Object.entries(mg.machines).map(([id,machine]) => ({
id,
state: machine.state.getCurrentState(),
position: machine.state.getCurrentPosition(),
predictedFlow: machine.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() || 0,
predictedPower: machine.measurements.type("power").variant("predicted").position("upstream").getCurrentValue() || 0
})),
totals: {
flow: mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() || 0,
power: mg.measurements.type("power").variant("predicted").position("upstream").getCurrentValue() || 0,
efficiency: mg.measurements.type("efficiency").variant("predicted").position("downstream").getCurrentValue() || 0
}
};
}
async function testNormalizedScaling(mg,pt){
const label = "Normalized scaling tracks expected flow";
try{
mg.setScaling("normalized");
const dynamic = mg.calcDynamicTotals();
const checkpoints = [0,10,25,50,75,100];
for (const demand of checkpoints){
await mg.handleInput("parent", demand);
pt.calculateInput(1400);
await sleep(20);
const totals = mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() || 0;
const expected = dynamic.flow.min + (demand/100)*(dynamic.flow.max - dynamic.flow.min);
if(!approxEqual(totals, expected, 2)){
throw new Error(`Flow ${totals.toFixed(2)} outside expectation ${expected.toFixed(2)} @ ${demand}%`);
}
}
logPass(label);
}catch(err){ logFail(label, err); }
}
async function testAbsoluteScaling(mg,pt){
const label = "Absolute scaling accepts direct flow targets";
try{
mg.setScaling("absolute");
mg.setMode("optimalcontrol");
const absMin = mg.dynamicTotals.flow.min;
const absMax = mg.dynamicTotals.flow.max;
const demandPoints = [absMin, absMin+20, (absMin+absMax)/2, absMax-20];
for(const setpoint of demandPoints){
await mg.handleInput("parent", setpoint);
pt.calculateInput(1400);
await sleep(20);
const flow = mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() || 0;
if(!approxEqual(flow, setpoint, 2)){
throw new Error(`Flow ${flow.toFixed(2)} != demand ${setpoint.toFixed(2)}`);
}
}
logPass(label);
}catch(err){ logFail(label, err); }
}
async function testModeTransitions(mg,pt){
const label = "Mode transitions keep machines responsive";
try{
const modes = ["optimalcontrol","prioritycontrol","prioritypercentagecontrol"];
mg.setScaling("normalized");
for(const mode of modes){
mg.setMode(mode);
await mg.handleInput("parent", 50);
pt.calculateInput(1300);
await sleep(20);
const snapshot = captureState(mg, mode);
const active = snapshot.machines.filter(m => m.state !== "idle");
if(active.length === 0){
throw new Error(`No active machines after switching to ${mode}`);
}
}
logPass(label);
}catch(err){ logFail(label, err); }
}
async function testRampBehaviour(mg,pt){
const label = "Ramp up/down keeps monotonic flow";
try{
mg.setMode("optimalcontrol");
mg.setScaling("normalized");
const upDemands = [0,20,40,60,80,100];
let lastFlow = 0;
for(const demand of upDemands){
await mg.handleInput("parent", demand);
pt.calculateInput(1500);
await sleep(15);
const flow = mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() || 0;
if(flow < lastFlow - 1){
throw new Error(`Flow decreased during ramp up: ${flow.toFixed(2)} < ${lastFlow.toFixed(2)}`);
}
lastFlow = flow;
}
const downDemands = [100,80,60,40,20,0];
lastFlow = Infinity;
for(const demand of downDemands){
await mg.handleInput("parent", demand);
pt.calculateInput(1200);
await sleep(15);
const flow = mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() || 0;
if(flow > lastFlow + 1){
throw new Error(`Flow increased during ramp down: ${flow.toFixed(2)} > ${lastFlow.toFixed(2)}`);
}
lastFlow = flow;
}
logPass(label);
}catch(err){ logFail(label, err); }
}
async function testPressureAdaptation(mg,pt){
const label = "Pressure changes update predictions";
try{
mg.setMode("optimalcontrol");
mg.setScaling("normalized");
const pressures = [800,1200,1600,2000];
let previousFlow = null;
for(const p of pressures){
pt.calculateInput(p);
await mg.handleInput("parent", 50);
await sleep(20);
const flow = mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() || 0;
if(previousFlow !== null && Math.abs(flow - previousFlow) < 0.5){
throw new Error(`Flow did not react to pressure shift (${previousFlow.toFixed(2)} -> ${flow.toFixed(2)})`);
}
previousFlow = flow;
}
logPass(label);
}catch(err){ logFail(label, err); }
}
async function comparePriorityVsOptimal(mg, pt){
const label = "Priority vs Optimal efficiency comparison";
try{
mg.setScaling("normalized");
const pressures = [800, 1100, 1400, 1700];
const demands = [...Array(21)].map((_, idx) => idx * 5);
for (const pressure of pressures) {
pt.calculateInput(pressure);
await sleep(15);
for (const demand of demands) {
mg.setMode("optimalcontrol");
await mg.handleInput("parent", demand);
pt.calculateInput(pressure);
await sleep(20);
const optimalTotals = captureState(mg, `optimal-${pressure}-${demand}`).totals;
mg.setMode("prioritycontrol");
await mg.handleInput("parent", demand);
pt.calculateInput(pressure);
await sleep(20);
const priorityTotals = captureState(mg, `priority-${pressure}-${demand}`).totals;
efficiencyComparisons.push({
pressure,
demandPercent: demand,
optimalFlow: Number(optimalTotals.flow.toFixed(3)),
optimalPower: Number(optimalTotals.power.toFixed(3)),
optimalEfficiency: Number((optimalTotals.efficiency || 0).toFixed(4)),
priorityFlow: Number(priorityTotals.flow.toFixed(3)),
priorityPower: Number(priorityTotals.power.toFixed(3)),
priorityEfficiency: Number((priorityTotals.efficiency || 0).toFixed(4)),
efficiencyDelta: Number(((priorityTotals.efficiency || 0) - (optimalTotals.efficiency || 0)).toFixed(4)),
powerDelta: Number((priorityTotals.power - optimalTotals.power).toFixed(3))
});
}
}
logPass(label, "efficiencyComparisons array populated");
} catch (err) {
logFail(label, err);
}
}
async function run(){
console.log("🚀 Starting machine-group integration tests...");
const { mg, pt } = await bootstrapGroup();
await testNormalizedScaling(mg, pt);
await testAbsoluteScaling(mg, pt);
await testModeTransitions(mg, pt);
await testRampBehaviour(mg, pt);
await testPressureAdaptation(mg, pt);
await comparePriorityVsOptimal(mg, pt);
console.log("\n📋 TEST SUMMARY");
console.table(testSuite);
console.log("\n📊 efficiencyComparisons:");
console.dir(efficiencyComparisons, { depth:null });
console.log("✅ All tests completed.");
}
run().catch(err => {
console.error("💥 Test harness crashed:", err);
});
// ...existing code...
// Run all tests
run();

View File

@@ -58,6 +58,7 @@ class nodeClass {
}
_updateNodeStatus() {
//console.log('Updating node status...');
const mg = this.source;
const mode = mg.mode;
const scaling = mg.scaling;
@@ -72,7 +73,7 @@ class nodeClass {
const totalPower = mg.measurements
?.type("power")
?.variant("predicted")
?.position("upstream")
?.position("atEquipment")
?.getCurrentValue() || 0;
// Calculate total capacity based on available machines with safety checks
@@ -221,8 +222,7 @@ class nodeClass {
case "setMode":
const mode = msg.payload;
const source = "parent";
mg.setMode(source, mode);
mg.setMode(mode);
break;
case "setScaling":

View File

@@ -1,41 +1,3 @@
/**
* @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,configUtils,configManager, MeasurementContainer, interpolation , childRegistrationUtils} = require('generalFunctions');
@@ -74,12 +36,6 @@ class MachineGroup {
}
// 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();
}
registerChild(child,softwareType) {
this.logger.debug('Setting up childs specific for this class');
@@ -87,13 +43,27 @@ class MachineGroup {
if(softwareType == "machine"){
// Check if the machine is already registered
this.machines[child.config.general.id] === undefined ? this.machines[child.config.general.id] = child : this.logger.warn(`Machine ${child.config.general.id} is already registered.`);
this.handleChildChange();
/*
// Listen for changes in the child machine
child.emitter.on('stateChange', () => this.handleChildChange());
child.emitter.on('pressureChange', () => this.handlePressureChange());
child.emitter.on('ncogChange', () => this.handleChildChange());
*/
//listen for machine pressure changes
this.logger.debug(`Listening for pressure changes from machine ${child.config.general.id}`);
child.measurements.emitter.on("pressure.measured.differential", (eventData) => {
this.logger.debug(`Pressure update from ${child.config.general.id}: ${eventData.value} ${eventData.unit}`);
this.handlePressureChange();
});
child.measurements.emitter.on("pressure.measured.downstream", (eventData) => {
this.logger.debug(`Pressure update from ${child.config.general.id}: ${eventData.value} ${eventData.unit}`);
this.handlePressureChange();
});
child.measurements.emitter.on("flow.predicted.downstream", (eventData) => {
this.logger.debug(`Flow prediction update from ${child.config.general.id}: ${eventData.value} ${eventData.unit}`);
//later change to this.handleFlowPredictionChange();
this.handlePressureChange();
});
}
}
@@ -119,6 +89,7 @@ class MachineGroup {
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; }
@@ -127,6 +98,29 @@ class MachineGroup {
});
if(absoluteTotals.flow.min === Infinity) {
this.logger.warn(`Flow min ${absoluteTotals.flow.min} is Infinity. Setting to 0.`);
absoluteTotals.flow.min = 0;
}
if(absoluteTotals.power.min === Infinity) {
this.logger.warn(`Power min ${absoluteTotals.power.min} is Infinity. Setting to 0.`);
absoluteTotals.power.min = 0;
}
if(absoluteTotals.flow.max === -Infinity) {
this.logger.warn(`Flow max ${absoluteTotals.flow.max} is -Infinity. Setting to 0.`);
absoluteTotals.flow.max = 0;
}
if(absoluteTotals.power.max === -Infinity) {
this.logger.warn(`Power max ${absoluteTotals.power.max} is -Infinity. Setting to 0.`);
absoluteTotals.power.max = 0;
}
// Place data in object for external use
this.absoluteTotals = absoluteTotals;
return absoluteTotals;
}
@@ -134,26 +128,39 @@ class MachineGroup {
//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 };
const dynamicTotals = { flow: { min: Infinity, max: 0, act: 0 }, power: { min: Infinity, max: 0, act: 0 }, NCog : 0 };
this.logger.debug(`\n --------- Calculating dynamic totals for ${Object.keys(this.machines).length} machines. @ current pressure settings : ----------`);
Object.values(this.machines).forEach(machine => {
this.logger.debug(`Processing machine with id: ${machine.config.general.id}`);
this.logger.debug(`Current pressure settings: ${JSON.stringify(machine.predictFlow.currentF)}`);
//fetch min flow ever seen over all machines
const minFlow = machine.predictFlow.currentFxyYMin;
const maxFlow = machine.predictFlow.currentFxyYMax;
const minPower = machine.predictPower.currentFxyYMin;
const maxPower = machine.predictPower.currentFxyYMax;
const actFlow = machine.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue();
const actPower = machine.measurements.type("power").variant("predicted").position("atEquipment").getCurrentValue();
this.logger.debug(`Machine ${machine.config.general.id} - Min Flow: ${minFlow}, Max Flow: ${maxFlow}, Min Power: ${minPower}, Max Power: ${maxPower}, NCog: ${machine.NCog}`);
if( minFlow < dynamicTotals.flow.min ){ dynamicTotals.flow.min = minFlow; }
if( minPower < dynamicTotals.power.min ){ dynamicTotals.power.min = minPower; }
dynamicTotals.flow.max += maxFlow;
dynamicTotals.power.max += maxPower;
dynamicTotals.flow.act += actFlow;
dynamicTotals.power.act += actPower;
//fetch total Normalized Cog over all machines
dynamicTotals.NCog += machine.NCog;
});
// Place data in object for external use
this.dynamicTotals = dynamicTotals;
return dynamicTotals;
}
@@ -183,10 +190,16 @@ class MachineGroup {
}
handlePressureChange() {
this.logger.info("Pressure change detected.");
this.calcDynamicTotals();
this.logger.info("---------------------->>>>>>>>>>>>>>>>>>>>>>>>>>>Pressure change detected.");
// Recalculate totals
const { flow, power } = this.calcDynamicTotals();
this.logger.debug(`Dynamic Totals after pressure change - Flow: Min ${flow.min}, Max ${flow.max}, Act ${flow.act} | Power: Min ${power.min}, Max ${power.max}, Act ${power.act}`);
this.measurements.type("flow").variant("predicted").position("downstream").value(flow.act);
this.measurements.type("power").variant("predicted").position("atEquipment").value(power.act);
const { maxEfficiency, lowestEfficiency } = this.calcGroupEfficiency(this.machines);
const efficiency = this.measurements.type("efficiency").variant("predicted").position("downstream").getCurrentValue();
const efficiency = this.measurements.type("efficiency").variant("predicted").position("atEquipment").getCurrentValue();
this.calcDistanceBEP(efficiency,maxEfficiency,lowestEfficiency);
}
@@ -249,16 +262,13 @@ class MachineGroup {
// 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){
if( state === "off" || state === "coolingdown" || state === "stopping" || state === "emergencystop" || !validActionForMode){
return;
}
@@ -353,11 +363,6 @@ class MachineGroup {
}
// -------- 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);
@@ -369,8 +374,18 @@ class MachineGroup {
this.logger.debug(`Scaling set to: ${scaling}`);
}
async abortActiveMovements(reason = "new demand") {
await Promise.all(Object.values(this.machines).map(async machine => {
this.logger.warn(`Aborting active movements for machine ${machine.config.general.id} due to: ${reason}`);
if (typeof machine.abortMovement === "function") {
await machine.abortMovement(reason);
}
}));
}
//handle input from parent / user / UI
async optimalControl(Qd, powerCap = Infinity) {
try{
//we need to force the pressures of all machines to be equal to the highest pressure measured in the group
// this is to ensure a correct evaluation of the flow and power consumption
@@ -384,20 +399,25 @@ class MachineGroup {
const maxDownstream = Math.max(...pressures.map(p => p.downstream));
const minUpstream = Math.min(...pressures.map(p => p.upstream));
this.logger.debug(`Max downstream pressure: ${maxDownstream}, Min upstream pressure: ${minUpstream}`);
//set the pressures
Object.entries(this.machines).forEach(([machineId, machine]) => {
if(machine.state.getCurrentState() !== "operational" && machine.state.getCurrentState() !== "accelerating" && machine.state.getCurrentState() !== "decelerating"){
//Equilize pressures over all machines so we can make a proper calculation
machine.measurements.type("pressure").variant("measured").position("downstream").value(maxDownstream);
machine.measurements.type("pressure").variant("measured").position("upstream").value(minUpstream);
// after updating the measurement directly we need to force the update of the value OLIFANT this is not so clear now in the code
// we need to find a better way to do this but for now it works
machine.getMeasuredPressure();
}
});
//fetch dynamic totals
const dynamicTotals = this.dynamicTotals;
//update dynamic totals
const dynamicTotals = this.calcDynamicTotals();
const machineStates = Object.entries(this.machines).reduce((acc, [machineId, machine]) => {
acc[machineId] = machine.state.getCurrentState();
return acc;
@@ -420,7 +440,6 @@ class MachineGroup {
// 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){
@@ -432,35 +451,36 @@ class MachineGroup {
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("power").variant("predicted").position("atEquipment").value(bestResult.bestPower);
this.measurements.type("flow").variant("predicted").position("downstream").value(bestResult.bestFlow);
this.measurements.type("efficiency").variant("predicted").position("downstream").value(bestResult.bestFlow / bestResult.bestPower);
this.measurements.type("Ncog").variant("predicted").position("downstream").value(bestResult.bestCog);
this.measurements.type("efficiency").variant("predicted").position("atEquipment").value(bestResult.bestFlow / bestResult.bestPower);
this.measurements.type("Ncog").variant("predicted").position("atEquipment").value(bestResult.bestCog);
await Promise.all(Object.entries(this.machines).map(async ([machineId, machine]) => {
const pumpInfo = bestResult.bestCombination.find(item => item.machineId == machineId);
// Find the flow for this machine in the best combination
this.logger.debug(`Searching for machine ${machineId} with state ${machineStates[machineId]} in best combination.`);
const pumpInfo = bestResult.bestCombination.find(item => item.machineId == machineId);
let flow;
if(pumpInfo !== undefined){
flow = pumpInfo.flow;
} else {
this.logger.debug(`Machine ${machineId} not in best combination, setting flow to 0`);
this.logger.debug(`Machine ${machineId} not in best combination, setting flow control to 0`);
flow = 0;
}
if( (flow <= 0 ) && ( machineStates[machineId] === "operational" || machineStates[machineId] === "accelerating" || machineStates[machineId] === "decelerating" ) ){
await machine.handleInput("parent", "execSequence", "shutdown");
}
else if(machineStates[machineId] === "idle" && flow > 0){
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);
}
if(machineStates[machineId] === "operational" && flow > 0 ){
await machine.handleInput("parent", "flowMovement", flow);
}
}));
}
catch(err){
this.logger.error(err);
@@ -522,7 +542,7 @@ class MachineGroup {
.map(id => ({ id, machine: this.machines[id] }));
} else {
machinesInPriorityOrder = Object.entries(this.machines)
.map(([id, machine]) => ({ id: parseInt(id), machine }))
.map(([id, machine]) => ({ id: id, machine }))
.sort((a, b) => a.id - b.id);
}
return machinesInPriorityOrder;
@@ -531,10 +551,9 @@ class MachineGroup {
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 !(state === "off" || state === "coolingdown" || state === "stopping" || state === "emergencystop" || !validActionForMode);
});
return newList;
}
@@ -569,14 +588,6 @@ class MachineGroup {
// 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);
@@ -670,14 +681,16 @@ class MachineGroup {
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("power").variant("predicted").position("atEquipment").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);
this.measurements.type("efficiency").variant("predicted").position("atEquipment").value(totalFlow / totalPower);
this.measurements.type("Ncog").variant("predicted").position("atEquipment").value(totalCog);
this.logger.debug(`Flow distribution: ${JSON.stringify(flowDistribution)}`);
// Apply the flow distribution to machines
await Promise.all(flowDistribution.map(async ({ machineId, flow }) => {
const machine = this.machines[machineId];
this.logger.debug(this.machines[machineId].state);
const currentState = this.machines[machineId].state.getCurrentState();
if (flow <= 0 && (currentState === "operational" || currentState === "accelerating" || currentState === "decelerating")) {
@@ -782,8 +795,10 @@ class MachineGroup {
// fetch and store measurements
Object.entries(this.machines).forEach(([machineId, machine]) => {
const powerValue = machine.measurements.type("power").variant("predicted").position("upstream").getCurrentValue();
const powerValue = machine.measurements.type("power").variant("predicted").position("atEquipment").getCurrentValue();
const flowValue = machine.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue();
if (powerValue !== null) {
totalPower.push(powerValue);
}
@@ -792,10 +807,11 @@ class MachineGroup {
}
});
this.measurements.type("power").variant("predicted").position("upstream").value(totalPower.reduce((a, b) => a + b, 0));
this.measurements.type("power").variant("predicted").position("atEquipment").value(totalPower.reduce((a, b) => a + b, 0));
this.measurements.type("flow").variant("predicted").position("downstream").value(totalFlow.reduce((a, b) => a + b, 0));
if(totalPower.reduce((a, b) => a + b, 0) > 0){
this.measurements.type("efficiency").variant("predicted").position("downstream").value(totalFlow.reduce((a, b) => a + b, 0) / totalPower.reduce((a, b) => a + b, 0));
this.measurements.type("efficiency").variant("predicted").position("atEquipment").value(totalFlow.reduce((a, b) => a + b, 0) / totalPower.reduce((a, b) => a + b, 0));
}
}
@@ -804,49 +820,85 @@ class MachineGroup {
}
}
async handleInput(source, Qd, powerCap = Infinity, priorityList = null) {
async handleInput(source, demand, powerCap = Infinity, priorityList = null) {
if (!this.isValidSourceForMode(source, this.mode)) {
this.logger.warn(`Invalid source ${source} for mode ${this.mode}`);
return;
}
//abort current movements
await this.abortActiveMovements("new demand received");
const scaling = this.scaling;
const mode = this.mode;
let rawInput = Qd;
const dynamicTotals = this.calcDynamicTotals();
const demandQ = parseFloat(demand);
let demandQout = 0; // keep output Q by default 0 for safety
this.logger.debug(`Handling input from ${source}: Demand = ${demand}, Power Cap = ${powerCap}, Priority List = ${priorityList}`);
switch (scaling) {
case "absolute":
// 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;
if (isNaN(demandQ)) {
this.logger.warn(`Invalid absolute flow demand: ${demand}. Must be a number.`);
demandQout = 0;
return;
}
if (demandQ < absoluteTotals.flow.min) {
this.logger.warn(`Flow demand ${demandQ} is below minimum possible flow ${absoluteTotals.flow.min}. Capping to minimum flow.`);
demandQout = this.absoluteTotals.flow.min;
} else if (demandQout > absoluteTotals.flow.max) {
this.logger.warn(`Flow demand ${demandQ} is above maximum possible flow ${absoluteTotals.flow.max}. Capping to maximum flow.`);
demandQout = absoluteTotals.flow.max;
}else if(demandQout <= 0){
this.logger.debug(`Turning machines off`);
demandQout = 0;
//return early and turn all machines off
this.turnOffAllMachines();
return;
}
break;
case "normalized":
// 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);
this.logger.debug(`Normalizing flow demand: ${demandQ} with min: ${dynamicTotals.flow.min} and max: ${dynamicTotals.flow.max}`);
if(demand < 0){
this.logger.debug(`Turning machines off`);
demandQout = 0;
//return early and turn all machines off
this.turnOffAllMachines();
return;
}
else{
// Scale demand to 0-100% linear between min and max flow this is auto capped
demandQout = this.interpolation.interpolate_lin_single_point(demandQ, 0, 100, dynamicTotals.flow.min, dynamicTotals.flow.max );
this.logger.debug(`Normalized flow demand ${demandQ}% to: ${demandQout} Q units`);
}
break;
}
// Execute control based on mode
switch(mode) {
case "prioritycontrol":
await this.equalFlowControl(Qd,powerCap,priorityList);
this.logger.debug(`Calculating prio control. Input flow demand: ${demandQ} scaling : ${scaling} -> ${demandQout}`);
await this.equalFlowControl(demandQout,powerCap,priorityList);
break;
case "prioritypercentagecontrol":
this.logger.debug(`Calculating prio percentage control. Input flow demand: ${demandQ} scaling : ${scaling} -> ${demandQout}`);
if(scaling !== "normalized"){
this.logger.warn("Priority percentage control is only valid with normalized scaling.");
return;
}
await this.prioPercentageControl(rawInput,priorityList);
await this.prioPercentageControl(demandQout,priorityList);
break;
case "optimalcontrol":
await this.optimalControl(Qd,powerCap);
this.logger.debug(`Calculating optimal control. Input flow demand: ${demandQ} scaling : ${scaling} -> ${demandQout}`);
await this.optimalControl(demandQout,powerCap);
break;
default:
this.logger.warn(`${mode} is not a valid mode.`);
break;
}
@@ -857,8 +909,14 @@ class MachineGroup {
}
setMode(source,mode) {
this.isValidSourceForMode(source, mode) ? this.mode = mode : this.logger.warn(`Invalid source ${source} for mode ${mode}`);
async turnOffAllMachines(){
await Promise.all(Object.entries(this.machines).map(async ([machineId, machine]) => {
if (this.isMachineActive(machineId)) { await machine.handleInput("parent", "execSequence", "shutdown"); }
}));
}
setMode(mode) {
this.mode = mode;
}
getOutput() {
@@ -871,6 +929,7 @@ class MachineGroup {
this.measurements.getVariants(type).forEach(variant => {
const downstreamVal = this.measurements.type(type).variant(variant).position("downstream").getCurrentValue();
const atEquipmentVal = this.measurements.type(type).variant(variant).position("atEquipment").getCurrentValue();
const upstreamVal = this.measurements.type(type).variant(variant).position("upstream").getCurrentValue();
if (downstreamVal != null) {
@@ -879,6 +938,9 @@ class MachineGroup {
if (upstreamVal != null) {
output[`upstream_${variant}_${type}`] = upstreamVal;
}
if (atEquipmentVal != null) {
output[`atEquipment_${variant}_${type}`] = atEquipmentVal;
}
if (downstreamVal != null && upstreamVal != null) {
const diffVal = this.measurements.type(type).variant(variant).difference().value;
output[`differential_${variant}_${type}`] = diffVal;
@@ -902,17 +964,17 @@ class MachineGroup {
}
module.exports = MachineGroup;
/*
const Machine = require('../../rotatingMachine/src/specificClass');
const Measurement = require('../../measurement/src/specificClass');
const specs = require('../../generalFunctions/datasets/assetData/curves/hidrostal-H05K-S03R.json');
const { number } = require("../../generalFunctions/src/convert/lodash/lodash._objecttypes");
const { max } = require("mathjs");
function createBaseMachineConfig(machineNum, name,specs) {
return {
general: {
logging: { enabled: true, logLevel: "warn" },
logging: { enabled: true, logLevel: "debug" },
name: name,
id: machineNum,
unit: "m3/h"
@@ -950,6 +1012,23 @@ function createBaseMachineConfig(machineNum, name,specs) {
};
}
function createStateConfig(){
return {
time:{
starting: 1,
stopping: 1,
warmingup: 1,
coolingdown: 1,
emergencystop: 1
},
movement:{
mode:"dynspeed",
speed:100,
maxSpeed: 1000
}
}
};
function createBaseMachineGroupConfig(name) {
return {
general: {
@@ -970,9 +1049,13 @@ function createBaseMachineGroupConfig(name) {
}
const machineGroupConfig = createBaseMachineGroupConfig("testmachinegroup");
const stateConfigs = {};
const machineConfigs = {};
machineConfigs[1]= createBaseMachineConfig(1,"testmachine",specs);
machineConfigs[2] = createBaseMachineConfig(2,"testmachine2",specs);
stateConfigs[1] = createStateConfig();
stateConfigs[2] = createStateConfig();
machineConfigs[1]= createBaseMachineConfig("asdfkj;asdf","testmachine",specs);
machineConfigs[2] = createBaseMachineConfig("asdfkj;asdf2","testmachine2",specs);
const ptConfig = {
general: {
@@ -1002,14 +1085,16 @@ async function makeMachines(){
const pt1 = new Measurement(ptConfig);
const numofMachines = 2;
for(let i = 1; i <= numofMachines; i++){
const machine = new Machine(machineConfigs[i]);
const machine = new Machine(machineConfigs[i],stateConfigs[i]);
//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");
Object.keys(mg.machines).forEach(machineId => {
mg.machines[machineId].childRegistrationUtils.registerChild(pt1, "downstream");
});
mg.setMode("prioritycontrol");
mg.setScaling("normalized");
const absMax = mg.dynamicTotals.flow.max;
@@ -1018,14 +1103,13 @@ async function makeMachines(){
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("------------------------------------");
@@ -1038,24 +1122,24 @@ async function makeMachines(){
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){
//*//*
for(let demand = 0 ; demand <= 50 ; demand += 1){
//set pressure
console.log("------------------------------------");
console.log(`TESTING: processing demand of ${demand}`);
await mg.handleInput("parent",demand);
console.log(mg.machines[1].state.getCurrentState());
console.log(mg.machines[2].state.getCurrentState());
Object.keys(mg.machines).forEach(machineId => {
console.log(mg.machines[machineId].state.getCurrentState());
});
console.log(`updating pressure to 1400 mbar`);
pt1.calculateInput(1400);
console.log("Waiting for 0.2 sec ");
//await new Promise(resolve => setTimeout(resolve, 200));
console.log("------------------------------------");
}
@@ -1071,4 +1155,4 @@ async function makeMachines(){
makeMachines();
*/
//*/