Compare commits

...

11 Commits

Author SHA1 Message Date
681856104d Merge pull request 'changed colours, description based on s88' (#2) from dev-Rene into main
Reviewed-on: #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: #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
znetsixe
c62071992d working on a stable version 2025-09-23 11:19:22 +02:00
4 changed files with 605 additions and 205 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,28 +58,32 @@ class nodeClass {
}
_updateNodeStatus() {
//console.log('Updating node status...');
const mg = this.source;
const mode = mg.mode;
const scaling = mg.scaling;
const totalFlow =
Math.round(
mg.measurements
.type("flow")
.variant("predicted")
.position("downstream")
.getCurrentValue() * 1
) / 1;
const totalPower =
Math.round(
mg.measurements
.type("power")
.variant("predicted")
.position("upstream")
.getCurrentValue() * 1
) / 1;
// Add safety checks for measurements
const totalFlow = mg.measurements
?.type("flow")
?.variant("predicted")
?.position("downstream")
?.getCurrentValue() || 0;
const totalPower = mg.measurements
?.type("power")
?.variant("predicted")
?.position("atEquipment")
?.getCurrentValue() || 0;
// Calculate total capacity based on available machines
const availableMachines = Object.values(mg.machines).filter((machine) => {
// Calculate total capacity based on available machines with safety checks
const availableMachines = Object.values(mg.machines || {}).filter((machine) => {
// Safety check: ensure machine and machine.state exist
if (!machine || !machine.state || typeof machine.state.getCurrentState !== 'function') {
console.warn(`Machine missing or invalid:`, machine?.config?.general?.id || 'unknown');
return false;
}
const state = machine.state.getCurrentState();
const mode = machine.currentMode;
return !(
@@ -89,29 +93,27 @@ class nodeClass {
);
});
const totalCapacity = Math.round(mg.dynamicTotals.flow.max * 1) / 1;
const totalCapacity = Math.round((mg.dynamicTotals?.flow?.max || 0) * 1) / 1;
// Determine overall status based on available machines
const status =
availableMachines.length > 0
? `${availableMachines.length} machine(s) connected`
: "No machines";
const status = availableMachines.length > 0
? `${availableMachines.length} machine(s) connected`
: "No machines";
let scalingSymbol = "";
switch (scaling.toLowerCase()) {
switch ((scaling || "").toLowerCase()) {
case "absolute":
scalingSymbol = "Ⓐ"; // Clear symbol for Absolute mode
scalingSymbol = "Ⓐ";
break;
case "normalized":
scalingSymbol = "Ⓝ"; // Clear symbol for Normalized mode
scalingSymbol = "Ⓝ";
break;
default:
scalingSymbol = mode;
scalingSymbol = mode || "";
break;
}
// Generate status text in a single line
const text = ` ${mode} | ${scalingSymbol}: 💨=${totalFlow}/${totalCapacity} | ⚡=${totalPower} | ${status}`;
const text = ` ${mode || 'Unknown'} | ${scalingSymbol}: 💨=${Math.round(totalFlow)}/${totalCapacity} | ⚡=${Math.round(totalPower)} | ${status}`;
return {
fill: availableMachines.length > 0 ? "green" : "red",
@@ -197,24 +199,36 @@ class nodeClass {
const RED = this.RED;
switch (msg.topic) {
case "registerChild":
//console.log(`Registering child in mgc: ${msg.payload}`);
const childId = msg.payload;
const childObj = RED.nodes.getNode(childId);
mg.childRegistrationUtils.registerChild(
childObj.source,
msg.positionVsParent
);
break;
console.log(`Registering child in mgc: ${msg.payload}`);
const childId = msg.payload;
const childObj = RED.nodes.getNode(childId);
// Debug: Check what we're getting
console.log(`Child object:`, childObj ? 'found' : 'NOT FOUND');
console.log(`Child source:`, childObj?.source ? 'exists' : 'MISSING');
if (childObj?.source) {
console.log(`Child source type:`, childObj.source.constructor.name);
console.log(`Child has state:`, !!childObj.source.state);
}
mg.childRegistrationUtils.registerChild(
childObj.source,
msg.positionVsParent
);
// Debug: Check machines after registration
console.log(`Total machines after registration:`, Object.keys(mg.machines || {}).length);
break;
case "setMode":
const mode = msg.payload;
const source = "parent";
mg.setMode(source, mode);
mg.setMode(mode);
break;
case "setScaling":
const scaling = msg.payload;
mg.setScaling(scaling);
break;
case "Qd":

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,6 +43,28 @@ 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.`);
//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();
});
}
}
@@ -111,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; }
@@ -119,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;
}
@@ -126,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;
}
@@ -175,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);
}
@@ -207,6 +228,7 @@ class MachineGroup {
checkSpecialCases(machines, Qd) {
Object.values(machines).forEach(machine => {
const state = machine.state.getCurrentState();
const mode = machine.currentMode;
@@ -240,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;
}
@@ -344,23 +363,29 @@ 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);
}
setScaling(scaling) {
const scalingSet = new Set(defaultConfig.scaling.current.rules.values.map( (value) => value.value));
const scalingSet = new Set(this.defaultConfig.scaling.current.rules.values.map( (value) => value.value));
scalingSet.has(scaling)? this.scaling = scaling : this.logger.warn(`${scaling} is not a valid scaling option.`);
this.logger.debug(`Scaling set to: ${scaling}`);
}
async abortActiveMovements(reason = "new demand") {
await Promise.all(Object.values(this.machines).map(async machine => {
this.logger.warn(`Aborting active movements for machine ${machine.config.general.id} due to: ${reason}`);
if (typeof machine.abortMovement === "function") {
await machine.abortMovement(reason);
}
}));
}
//handle input from parent / user / UI
async optimalControl(Qd, powerCap = Infinity) {
try{
//we need to force the pressures of all machines to be equal to the highest pressure measured in the group
// this is to ensure a correct evaluation of the flow and power consumption
@@ -374,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;
@@ -409,48 +439,48 @@ class MachineGroup {
}
// fetch all valid combinations that meet expectations
const combinations = this.validPumpCombinations(this.machines, Qd, powerCap);
//
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("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);
@@ -512,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;
@@ -521,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;
}
@@ -559,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);
@@ -660,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")) {
@@ -772,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);
}
@@ -782,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));
}
}
@@ -794,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;
}
@@ -847,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() {
@@ -861,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) {
@@ -869,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;
@@ -892,31 +964,31 @@ class MachineGroup {
}
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) {
const Machine = require('../../rotatingMachine/src/specificClass');
const Measurement = require('../../measurement/src/specificClass');
const specs = require('../../generalFunctions/datasets/assetData/curves/hidrostal-H05K-S03R.json');
const { max } = require("mathjs");
function createBaseMachineConfig(machineNum, name,specs) {
return {
general: {
logging: { enabled: true, logLevel: "warn" },
logging: { enabled: true, logLevel: "debug" },
name: name,
id: machineNum,
unit: "m3/h"
},
functionality: {
softwareType: "machine",
role: "RotationalDeviceController"
role: "rotationaldevicecontroller"
},
asset: {
type: "pump",
subType: "Centrifugal",
model: "TestModel",
supplier: "Hydrostal",
machineCurve: specs[0].machineCurve
category: "pump",
type: "centrifugal",
model: "hidrostal-h05k-s03r",
supplier: "hydrostal",
machineCurve: specs
},
mode: {
current: "auto",
@@ -940,6 +1012,23 @@ function createBaseMachineConfig(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: {
@@ -947,8 +1036,8 @@ function createBaseMachineGroupConfig(name) {
name: name
},
functionality: {
softwareType: "machineGroup",
role: "GroupController"
softwareType: "machinegroup",
role: "groupcontroller"
},
scaling: {
current: "normalized"
@@ -959,22 +1048,30 @@ function createBaseMachineGroupConfig(name) {
};
}
const machineGroupConfig = createBaseMachineGroupConfig("TestMachineGroup");
const machineConfig = createBaseMachineConfig("TestMachine",specs);
const machineGroupConfig = createBaseMachineGroupConfig("testmachinegroup");
const stateConfigs = {};
const machineConfigs = {};
stateConfigs[1] = createStateConfig();
stateConfigs[2] = createStateConfig();
machineConfigs[1]= createBaseMachineConfig("asdfkj;asdf","testmachine",specs);
machineConfigs[2] = createBaseMachineConfig("asdfkj;asdf2","testmachine2",specs);
const ptConfig = {
general: {
logging: { enabled: true, logLevel: "debug" },
name: "TestPT",
name: "testpt",
id: "0",
unit: "mbar",
},
functionality: {
softwareType: "measurement",
role: "Sensor"
role: "sensor"
},
asset: {
type: "sensor",
subType: "pressure",
model: "TestModel",
category: "sensor",
type: "pressure",
model: "testmodel",
supplier: "vega"
},
scaling:{
@@ -987,15 +1084,17 @@ 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);
for(let i = 1; i <= numofMachines; 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;
@@ -1004,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("------------------------------------");
@@ -1024,22 +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){
//set pressure
console.log("------------------------------------");
//*//*
for(let demand = 0 ; demand <= 50 ; demand += 1){
//set pressure
console.log(`TESTING: processing demand of ${demand}`);
await mg.handleInput("parent",demand);
Object.keys(mg.machines).forEach(machineId => {
console.log(mg.machines[machineId].state.getCurrentState());
});
console.log(`updating pressure to 1400 mbar`);
pt1.calculateInput(1400);
console.log("Waiting for 0.2 sec ");
//await new Promise(resolve => setTimeout(resolve, 200));
console.log("------------------------------------");
}
@@ -1054,4 +1154,5 @@ async function makeMachines(){
makeMachines();
//*/
//*/