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."
}
}
}
}