// ...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();