updates to machinegroupcontrol to work in new gitea repo

This commit is contained in:
znetsixe
2025-05-14 08:23:29 +02:00
parent 5856a739cb
commit 2f180ae37d
7 changed files with 2300 additions and 0 deletions

1056
dependencies/machineGroup/machineGroup.js vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,566 @@
const MachineGroup = require('./machineGroup');
const Machine = require('../../../rotatingMachine/dependencies/machine/machine');
const specs = require('../../../generalFunctions/datasets/assetData/pumps/hydrostal/centrifugal pumps/models.json');
class MachineGroupTester {
constructor() {
this.totalTests = 0;
this.passedTests = 0;
this.failedTests = 0;
this.machineCurve = specs[0].machineCurve;
}
assert(condition, message) {
this.totalTests++;
if (condition) {
console.log(`✓ PASS: ${message}`);
this.passedTests++;
} else {
console.log(`✗ FAIL: ${message}`);
this.failedTests++;
}
}
createBaseMachineConfig(name) {
return {
general: {
logging: { enabled: true, logLevel: "debug" },
name: name,
unit: "m3/h"
},
functionality: {
softwareType: "machine",
role: "RotationalDeviceController"
},
asset: {
type: "pump",
subType: "Centrifugal",
model: "TestModel",
supplier: "Hydrostal",
machineCurve: this.machineCurve
},
mode: {
current: "auto",
allowedActions: {
auto: ["execSequence", "execMovement", "statusCheck"],
virtualControl: ["execMovement", "statusCheck"],
fysicalControl: ["statusCheck"]
},
allowedSources: {
auto: ["parent", "GUI"],
virtualControl: ["GUI"],
fysicalControl: ["fysical"]
}
},
sequences: {
startup: ["starting", "warmingup", "operational"],
shutdown: ["stopping", "coolingdown", "idle"],
emergencystop: ["emergencystop", "off"],
boot: ["idle", "starting", "warmingup", "operational"]
},
calculationMode: "medium"
};
}
createBaseMachineGroupConfig(name) {
return {
general: {
logging: { enabled: true, logLevel: "debug" },
name: name
},
functionality: {
softwareType: "machineGroup",
role: "GroupController"
},
scaling: {
current: "normalized"
},
mode: {
current: "optimalControl"
}
};
}
async testSingleMachineOperation() {
console.log('\nTesting Single Machine Operation...');
const machineGroupConfig = this.createBaseMachineGroupConfig("TestMachineGroup");
const machineConfig = this.createBaseMachineConfig("TestMachine1");
try {
const mg = new MachineGroup(machineGroupConfig);
const machine = new Machine(machineConfig);
// Register machine with group
mg.childRegistrationUtils.registerChild(machine, "downstream");
machine.measurements.type("pressure").variant("measured").position("downstream").value(800);
await machine.state.transitionToState("idle");
// Test 1: Basic initialization
this.assert(
Object.keys(mg.machines).length === 0,
'Machine group should have exactly zero machine'
);
// Test 2: Calculate demand with single machine
await machine.handleInput("parent", "execSequence", "startup");
await mg.handleFlowInput(50);
this.assert(
mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() > 0,
'Total flow should be greater than 0 for demand of 50'
);
// Test 3: Check machine mode handling
machine.setMode("virtualControl");
const {single, machineNum} = mg.singleMachine();
this.assert(
single === true,
'Should identify as single machine when in virtual control'
);
// Test 4: Zero demand handling
await mg.handleFlowInput(0);
this.assert(
!mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() ||
mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() === 0,
'Total flow should be 0 for zero demand'
);
// Test 5: Max demand handling
await mg.handleFlowInput(100);
this.assert(
mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() > 0,
'Total flow should be greater than 0 for max demand'
);
} catch (error) {
console.error('Test failed with error:', error);
this.failedTests++;
}
}
async testMultipleMachineOperation() {
console.log('\nTesting Multiple Machine Operation...');
const machineGroupConfig = this.createBaseMachineGroupConfig("TestMachineGroup");
try {
const mg = new MachineGroup(machineGroupConfig);
const machine1 = new Machine(this.createBaseMachineConfig("Machine1"));
const machine2 = new Machine(this.createBaseMachineConfig("Machine2"));
mg.childRegistrationUtils.registerChild(machine1, "downstream");
mg.childRegistrationUtils.registerChild(machine2, "downstream");
machine1.measurements.type("pressure").variant("measured").position("downstream").value(800);
machine2.measurements.type("pressure").variant("measured").position("downstream").value(800);
await machine1.state.transitionToState("idle");
await machine2.state.transitionToState("idle");
await machine1.handleInput("parent", "execSequence", "startup");
await machine2.handleInput("parent", "execSequence", "startup");
// Test 1: Multiple machine registration
this.assert(
Object.keys(mg.machines).length === 2,
'Machine group should have exactly two machines'
);
// Test 1.1: Calculate demand with multiple machines
await mg.handleFlowInput(0); // Testing with higher demand for two machines
const machineOutputs = Object.keys(mg.machines).filter(id =>
mg.machines[id].measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() > 0
);
this.assert(
mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() > 0 &&
machineOutputs.length > 0,
'Should distribute load between machines'
);
// Test 1.2: Calculate demand with multiple machines with an increment of 10
for(let i = 0; i < 100; i+=10){
await mg.handleFlowInput(i); // Testing with incrementing demand
const flowValue = mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue();
this.assert(
flowValue !== undefined && !isNaN(flowValue),
`Should handle demand of ${i} units properly`
);
}
// Test 2: Calculate nonsense demands with multiple machines
await mg.handleFlowInput(150); // Testing with higher demand for two machines
this.assert(
mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() > 0,
'Should handle excessive demand gracefully'
);
// Test 3: Force single machine mode
machine2.setMode("maintenance");
const {single} = mg.singleMachine();
this.assert(
single === true,
'Should identify as single machine when one machine is in maintenance'
);
} catch (error) {
console.error('Test failed with error:', error);
this.failedTests++;
}
}
async testDynamicTotals() {
console.log('\nTesting Dynamic Totals...');
const mg = new MachineGroup(this.createBaseMachineGroupConfig("TestMachineGroup"));
const machine = new Machine(this.createBaseMachineConfig("TestMachine"));
try {
mg.childRegistrationUtils.registerChild(machine, "downstream");
machine.measurements.type("pressure").variant("measured").position("downstream").value(800);
await machine.state.transitionToState("idle");
await machine.handleInput("parent", "execSequence", "startup");
// Test 1: Dynamic totals initialization
const maxFlow = machine.predictFlow.currentFxyYMax;
const maxPower = machine.predictPower.currentFxyYMax;
this.assert(
mg.dynamicTotals.flow.max === maxFlow && mg.dynamicTotals.power.max === maxPower,
'Dynamic totals should reflect machine capabilities'
);
// Test 2: Demand scaling
await mg.handleFlowInput(50); // 50% of max
const actualFlow = mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue();
this.assert(
actualFlow <= maxFlow * 0.6, // Allow some margin for interpolation
'Scaled demand should be approximately 50% of max flow'
);
} catch (error) {
console.error('Test failed with error:', error);
this.failedTests++;
}
}
async testInterpolation() {
console.log('\nTesting Interpolation...');
const machineGroupConfig = this.createBaseMachineGroupConfig("TestMachineGroup");
const machineConfig = this.createBaseMachineConfig("TestMachine");
try {
const mg = new MachineGroup(machineGroupConfig);
const machine = new Machine(machineConfig);
// Register machine and set initial state
mg.childRegistrationUtils.registerChild(machine, "downstream");
machine.measurements.type("pressure").variant("measured").position("downstream").value(1);
machine.state.transitionToState("idle");
// Test interpolation at different demand points
const testPoints = [0, 25, 50, 75, 100];
for (const demand of testPoints) {
await mg.handleFlowInput(demand);
const flowValue = mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue();
const powerValue = mg.measurements.type("power").variant("predicted").position("upstream").getCurrentValue();
this.assert(
flowValue !== undefined && !isNaN(flowValue),
`Interpolation should produce valid flow value for demand ${demand}`
);
this.assert(
powerValue !== undefined && !isNaN(powerValue),
`Interpolation should produce valid power value for demand ${demand}`
);
}
// Test interpolation between curve points
const interpolatedPoint = 45; // Should interpolate between 40 and 60
await mg.handleFlowInput(interpolatedPoint);
this.assert(
mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() > 0,
`Interpolation should handle non-exact point ${interpolatedPoint}`
);
} catch (error) {
console.error('Test failed with error:', error);
this.failedTests++;
}
}
async testSingleMachineControlModes() {
console.log('\nTesting Single Machine Control Modes...');
const machineGroupConfig = this.createBaseMachineGroupConfig("TestMachineGroup");
const machineConfig = this.createBaseMachineConfig("TestMachine1");
try {
const mg = new MachineGroup(machineGroupConfig);
const machine = new Machine(machineConfig);
// Register machine and initialize
mg.childRegistrationUtils.registerChild(machine, "downstream");
machine.measurements.type("pressure").variant("measured").position("downstream").value(800);
await machine.state.transitionToState("idle");
await machine.handleInput("parent", "execSequence", "startup");
// Test 1: Virtual Control Mode
machine.setMode("virtualControl");
await mg.handleFlowInput(50);
this.assert(
machine.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() !== undefined,
'Should handle virtual control mode'
);
// Test 2: Physical Control Mode
machine.setMode("fysicalControl");
await mg.handleFlowInput(75);
this.assert(
machine.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() !== undefined,
'Should handle physical control mode'
);
// Test 3: Auto Mode Return
machine.setMode("auto");
await mg.handleFlowInput(60);
this.assert(
mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() > 0,
'Should return to normal operation in auto mode'
);
} catch (error) {
console.error('Test failed with error:', error);
this.failedTests++;
}
}
async testMachinesOffNormalized() {
console.log('\nTesting Machines Off with Normalized Flow...');
const machineGroupConfig = this.createBaseMachineGroupConfig("TestMachineGroup_OffNormalized");
// scaling is "normalized" by default
const mg = new MachineGroup(machineGroupConfig);
const machine = new Machine(this.createBaseMachineConfig("TestMachine_OffNormalized"));
mg.childRegistrationUtils.registerChild(machine, "downstream");
machine.measurements.type("pressure").variant("measured").position("downstream").value(800);
await machine.state.transitionToState("idle");
await machine.handleInput("parent", "execSequence", "startup");
// Turn machines off by setting demand to 0 with normalized scaling
await mg.handleFlowInput(-1);
this.assert(
!mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() ||
mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() === 0,
'Total flow should be 0 when demand is < 0 in normalized scaling'
);
}
async testMachinesOffAbsolute() {
console.log('\nTesting Machines Off with Absolute Flow...');
const machineGroupConfig = this.createBaseMachineGroupConfig("TestMachineGroup_OffAbsolute");
// Switch scaling to "absolute"
machineGroupConfig.scaling.current = "absolute";
const mg = new MachineGroup(machineGroupConfig);
const machine = new Machine(this.createBaseMachineConfig("TestMachine_OffAbsolute"));
mg.childRegistrationUtils.registerChild(machine, "downstream");
machine.measurements.type("pressure").variant("measured").position("downstream").value(800);
await machine.state.transitionToState("idle");
await machine.handleInput("parent", "execSequence", "startup");
// Turn machines off by setting demand to 0 with absolute scaling
await mg.handleFlowInput(0);
this.assert(
!mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() ||
mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() === 0,
'Total flow should be 0 when demand is 0 in absolute scaling'
);
}
async testPriorityControl() {
console.log('\nTesting Priority Control...');
const machineGroupConfig = this.createBaseMachineGroupConfig("TestMachineGroup_Priority");
const mg = new MachineGroup(machineGroupConfig);
try {
// Create 3 machines with different configurations for clearer testing
const machines = [];
for(let i = 1; i <= 3; i++) {
const machineConfig = this.createBaseMachineConfig(`Machine${i}`);
const machine = new Machine(machineConfig);
machines.push(machine);
mg.childRegistrationUtils.registerChild(machine, "downstream");
// Set different max flows to make priority visible
machine.predictFlow = {
currentFxyYMin: 10 * i, // Different min flows
currentFxyYMax: 50 * i // Different max flows
};
machine.measurements.type("pressure").variant("measured").position("downstream").value(800);
await machine.state.transitionToState("idle");
await machine.handleInput("parent", "execSequence", "startup");
// Mock the inputFlowCalcPower method for testing
machine.inputFlowCalcPower = (flow) => flow * 2; // Simple mock function
}
// Test 1: Default priority (by machine ID)
// Use handleInput which routes to equalControl in prioritycontrol mode
await mg.handleInput("parent", 80);
const flowAfterDefaultPriority = Object.values(mg.machines).map(machine =>
machine.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() || 0
);
this.assert(
flowAfterDefaultPriority[0] > 0 && flowAfterDefaultPriority[1] > 0 && flowAfterDefaultPriority[2] === 0,
'Default priority should use machines in ID order until demand is met'
);
// Test 2: Custom priority list
await mg.handleInput("parent", 120, Infinity, [3, 2, 1]);
await new Promise(resolve => setTimeout(resolve, 100));
const flowAfterCustomPriority = Object.values(mg.machines).map(machine =>
machine.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() || 0
);
this.assert(
flowAfterCustomPriority[2] > 0 && flowAfterCustomPriority[1] > 0 && flowAfterCustomPriority[0] === 0,
'Custom priority should use machines in specified order until demand is met'
);
// Test 3: Zero demand should shut down all machines
await mg.handleInput("parent", 0);
const noFlowCondition = Object.values(mg.machines).every(machine =>
!machine.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() ||
machine.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() === 0
);
this.assert(
noFlowCondition,
'Zero demand should result in no flow from any machine'
);
// Test 4: Handling excessive demand (more than total capacity)
const totalMaxFlow = machines.reduce((sum, machine) => sum + machine.predictFlow.currentFxyYMax, 0);
await mg.handleInput("parent", totalMaxFlow + 100);
const totalActualFlow = mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue();
this.assert(
totalActualFlow <= totalMaxFlow && totalActualFlow > 0,
'Excessive demand should be capped to maximum possible flow'
);
// Test 5: Check all measurements are updated correctly
this.assert(
mg.measurements.type("power").variant("predicted").position("upstream").getCurrentValue() > 0 &&
mg.measurements.type("efficiency").variant("predicted").position("downstream").getCurrentValue() > 0,
'All measurements should be updated after priority control'
);
} catch (error) {
console.error('Priority control test failed with error:', error);
this.failedTests++;
}
}
async runAllTests() {
console.log('Starting MachineGroup Tests...\n');
await this.testSingleMachineOperation();
await this.testMultipleMachineOperation();
await this.testDynamicTotals();
await this.testInterpolation();
await this.testSingleMachineControlModes();
await this.testMachinesOffNormalized();
await this.testMachinesOffAbsolute();
await this.testPriorityControl(); // Add the new test
await testCombinationIterations();
console.log('\nTest Summary:');
console.log(`Total Tests: ${this.totalTests}`);
console.log(`Passed: ${this.passedTests}`);
console.log(`Failed: ${this.failedTests}`);
// Return exit code based on test results
process.exit(this.failedTests > 0 ? 1 : 0);
}
}
// Add a custom logger to capture debug logs during tests
class CapturingLogger {
constructor() {
this.logs = [];
}
debug(message) {
this.logs.push({ level: "debug", message });
console.debug(message);
}
info(message) {
this.logs.push({ level: "info", message });
console.info(message);
}
warn(message) {
this.logs.push({ level: "warn", message });
console.warn(message);
}
error(message) {
this.logs.push({ level: "error", message });
console.error(message);
}
getAll() {
return this.logs;
}
clear() {
this.logs = [];
}
}
// Modify one of the test functions to override the machineGroup logger
async function testCombinationIterations() {
console.log('\nTesting Combination Iterations Logging...');
const machineGroupConfig = tester.createBaseMachineGroupConfig("TestCombinationIterations");
const mg = new MachineGroup(machineGroupConfig);
// Override logger with a capturing logger
const customLogger = new CapturingLogger();
mg.logger = customLogger;
// Create one machine for simplicity (or two if you like)
const machine = new Machine(tester.createBaseMachineConfig("TestMachineForCombo"));
mg.childRegistrationUtils.registerChild(machine, "downstream");
machine.measurements.type("pressure").variant("measured").position("downstream").value(800);
await machine.state.transitionToState("idle");
await machine.handleInput("parent", "execSequence", "startup");
// For testing, force dynamic totals so that combination search is exercised
mg.dynamicTotals.flow = { min: 0, max: 200 }; // example totalling
// Call handleFlowInput with a demand that requires iterations
await mg.handleFlowInput(120);
// After running, output captured iteration debug logs
console.log("\n-- Captured Debug Logs for Combination Search Iterations --");
customLogger.getAll().forEach(log => {
if(log.level === "debug") {
console.log(log.message);
}
});
// Also output best result details if any needed for further improvement
console.log("\n-- Final Output --");
const totalFlow = mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue();
console.log("Total Flow: ", totalFlow);
// Get machine outputs by checking each machine's measurements
const machineOutputs = {};
Object.entries(mg.machines).forEach(([id, machine]) => {
const flow = machine.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue();
if (flow) machineOutputs[id] = flow;
});
console.log("Machine Outputs: ", machineOutputs);
}
// Run the tests
const tester = new MachineGroupTester();
tester.runAllTests().catch(console.error);

View File

@@ -0,0 +1,188 @@
{
"general": {
"name": {
"default": "Machine Group Configuration",
"rules": {
"type": "string",
"description": "A human-readable name or label for this machine group configuration."
}
},
"id": {
"default": null,
"rules": {
"type": "string",
"nullable": true,
"description": "A unique identifier for this configuration. If not provided, defaults to null."
}
},
"logging": {
"logLevel": {
"default": "info",
"rules": {
"type": "enum",
"values": [
{
"value": "debug",
"description": "Log messages are printed for debugging purposes."
},
{
"value": "info",
"description": "Informational messages are printed."
},
{
"value": "warn",
"description": "Warning messages are printed."
},
{
"value": "error",
"description": "Error messages are printed."
}
]
}
},
"enabled": {
"default": true,
"rules": {
"type": "boolean",
"description": "Indicates whether logging is active. If true, log messages will be generated."
}
}
}
},
"functionality": {
"softwareType": {
"default": "machineGroup",
"rules": {
"type": "string",
"description": "Logical name identifying the software type."
}
},
"role": {
"default": "GroupController",
"rules": {
"type": "string",
"description": "Controls a group of machines within the system."
}
}
},
"mode": {
"current": {
"default": "optimalControl",
"rules": {
"type": "enum",
"values": [
{
"value": "optimalControl",
"description": "The group controller selects the most optimal combination of machines based on their real-time performance curves."
},
{
"value": "priorityControl",
"description": "Machines are controlled sequentially from minimum to maximum output until each is maxed out, then additional machines are added."
},
{
"value": "prioritypercentagecontrol",
"description": "Machines are controlled sequentially from minimum to maximum output until each is maxed out, then additional machines are added based on a percentage of the total demand."
},
{
"value": "maintenance",
"description": "The group is in maintenance mode with limited actions (monitoring only)."
}
],
"description": "The operational mode of the machine group controller."
}
},
"allowedActions": {
"default": {},
"rules": {
"type": "object",
"schema": {
"optimalControl": {
"default": ["statusCheck", "execOptimalCombination", "balanceLoad", "emergencyStop"],
"rules": {
"type": "set",
"itemType": "string",
"description": "Actions allowed in optimalControl mode."
}
},
"priorityControl": {
"default": ["statusCheck", "execSequentialControl", "balanceLoad", "emergencyStop"],
"rules": {
"type": "set",
"itemType": "string",
"description": "Actions allowed in priorityControl mode."
}
},
"prioritypercentagecontrol": {
"default": ["statusCheck", "execSequentialControl", "balanceLoad", "emergencyStop"],
"rules": {
"type": "set",
"itemType": "string",
"description": "Actions allowed in manualOverride mode."
}
},
"maintenance": {
"default": ["statusCheck"],
"rules": {
"type": "set",
"itemType": "string",
"description": "Actions allowed in maintenance mode."
}
}
},
"description": "Defines the actions available for each operational mode of the machine group controller."
}
},
"allowedSources": {
"default": {},
"rules": {
"type": "object",
"schema": {
"optimalcontrol": {
"default": ["parent", "GUI", "physical", "API"],
"rules": {
"type": "set",
"itemType": "string",
"description": "Command sources allowed in optimalControl mode."
}
},
"prioritycontrol": {
"default": ["parent", "GUI", "physical", "API"],
"rules": {
"type": "set",
"itemType": "string",
"description": "Command sources allowed in priorityControl mode."
}
},
"prioritypercentagecontrol": {
"default": ["parent", "GUI", "physical", "API"],
"rules": {
"type": "set",
"itemType": "string",
"description": "Command sources allowed "
}
}
},
"description": "Specifies the valid command sources recognized by the machine group controller for each mode."
}
}
},
"scaling": {
"current": {
"default": "normalized",
"rules": {
"type": "enum",
"values": [
{
"value": "normalized",
"description": "Scales the demand between 0100% of the total flow capacity, interpolating to calculate the effective demand."
},
{
"value": "absolute",
"description": "Uses the absolute demand value directly, capped between the min and max machine flow capacities."
}
],
"description": "The scaling mode for demand calculations."
}
}
}
}

137
dependencies/test.js vendored Normal file
View File

@@ -0,0 +1,137 @@
/**
* This file implements a pump optimization algorithm that:
* 1. Models different pumps with efficiency characteristics
* 2. Determines all possible pump combinations that can meet a demand flow
* 3. Finds the optimal combination that minimizes power consumption
* 4. Tests the algorithm with different demand levels
*/
/**
* Pump Class
* Represents a pump with specific operating characteristics including:
* - Maximum flow capacity
* - Center of Gravity (CoG) for efficiency
* - Efficiency curve mapping flow percentages to power consumption
*/
class Pump {
constructor(name, maxFlow, cog, efficiencyCurve) {
this.name = name;
this.maxFlow = maxFlow; // Maximum flow at a given pressure
this.CoG = cog; // Efficiency center of gravity percentage
this.efficiencyCurve = efficiencyCurve; // Flow % -> Power usage mapping
}
/**
* Returns pump flow at a given pressure
* Currently assumes constant flow regardless of pressure
*/
getFlow(pressure) {
return this.maxFlow; // Assume constant flow at a given pressure
}
/**
* Calculates power consumption based on flow and pressure
* Uses the efficiency curve when available, otherwise uses linear approximation
*/
getPowerConsumption(flow, pressure) {
let flowPercent = flow / this.maxFlow;
return this.efficiencyCurve[flowPercent] || (1.2 * flow); // Default linear approximation
}
}
/**
* Test pump definitions
* Three pump models with different flow capacities and efficiency characteristics
*/
const pumps = [
new Pump("Pump A", 100, 0.6, {0.6: 50, 0.8: 70, 1.0: 100}),
new Pump("Pump B", 120, 0.7, {0.6: 55, 0.8: 75, 1.0: 110}),
new Pump("Pump C", 90, 0.5, {0.5: 40, 0.7: 60, 1.0: 90}),
];
const pressure = 1.0; // Assume constant pressure
/**
* Get all valid pump combinations that meet the required demand flow (Qd)
*
* @param {Array} pumps - Available pump array
* @param {Number} Qd - Required demand flow
* @param {Number} pressure - System pressure
* @returns {Array} Array of valid pump combinations that can meet or exceed the demand
*
* This function:
* 1. Generates all possible subsets of pumps (power set)
* 2. Filters for non-empty subsets that can meet or exceed demand flow
*/
function getValidPumpCombinations(pumps, Qd, pressure) {
let subsets = [[]];
for (let pump of pumps) {
let newSubsets = subsets.map(set => [...set, pump]);
subsets = subsets.concat(newSubsets);
}
return subsets.filter(subset => subset.length > 0 &&
subset.reduce((sum, p) => sum + p.getFlow(pressure), 0) >= Qd);
}
/**
* Find the optimal pump combination that minimizes power consumption
*
* @param {Array} pumps - Available pump array
* @param {Number} Qd - Required demand flow
* @param {Number} pressure - System pressure
* @returns {Object} Object containing the best pump combination and its power consumption
*
* This function:
* 1. Gets all valid pump combinations that meet demand
* 2. For each combination, distributes flow based on CoG proportions
* 3. Calculates total power consumption for each distribution
* 4. Returns the combination with minimum power consumption
*/
function optimizePumpSelection(pumps, Qd, pressure) {
let validCombinations = getValidPumpCombinations(pumps, Qd, pressure);
let bestCombination = null;
let minPower = Infinity;
validCombinations.forEach(combination => {
let totalFlow = combination.reduce((sum, pump) => sum + pump.getFlow(pressure), 0);
let totalCoG = combination.reduce((sum, pump) => sum + pump.CoG, 0);
// Distribute flow based on CoG proportions
let flowDistribution = combination.map(pump => ({
pump,
flow: (pump.CoG / totalCoG) * Qd
}));
let totalPower = flowDistribution.reduce((sum, { pump, flow }) =>
sum + pump.getPowerConsumption(flow, pressure), 0);
if (totalPower < minPower) {
minPower = totalPower;
bestCombination = flowDistribution;
}
});
return { bestCombination, minPower };
}
/**
* Test function that runs optimization for different demand levels
* Tests from 0% to 100% of total available flow in 10% increments
* Outputs the selected pumps, flow allocation, and power consumption for each scenario
*/
console.log("Pump Optimization Results:");
const totalAvailableFlow = pumps.reduce((sum, pump) => sum + pump.getFlow(pressure), 0);
for (let i = 0; i <= 10; i++) {
let Qd = (i / 10) * totalAvailableFlow; // Incremental flow demand
let { bestCombination, minPower } = optimizePumpSelection(pumps, Qd, pressure);
console.log(`\nTotal Demand Flow: ${Qd.toFixed(2)}`);
console.log("Selected Pumps and Allocated Flow:");
bestCombination.forEach(({ pump, flow }) => {
console.log(` ${pump.name}: ${flow.toFixed(2)} units`);
});
console.log(`Total Power Consumption: ${minPower.toFixed(2)} kW`);
}

154
mgc.html Normal file
View File

@@ -0,0 +1,154 @@
<!--
brabantse delta kleuren:
#eaf4f1
#86bbdd
#bad33b
#0c99d9
#a9daee
#0f52a5
#50a8d9
#cade63
#4f8582
#c4cce0
-->
<script type="text/javascript">
RED.nodes.registerType('machineGroupControl',{
category: 'digital twin',
color: '#eaf4f1',
defaults: {
name: {value:""},
enableLog: { value: false },
logLevel: { value: "error" },
},
inputs:1,
outputs:4,
inputLabels: "Usage see manual",
outputLabels: ["process", "dbase", "upstreamParent", "downstreamParent"],
icon: "font-awesome/fa-tachometer",
//define label function
label: function() {
return this.name || "MachineGroup controller";
},
oneditprepare: function() {
const node = this;
console.log("Rotating Machine Node: Edit Prepare");
const elements = {
// Basic fields
name: document.getElementById("node-input-name"),
// Logging fields
logCheckbox: document.getElementById("node-input-enableLog"),
logLevelSelect: document.getElementById("node-input-logLevel"),
rowLogLevel: document.getElementById("row-logLevel"),
};
const projecSettingstURL = "http://localhost:1880/generalFunctions/settings/projectSettings.json";
try{
// Fetch project settings
menuUtils.fetchProjectData(projecSettingstURL)
.then((projectSettings) => {
//assign to node vars
node.configUrls = projectSettings.configUrls;
const { cloudConfigURL, localConfigURL } = menuUtils.getSpecificConfigUrl("machineGroupControl",node.configUrls.cloud.taggcodeAPI);
node.configUrls.cloud.config = cloudConfigURL; // first call
node.configUrls.local.config = localConfigURL; // backup call
// Gets the ID of the active workspace (Flow)
const activeFlowId = RED.workspaces.active(); //fetches active flow id
node.processId = activeFlowId;
// UI elements
menuUtils.initBasicToggles(elements);
})
}catch(e){
console.log("Error fetching project settings", e);
}
if(node.d){
//this means node is disabled
console.log("Current status of node is disabled");
}
},
oneditsave: function(){
const node = this;
//save basic properties
["name"].forEach(
(field) => {
const element = document.getElementById(`node-input-${field}`);
if (element) {
node[field] = element.value || "";
}
}
);
const logLevelElement = document.getElementById("node-input-logLevel");
node.logLevel = logLevelElement ? logLevelElement.value || "info" : "info";
}
});
</script>
<script type="text/html" data-template-name="machineGroupControl">
<!-------------------------------------------INPUT NAME / TYPE ----------------------------------------------->
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i>Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
<hr />
<!-- loglevel checkbox -->
<div class="form-row">
<label for="node-input-enableLog"
><i class="fa fa-cog"></i> Enable Log</label
>
<input
type="checkbox"
id="node-input-enableLog"
style="width:20px; vertical-align:baseline;"
/>
<span>Enable logging</span>
</div>
<div class="form-row" id="row-logLevel">
<label for="node-input-logLevel"><i class="fa fa-cog"></i> Log Level</label>
<select id="node-input-logLevel" style="width:60%;">
<option value="info">Info</option>
<option value="debug">Debug</option>
<option value="warn">Warn</option>
<option value="error">Error</option>
</select>
</div>
<!-------------------------------------------INPUT TRANSLATION TO OUTPUT ----------------------------------------------->
<hr />
<div class="form-tips"></div>
<b>Tip:</b> Ensure that the "Name" field is unique to easily identify the node.
Enable logging if you need detailed information for debugging purposes.
Choose the appropriate log level based on the verbosity required.
</div>
</script>
<script type="text/html" data-help-name="machineGroupControl">
<p>A machineGroupControl node</p>
</script>

171
mgc.js Normal file
View File

@@ -0,0 +1,171 @@
module.exports = function (RED) {
function machineGroupControl(config) {
//create node
RED.nodes.createNode(this, config);
//call this => node so whenver you want to call a node function type node and the function behind it
var node = this;
//fetch machine object from machine.js
const MachineGroup = require('./dependencies/machineGroup/machineGroup');
const OutputUtils = require("../generalFunctions/helper/outputUtils");
const mgConfig = config = {
general: {
name: config.name,
id : config.id,
logging: {
enabled: config.loggingEnabled,
logLevel: config.logLevel,
}
},
};
//make new class on creation to work with.
const mg = new MachineGroup(mgConfig);
// put mg on node memory as source
node.source = mg;
//load output utils
const output = new OutputUtils();
//update node status
function updateNodeStatus(mg) {
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;
// Calculate total capacity based on available machines
const availableMachines = Object.values(mg.machines).filter(machine => {
const state = machine.state.getCurrentState();
const mode = machine.currentMode;
return !(state === "off" || state === "maintenance" || mode === "maintenance");
});
const totalCapacity = Math.round(mg.dynamicTotals.flow.max * 1) / 1;
// Determine overall status based on available machines
const status = availableMachines.length > 0
? `${availableMachines.length} machines`
: "No machines";
let scalingSymbol = '';
switch (scaling.toLowerCase()) {
case 'absolute':
scalingSymbol = 'Ⓐ'; // Clear symbol for Absolute mode
break;
case 'normalized':
scalingSymbol = 'Ⓝ'; // Clear symbol for Normalized mode
break;
default:
scalingSymbol = mode;
break;
}
// Generate status text in a single line
const text = ` ${mode} | ${scalingSymbol}: 💨=${totalFlow}/${totalCapacity} | ⚡=${totalPower} | ${status}`;
return {
fill: availableMachines.length > 0 ? "green" : "red",
shape: "dot",
text
};
}
//never ending functions
function tick(){
//source.tick();
const status = updateNodeStatus(mg);
node.status(status);
//get output
const classOutput = mg.getOutput();
const dbOutput = output.formatMsg(classOutput, mg.config, "influxdb");
const pOutput = output.formatMsg(classOutput, mg.config, "process");
//only send output on values that changed
let msgs = [];
msgs[0] = pOutput;
msgs[1] = dbOutput;
node.send(msgs);
}
// register child on first output this timeout is needed because of node - red stuff
setTimeout(
() => {
/*---execute code on first start----*/
let msgs = [];
msgs[2] = { topic : "registerChild" , payload: node.id, positionVsParent: "upstream" };
msgs[3] = { topic : "registerChild" , payload: node.id, positionVsParent: "downstream" };
//send msg
this.send(msgs);
},
100
);
//declare refresh interval internal node
setTimeout(
() => {
/*---execute code on first start----*/
this.interval_id = setInterval(function(){ tick() },1000)
},
1000
);
//-------------------------------------------------------------------->>what to do on input
node.on("input", async function (msg,send,done) {
if(msg.topic == 'registerChild'){
const childId = msg.payload;
const childObj = RED.nodes.getNode(childId);
mg.childRegistrationUtils.registerChild(childObj.source,msg.positionVsParent);
}
if(msg.topic == 'setMode'){
const mode = msg.payload;
const source = "parent";
mg.setMode(source,mode);
}
if(msg.topic == 'setScaling'){
const scaling = msg.payload;
mg.setScaling(scaling);
}
if(msg.topic == 'Qd'){
const Qd = parseFloat(msg.payload);
const source = "parent";
if (isNaN(Qd)) {
return mg.logger.error(`Invalid demand value: ${Qd}`);
};
try{
await mg.handleInput(source,Qd);
msg.topic = mg.config.general.name;
msg.payload = "done";
send(msg);
}catch(e){
console.log(e);
}
}
// tidy up any async code here - shutdown connections and so on.
node.on('close', function() {
clearTimeout(this.interval_id);
});
});
}
RED.nodes.registerType("machineGroupControl", machineGroupControl);
};

28
package.json Normal file
View File

@@ -0,0 +1,28 @@
{
"name": "machineGroupControl",
"version": "0.9.0",
"description": "Control module machineGroupControl",
"main": "mgc.js",
"scripts": {
"test": "node mgc.js"
},
"repository": {
"type": "git",
"url": "https://gitea.centraal.wbd-rd.nl/RnD/machineGroupControl.git"
},
"keywords": [
"machineGroupControl",
"node-red"
],
"author": "Rene De Ren",
"license": "MIT",
"dependencies": {
"generalFunctions": "git+https://gitea.centraal.wbd-rd.nl/RnD/generalFunctions.git",
"predict": "git+https://gitea.centraal.wbd-rd.nl/RnD/predict.git"
},
"node-red": {
"nodes": {
"machineGroupControl": "mgc.js"
}
}
}