forked from RnD/machineGroupControl
Compare commits
8 Commits
c62071992d
...
e0526250c2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0526250c2 | ||
|
|
426d45890f | ||
|
|
8c59a921d5 | ||
|
|
15501e8b1d | ||
|
|
b4364094c6 | ||
|
|
a55c6bdbea | ||
|
|
ac9d1b4fdd | ||
|
|
cbc0840f0c |
23
mgc.html
23
mgc.html
@@ -1,15 +1,12 @@
|
|||||||
<!--
|
<!--
|
||||||
brabantse delta kleuren:
|
| S88-niveau | Primair (blokkleur) | Tekstkleur |
|
||||||
#eaf4f1
|
| ---------------------- | ------------------- | ---------- |
|
||||||
#86bbdd
|
| **Area** | `#0f52a5` | wit |
|
||||||
#bad33b
|
| **Process Cell** | `#0c99d9` | wit |
|
||||||
#0c99d9
|
| **Unit** | `#50a8d9` | zwart |
|
||||||
#a9daee
|
| **Equipment (Module)** | `#86bbdd` | zwart |
|
||||||
#0f52a5
|
| **Control Module** | `#a9daee` | zwart |
|
||||||
#50a8d9
|
|
||||||
#cade63
|
|
||||||
#4f8582
|
|
||||||
#c4cce0
|
|
||||||
-->
|
-->
|
||||||
<script src="/machineGroupControl/menu.js"></script> <!-- Load the menu script for dynamic dropdowns -->
|
<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 -->
|
<script src="/machineGroupControl/configData.js"></script> <!-- Load the config script for node information -->
|
||||||
@@ -17,7 +14,7 @@
|
|||||||
<script>
|
<script>
|
||||||
RED.nodes.registerType('machineGroupControl',{
|
RED.nodes.registerType('machineGroupControl',{
|
||||||
category: "EVOLV",
|
category: "EVOLV",
|
||||||
color: "#eaf4f1",
|
color: "#50a8d9",
|
||||||
defaults: {
|
defaults: {
|
||||||
// Define default properties
|
// Define default properties
|
||||||
name: { value: "" },
|
name: { value: "" },
|
||||||
@@ -39,7 +36,7 @@
|
|||||||
outputs:3,
|
outputs:3,
|
||||||
inputLabels: ["Input"],
|
inputLabels: ["Input"],
|
||||||
outputLabels: ["process", "dbase", "parent"],
|
outputLabels: ["process", "dbase", "parent"],
|
||||||
icon: "font-awesome/fa-tachometer",
|
icon: "font-awesome/fa-cogs",
|
||||||
|
|
||||||
label: function () {
|
label: function () {
|
||||||
return this.positionIcon + " " + "machineGroup";
|
return this.positionIcon + " " + "machineGroup";
|
||||||
|
|||||||
288
src/groupcontrol.test.js
Normal file
288
src/groupcontrol.test.js
Normal 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();
|
||||||
@@ -58,6 +58,7 @@ class nodeClass {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_updateNodeStatus() {
|
_updateNodeStatus() {
|
||||||
|
//console.log('Updating node status...');
|
||||||
const mg = this.source;
|
const mg = this.source;
|
||||||
const mode = mg.mode;
|
const mode = mg.mode;
|
||||||
const scaling = mg.scaling;
|
const scaling = mg.scaling;
|
||||||
@@ -72,7 +73,7 @@ class nodeClass {
|
|||||||
const totalPower = mg.measurements
|
const totalPower = mg.measurements
|
||||||
?.type("power")
|
?.type("power")
|
||||||
?.variant("predicted")
|
?.variant("predicted")
|
||||||
?.position("upstream")
|
?.position("atEquipment")
|
||||||
?.getCurrentValue() || 0;
|
?.getCurrentValue() || 0;
|
||||||
|
|
||||||
// Calculate total capacity based on available machines with safety checks
|
// Calculate total capacity based on available machines with safety checks
|
||||||
@@ -221,8 +222,7 @@ class nodeClass {
|
|||||||
|
|
||||||
case "setMode":
|
case "setMode":
|
||||||
const mode = msg.payload;
|
const mode = msg.payload;
|
||||||
const source = "parent";
|
mg.setMode(mode);
|
||||||
mg.setMode(source, mode);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "setScaling":
|
case "setScaling":
|
||||||
|
|||||||
@@ -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
|
//load local dependencies
|
||||||
const EventEmitter = require("events");
|
const EventEmitter = require("events");
|
||||||
const {logger,configUtils,configManager, MeasurementContainer, interpolation , childRegistrationUtils} = require('generalFunctions');
|
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) {
|
registerChild(child,softwareType) {
|
||||||
this.logger.debug('Setting up childs specific for this class');
|
this.logger.debug('Setting up childs specific for this class');
|
||||||
@@ -87,13 +43,27 @@ class MachineGroup {
|
|||||||
if(softwareType == "machine"){
|
if(softwareType == "machine"){
|
||||||
// Check if the machine is already registered
|
// 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.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 machine pressure changes
|
||||||
// Listen for changes in the child machine
|
this.logger.debug(`Listening for pressure changes from machine ${child.config.general.id}`);
|
||||||
child.emitter.on('stateChange', () => this.handleChildChange());
|
|
||||||
child.emitter.on('pressureChange', () => this.handlePressureChange());
|
child.measurements.emitter.on("pressure.measured.differential", (eventData) => {
|
||||||
child.emitter.on('ncogChange', () => this.handleChildChange());
|
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; }
|
if( maxPower > totals.power.max ){ totals.power.max = maxPower; }
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
//surplus machines for max flow and power
|
//surplus machines for max flow and power
|
||||||
if( totals.flow.min < absoluteTotals.flow.min ){ absoluteTotals.flow.min = totals.flow.min; }
|
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; }
|
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;
|
return absoluteTotals;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -134,26 +128,39 @@ class MachineGroup {
|
|||||||
//max and min current flow and power based on their actual pressure curve
|
//max and min current flow and power based on their actual pressure curve
|
||||||
calcDynamicTotals() {
|
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 => {
|
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
|
//fetch min flow ever seen over all machines
|
||||||
const minFlow = machine.predictFlow.currentFxyYMin;
|
const minFlow = machine.predictFlow.currentFxyYMin;
|
||||||
const maxFlow = machine.predictFlow.currentFxyYMax;
|
const maxFlow = machine.predictFlow.currentFxyYMax;
|
||||||
const minPower = machine.predictPower.currentFxyYMin;
|
const minPower = machine.predictPower.currentFxyYMin;
|
||||||
const maxPower = machine.predictPower.currentFxyYMax;
|
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( minFlow < dynamicTotals.flow.min ){ dynamicTotals.flow.min = minFlow; }
|
||||||
if( minPower < dynamicTotals.power.min ){ dynamicTotals.power.min = minPower; }
|
if( minPower < dynamicTotals.power.min ){ dynamicTotals.power.min = minPower; }
|
||||||
|
|
||||||
dynamicTotals.flow.max += maxFlow;
|
dynamicTotals.flow.max += maxFlow;
|
||||||
dynamicTotals.power.max += maxPower;
|
dynamicTotals.power.max += maxPower;
|
||||||
|
dynamicTotals.flow.act += actFlow;
|
||||||
|
dynamicTotals.power.act += actPower;
|
||||||
|
|
||||||
//fetch total Normalized Cog over all machines
|
//fetch total Normalized Cog over all machines
|
||||||
dynamicTotals.NCog += machine.NCog;
|
dynamicTotals.NCog += machine.NCog;
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Place data in object for external use
|
||||||
|
this.dynamicTotals = dynamicTotals;
|
||||||
|
|
||||||
return dynamicTotals;
|
return dynamicTotals;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,10 +190,16 @@ class MachineGroup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handlePressureChange() {
|
handlePressureChange() {
|
||||||
this.logger.info("Pressure change detected.");
|
this.logger.info("---------------------->>>>>>>>>>>>>>>>>>>>>>>>>>>Pressure change detected.");
|
||||||
this.calcDynamicTotals();
|
// 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 { 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);
|
this.calcDistanceBEP(efficiency,maxEfficiency,lowestEfficiency);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,16 +262,13 @@ class MachineGroup {
|
|||||||
|
|
||||||
// Generate all possible subsets of machines (power set)
|
// Generate all possible subsets of machines (power set)
|
||||||
Object.keys(machines).forEach(machineId => {
|
Object.keys(machines).forEach(machineId => {
|
||||||
//machineId = parseInt(machineId);
|
|
||||||
|
|
||||||
const state = machines[machineId].state.getCurrentState();
|
const state = machines[machineId].state.getCurrentState();
|
||||||
|
|
||||||
const validSourceForMode = machines[machineId].isValidSourceForMode("parent", "auto");
|
|
||||||
const validActionForMode = machines[machineId].isValidActionForMode("execSequence", "auto");
|
const validActionForMode = machines[machineId].isValidActionForMode("execSequence", "auto");
|
||||||
|
|
||||||
|
|
||||||
// Reasons why a machine is not valid for the combination
|
// 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,11 +363,6 @@ class MachineGroup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// -------- Mode and Input Management -------- //
|
// -------- Mode and Input Management -------- //
|
||||||
isValidSourceForMode(source, mode) {
|
|
||||||
const allowedSourcesSet = this.config.mode.allowedSources[mode] || [];
|
|
||||||
return allowedSourcesSet.has(source);
|
|
||||||
}
|
|
||||||
|
|
||||||
isValidActionForMode(action, mode) {
|
isValidActionForMode(action, mode) {
|
||||||
const allowedActionsSet = this.config.mode.allowedActions[mode] || [];
|
const allowedActionsSet = this.config.mode.allowedActions[mode] || [];
|
||||||
return allowedActionsSet.has(action);
|
return allowedActionsSet.has(action);
|
||||||
@@ -369,8 +374,18 @@ class MachineGroup {
|
|||||||
this.logger.debug(`Scaling set to: ${scaling}`);
|
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
|
//handle input from parent / user / UI
|
||||||
async optimalControl(Qd, powerCap = Infinity) {
|
async optimalControl(Qd, powerCap = Infinity) {
|
||||||
|
|
||||||
try{
|
try{
|
||||||
//we need to force the pressures of all machines to be equal to the highest pressure measured in the group
|
//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
|
// 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 maxDownstream = Math.max(...pressures.map(p => p.downstream));
|
||||||
const minUpstream = Math.min(...pressures.map(p => p.upstream));
|
const minUpstream = Math.min(...pressures.map(p => p.upstream));
|
||||||
|
|
||||||
|
this.logger.debug(`Max downstream pressure: ${maxDownstream}, Min upstream pressure: ${minUpstream}`);
|
||||||
|
|
||||||
//set the pressures
|
//set the pressures
|
||||||
Object.entries(this.machines).forEach(([machineId, machine]) => {
|
Object.entries(this.machines).forEach(([machineId, machine]) => {
|
||||||
if(machine.state.getCurrentState() !== "operational" && machine.state.getCurrentState() !== "accelerating" && machine.state.getCurrentState() !== "decelerating"){
|
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("downstream").value(maxDownstream);
|
||||||
machine.measurements.type("pressure").variant("measured").position("upstream").value(minUpstream);
|
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
|
// 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
|
// we need to find a better way to do this but for now it works
|
||||||
machine.getMeasuredPressure();
|
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]) => {
|
const machineStates = Object.entries(this.machines).reduce((acc, [machineId, machine]) => {
|
||||||
acc[machineId] = machine.state.getCurrentState();
|
acc[machineId] = machine.state.getCurrentState();
|
||||||
return acc;
|
return acc;
|
||||||
@@ -419,48 +439,48 @@ class MachineGroup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// fetch all valid combinations that meet expectations
|
// 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);
|
const bestResult = this.calcBestCombination(combinations, Qd);
|
||||||
|
|
||||||
if(bestResult.bestCombination === null){
|
if(bestResult.bestCombination === null){
|
||||||
this.logger.warn(`Demand: ${Qd.toFixed(2)} -> No valid combination found => not updating control `);
|
this.logger.warn(`Demand: ${Qd.toFixed(2)} -> No valid combination found => not updating control `);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const debugInfo = bestResult.bestCombination.map(({ machineId, flow }) => `${machineId}: ${flow.toFixed(2)} units`).join(" | ");
|
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)}`);
|
this.logger.debug(`Moving to demand: ${Qd.toFixed(2)} -> Pumps: [${debugInfo}] => Total Power: ${bestResult.bestPower.toFixed(2)}`);
|
||||||
|
|
||||||
//store the total delivered power
|
//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("flow").variant("predicted").position("downstream").value(bestResult.bestFlow);
|
||||||
this.measurements.type("efficiency").variant("predicted").position("downstream").value(bestResult.bestFlow / bestResult.bestPower);
|
this.measurements.type("efficiency").variant("predicted").position("atEquipment").value(bestResult.bestFlow / bestResult.bestPower);
|
||||||
this.measurements.type("Ncog").variant("predicted").position("downstream").value(bestResult.bestCog);
|
this.measurements.type("Ncog").variant("predicted").position("atEquipment").value(bestResult.bestCog);
|
||||||
|
|
||||||
await Promise.all(Object.entries(this.machines).map(async ([machineId, machine]) => {
|
await Promise.all(Object.entries(this.machines).map(async ([machineId, machine]) => {
|
||||||
|
// Find the flow for this machine in the best combination
|
||||||
const pumpInfo = bestResult.bestCombination.find(item => item.machineId == machineId);
|
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;
|
let flow;
|
||||||
if(pumpInfo !== undefined){
|
if(pumpInfo !== undefined){
|
||||||
flow = pumpInfo.flow;
|
flow = pumpInfo.flow;
|
||||||
} else {
|
} 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;
|
flow = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if( (flow <= 0 ) && ( machineStates[machineId] === "operational" || machineStates[machineId] === "accelerating" || machineStates[machineId] === "decelerating" ) ){
|
if( (flow <= 0 ) && ( machineStates[machineId] === "operational" || machineStates[machineId] === "accelerating" || machineStates[machineId] === "decelerating" ) ){
|
||||||
await machine.handleInput("parent", "execSequence", "shutdown");
|
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");
|
await machine.handleInput("parent", "execSequence", "startup");
|
||||||
}
|
|
||||||
else if(machineStates[machineId] === "operational" && flow > 0 ){
|
|
||||||
await machine.handleInput("parent", "flowMovement", flow);
|
await machine.handleInput("parent", "flowMovement", flow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(machineStates[machineId] === "operational" && flow > 0 ){
|
||||||
|
await machine.handleInput("parent", "flowMovement", flow);
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
}
|
}
|
||||||
catch(err){
|
catch(err){
|
||||||
this.logger.error(err);
|
this.logger.error(err);
|
||||||
@@ -522,7 +542,7 @@ class MachineGroup {
|
|||||||
.map(id => ({ id, machine: this.machines[id] }));
|
.map(id => ({ id, machine: this.machines[id] }));
|
||||||
} else {
|
} else {
|
||||||
machinesInPriorityOrder = Object.entries(this.machines)
|
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);
|
.sort((a, b) => a.id - b.id);
|
||||||
}
|
}
|
||||||
return machinesInPriorityOrder;
|
return machinesInPriorityOrder;
|
||||||
@@ -531,10 +551,9 @@ class MachineGroup {
|
|||||||
filterOutUnavailableMachines(list) {
|
filterOutUnavailableMachines(list) {
|
||||||
const newList = list.filter(({ id, machine }) => {
|
const newList = list.filter(({ id, machine }) => {
|
||||||
const state = machine.state.getCurrentState();
|
const state = machine.state.getCurrentState();
|
||||||
const validSourceForMode = machine.isValidSourceForMode("parent", "auto");
|
|
||||||
const validActionForMode = machine.isValidActionForMode("execSequence", "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;
|
return newList;
|
||||||
}
|
}
|
||||||
@@ -569,14 +588,6 @@ class MachineGroup {
|
|||||||
// Update dynamic totals
|
// Update dynamic totals
|
||||||
const dynamicTotals = this.calcDynamicTotals();
|
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
|
// Cap flow demand to min/max possible values
|
||||||
Qd = this.capFlowDemand(Qd,dynamicTotals);
|
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)}`);
|
this.logger.debug(`Priority control for demand: ${totalFlow.toFixed(2)} -> Active pumps: [${debugInfo}] => Total Power: ${totalPower.toFixed(2)}`);
|
||||||
|
|
||||||
// Store measurements
|
// 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("flow").variant("predicted").position("downstream").value(totalFlow);
|
||||||
this.measurements.type("efficiency").variant("predicted").position("downstream").value(totalFlow / totalPower);
|
this.measurements.type("efficiency").variant("predicted").position("atEquipment").value(totalFlow / totalPower);
|
||||||
this.measurements.type("Ncog").variant("predicted").position("downstream").value(totalCog);
|
this.measurements.type("Ncog").variant("predicted").position("atEquipment").value(totalCog);
|
||||||
|
|
||||||
|
this.logger.debug(`Flow distribution: ${JSON.stringify(flowDistribution)}`);
|
||||||
// Apply the flow distribution to machines
|
// Apply the flow distribution to machines
|
||||||
await Promise.all(flowDistribution.map(async ({ machineId, flow }) => {
|
await Promise.all(flowDistribution.map(async ({ machineId, flow }) => {
|
||||||
const machine = this.machines[machineId];
|
const machine = this.machines[machineId];
|
||||||
|
this.logger.debug(this.machines[machineId].state);
|
||||||
const currentState = this.machines[machineId].state.getCurrentState();
|
const currentState = this.machines[machineId].state.getCurrentState();
|
||||||
|
|
||||||
if (flow <= 0 && (currentState === "operational" || currentState === "accelerating" || currentState === "decelerating")) {
|
if (flow <= 0 && (currentState === "operational" || currentState === "accelerating" || currentState === "decelerating")) {
|
||||||
@@ -782,8 +795,10 @@ class MachineGroup {
|
|||||||
|
|
||||||
// fetch and store measurements
|
// fetch and store measurements
|
||||||
Object.entries(this.machines).forEach(([machineId, machine]) => {
|
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();
|
const flowValue = machine.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue();
|
||||||
|
|
||||||
if (powerValue !== null) {
|
if (powerValue !== null) {
|
||||||
totalPower.push(powerValue);
|
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));
|
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){
|
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) {
|
||||||
|
|
||||||
|
//abort current movements
|
||||||
if (!this.isValidSourceForMode(source, this.mode)) {
|
await this.abortActiveMovements("new demand received");
|
||||||
this.logger.warn(`Invalid source ${source} for mode ${this.mode}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const scaling = this.scaling;
|
const scaling = this.scaling;
|
||||||
const mode = this.mode;
|
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) {
|
switch (scaling) {
|
||||||
case "absolute":
|
case "absolute":
|
||||||
// No scaling needed but cap range
|
if (isNaN(demandQ)) {
|
||||||
if (Qd < this.absoluteTotals.flow.min) {
|
this.logger.warn(`Invalid absolute flow demand: ${demand}. Must be a number.`);
|
||||||
this.logger.warn(`Flow demand ${Qd} is below minimum possible flow ${this.absoluteTotals.flow.min}. Capping to minimum flow.`);
|
demandQout = 0;
|
||||||
Qd = this.absoluteTotals.flow.min;
|
return;
|
||||||
} 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 (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;
|
break;
|
||||||
|
|
||||||
case "normalized":
|
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;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Execute control based on mode
|
||||||
switch(mode) {
|
switch(mode) {
|
||||||
case "prioritycontrol":
|
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;
|
break;
|
||||||
|
|
||||||
case "prioritypercentagecontrol":
|
case "prioritypercentagecontrol":
|
||||||
|
this.logger.debug(`Calculating prio percentage control. Input flow demand: ${demandQ} scaling : ${scaling} -> ${demandQout}`);
|
||||||
if(scaling !== "normalized"){
|
if(scaling !== "normalized"){
|
||||||
this.logger.warn("Priority percentage control is only valid with normalized scaling.");
|
this.logger.warn("Priority percentage control is only valid with normalized scaling.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await this.prioPercentageControl(rawInput,priorityList);
|
await this.prioPercentageControl(demandQout,priorityList);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "optimalcontrol":
|
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;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -857,8 +909,14 @@ class MachineGroup {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setMode(source,mode) {
|
async turnOffAllMachines(){
|
||||||
this.isValidSourceForMode(source, mode) ? this.mode = mode : this.logger.warn(`Invalid source ${source} for mode ${mode}`);
|
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() {
|
getOutput() {
|
||||||
@@ -871,6 +929,7 @@ class MachineGroup {
|
|||||||
this.measurements.getVariants(type).forEach(variant => {
|
this.measurements.getVariants(type).forEach(variant => {
|
||||||
|
|
||||||
const downstreamVal = this.measurements.type(type).variant(variant).position("downstream").getCurrentValue();
|
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();
|
const upstreamVal = this.measurements.type(type).variant(variant).position("upstream").getCurrentValue();
|
||||||
|
|
||||||
if (downstreamVal != null) {
|
if (downstreamVal != null) {
|
||||||
@@ -879,6 +938,9 @@ class MachineGroup {
|
|||||||
if (upstreamVal != null) {
|
if (upstreamVal != null) {
|
||||||
output[`upstream_${variant}_${type}`] = upstreamVal;
|
output[`upstream_${variant}_${type}`] = upstreamVal;
|
||||||
}
|
}
|
||||||
|
if (atEquipmentVal != null) {
|
||||||
|
output[`atEquipment_${variant}_${type}`] = atEquipmentVal;
|
||||||
|
}
|
||||||
if (downstreamVal != null && upstreamVal != null) {
|
if (downstreamVal != null && upstreamVal != null) {
|
||||||
const diffVal = this.measurements.type(type).variant(variant).difference().value;
|
const diffVal = this.measurements.type(type).variant(variant).difference().value;
|
||||||
output[`differential_${variant}_${type}`] = diffVal;
|
output[`differential_${variant}_${type}`] = diffVal;
|
||||||
@@ -902,17 +964,17 @@ class MachineGroup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = MachineGroup;
|
module.exports = MachineGroup;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
const Machine = require('../../rotatingMachine/src/specificClass');
|
const Machine = require('../../rotatingMachine/src/specificClass');
|
||||||
const Measurement = require('../../measurement/src/specificClass');
|
const Measurement = require('../../measurement/src/specificClass');
|
||||||
const specs = require('../../generalFunctions/datasets/assetData/curves/hidrostal-H05K-S03R.json');
|
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) {
|
function createBaseMachineConfig(machineNum, name,specs) {
|
||||||
return {
|
return {
|
||||||
general: {
|
general: {
|
||||||
logging: { enabled: true, logLevel: "warn" },
|
logging: { enabled: true, logLevel: "debug" },
|
||||||
name: name,
|
name: name,
|
||||||
id: machineNum,
|
id: machineNum,
|
||||||
unit: "m3/h"
|
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) {
|
function createBaseMachineGroupConfig(name) {
|
||||||
return {
|
return {
|
||||||
general: {
|
general: {
|
||||||
@@ -970,9 +1049,13 @@ function createBaseMachineGroupConfig(name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const machineGroupConfig = createBaseMachineGroupConfig("testmachinegroup");
|
const machineGroupConfig = createBaseMachineGroupConfig("testmachinegroup");
|
||||||
|
const stateConfigs = {};
|
||||||
const machineConfigs = {};
|
const machineConfigs = {};
|
||||||
machineConfigs[1]= createBaseMachineConfig(1,"testmachine",specs);
|
stateConfigs[1] = createStateConfig();
|
||||||
machineConfigs[2] = createBaseMachineConfig(2,"testmachine2",specs);
|
stateConfigs[2] = createStateConfig();
|
||||||
|
machineConfigs[1]= createBaseMachineConfig("asdfkj;asdf","testmachine",specs);
|
||||||
|
machineConfigs[2] = createBaseMachineConfig("asdfkj;asdf2","testmachine2",specs);
|
||||||
|
|
||||||
|
|
||||||
const ptConfig = {
|
const ptConfig = {
|
||||||
general: {
|
general: {
|
||||||
@@ -1002,14 +1085,16 @@ async function makeMachines(){
|
|||||||
const pt1 = new Measurement(ptConfig);
|
const pt1 = new Measurement(ptConfig);
|
||||||
const numofMachines = 2;
|
const numofMachines = 2;
|
||||||
for(let i = 1; i <= numofMachines; i++){
|
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.machines[i] = machine;
|
||||||
mg.childRegistrationUtils.registerChild(machine, "downstream");
|
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");
|
mg.setScaling("normalized");
|
||||||
|
|
||||||
const absMax = mg.dynamicTotals.flow.max;
|
const absMax = mg.dynamicTotals.flow.max;
|
||||||
@@ -1018,14 +1103,13 @@ async function makeMachines(){
|
|||||||
const percMax = 100;
|
const percMax = 100;
|
||||||
|
|
||||||
try{
|
try{
|
||||||
/*
|
/*
|
||||||
for(let demand = mg.dynamicTotals.flow.min ; demand <= mg.dynamicTotals.flow.max ; demand += 2){
|
for(let demand = mg.dynamicTotals.flow.min ; demand <= mg.dynamicTotals.flow.max ; demand += 2){
|
||||||
//set pressure
|
//set pressure
|
||||||
|
|
||||||
console.log("------------------------------------");
|
console.log("------------------------------------");
|
||||||
await mg.handleInput("parent",demand);
|
await mg.handleInput("parent",demand);
|
||||||
pt1.calculateInput(1400);
|
pt1.calculateInput(1400);
|
||||||
console.log("Waiting for 0.2 sec ");
|
|
||||||
//await new Promise(resolve => setTimeout(resolve, 200));
|
//await new Promise(resolve => setTimeout(resolve, 200));
|
||||||
console.log("------------------------------------");
|
console.log("------------------------------------");
|
||||||
|
|
||||||
@@ -1038,24 +1122,24 @@ async function makeMachines(){
|
|||||||
|
|
||||||
await mg.handleInput("parent",demand);
|
await mg.handleInput("parent",demand);
|
||||||
pt1.calculateInput(1400);
|
pt1.calculateInput(1400);
|
||||||
console.log("Waiting for 0.2 sec ");
|
|
||||||
//await new Promise(resolve => setTimeout(resolve, 200));
|
//await new Promise(resolve => setTimeout(resolve, 200));
|
||||||
console.log("------------------------------------");
|
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);
|
await mg.handleInput("parent",demand);
|
||||||
console.log(mg.machines[1].state.getCurrentState());
|
Object.keys(mg.machines).forEach(machineId => {
|
||||||
console.log(mg.machines[2].state.getCurrentState());
|
console.log(mg.machines[machineId].state.getCurrentState());
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`updating pressure to 1400 mbar`);
|
||||||
pt1.calculateInput(1400);
|
pt1.calculateInput(1400);
|
||||||
console.log("Waiting for 0.2 sec ");
|
|
||||||
//await new Promise(resolve => setTimeout(resolve, 200));
|
|
||||||
console.log("------------------------------------");
|
console.log("------------------------------------");
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1071,4 +1155,4 @@ async function makeMachines(){
|
|||||||
|
|
||||||
makeMachines();
|
makeMachines();
|
||||||
|
|
||||||
*/
|
//*/
|
||||||
Reference in New Issue
Block a user