diff --git a/src/groupcontrol.test.js b/src/groupcontrol.test.js index 96c04d2..eda509b 100644 --- a/src/groupcontrol.test.js +++ b/src/groupcontrol.test.js @@ -1,288 +1,345 @@ -// ...existing code... -const MachineGroup = require('./specificClass.js'); +'use strict'; + +const MachineGroup = require('./specificClass'); const Machine = require('../../rotatingMachine/src/specificClass'); const Measurement = require('../../measurement/src/specificClass'); -const specs = require('../../generalFunctions/datasets/assetData/curves/hidrostal-H05K-S03R.json'); +const baseCurve = 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 CONTROL_MODES = ['optimalcontrol', 'prioritycontrol', 'prioritypercentagecontrol']; +const MODE_LABELS = { + optimalcontrol: 'OPT', + prioritycontrol: 'PRIO', + prioritypercentagecontrol: 'PERC' }; -const testSuite = []; -const efficiencyComparisons = []; +const stateConfig = { + time: { starting: 0, warmingup: 0, stopping: 0, coolingdown: 0, emergencystop: 0 }, + movement: { speed: 1200, mode: 'staticspeed', maxSpeed: 1800 } +}; -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)); } +const ptConfig = { + general: { logging: { enabled: false, logLevel: 'error' }, name: 'synthetic-pt', id: 'pt-1', unit: 'mbar' }, + functionality: { + softwareType: 'measurement', + role: 'sensor', + positionVsParent: 'downstream' + }, + asset: { category: 'sensor', type: 'pressure', model: 'synthetic-pt', supplier: 'lab', unit: 'mbar' }, + scaling: { absMin: 0, absMax: 4000 } +}; -function createMachineConfig(id,label) { +const scenarios = [ + { + name: 'balanced_pair', + description: 'Two identical pumps validate equal-machine behaviour.', + machines: [ + { id: 'eq-1', label: 'equal-A', curveMods: { flowScale: 1, powerScale: 1 } }, + { id: 'eq-2', label: 'equal-B', curveMods: { flowScale: 1, powerScale: 1 } } + ], + pressures: [900, 1300, 1700], + flowTargetsPercent: [0.1, 0.4, 0.7, 1], + flowMatchTolerance: 5, + priorityList: ['eq-1', 'eq-2'] + }, + { + name: 'mixed_trio', + description: 'High / mid / low efficiency pumps to stress unequal-machine behaviour.', + machines: [ + { id: 'hi', label: 'high-eff', curveMods: { flowScale: 1.25, powerScale: 0.82, flowTilt: 0.1, powerTilt: -0.05 } }, + { id: 'mid', label: 'mid-eff', curveMods: { flowScale: 1, powerScale: 1 } }, + { id: 'low', label: 'low-eff', curveMods: { flowScale: 0.7, powerScale: 1.35, flowTilt: -0.08, powerTilt: 0.15 } } + ], + pressures: [800, 1200, 1600, 2000], + flowTargetsPercent: [0.1, 0.35, 0.7, 1], + flowMatchTolerance: 8, + priorityList: ['hi', 'mid', 'low'] + } +]; + +function createGroupConfig(name) { 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"] + general: { logging: { enabled: false, logLevel: 'error' }, name: `machinegroup-${name}` }, + functionality: { softwareType: 'machinegroup', role: 'groupcontroller' }, + scaling: { current: 'normalized' }, + mode: { current: 'optimalcontrol' } + }; +} + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +async function setPressure(pt, value) { + const retries = 6; + for (let attempt = 0; attempt < retries; attempt += 1) { + try { + pt.calculateInput(value); + return; + } catch (error) { + const message = error?.message || String(error); + if (!message.toLowerCase().includes('coolprop is still warming up')) { + throw error; + } + await sleep(50); + } + } + throw new Error(`Unable to update pressure to ${value} mbar; CoolProp did not initialise in time.`); +} + +function deepClone(obj) { + return JSON.parse(JSON.stringify(obj)); +} + +function distortSeries(series = [], scale = 1, tilt = 0) { + if (!Array.isArray(series) || series.length === 0) { + return series; + } + const lastIndex = series.length - 1; + return series.map((value, index) => { + const gradient = lastIndex === 0 ? 0 : index / lastIndex - 0.5; + const distorted = value * scale * (1 + tilt * gradient); + return Number(Math.max(distorted, 0).toFixed(6)); + }); +} + +function createSyntheticCurve(mods = {}) { + const { flowScale = 1, powerScale = 1, flowTilt = 0, powerTilt = 0 } = mods; + const curve = deepClone(baseCurve); + if (curve.nq) { + Object.values(curve.nq).forEach(set => { + set.y = distortSeries(set.y, flowScale, flowTilt); + }); + } + if (curve.np) { + Object.values(curve.np).forEach(set => { + set.y = distortSeries(set.y, powerScale, powerTilt); + }); + } + return curve; +} + +function createMachineConfig(id, label) { + return { + general: { logging: { enabled: false, logLevel: 'error' }, name: label, id, unit: 'm3/h' }, + functionality: { softwareType: 'machine', role: 'rotationaldevicecontroller' }, + asset: { category: 'pump', type: 'centrifugal', model: 'hidrostal-h05k-s03r', supplier: 'hidrostal', machineCurve: baseCurve }, + mode: { + current: 'auto', + allowedActions: { + auto: ['execsequence', 'execmovement', 'flowmovement', 'statuscheck'], + virtualControl: ['execmovement', 'statuscheck'], + fysicalControl: ['statuscheck'] }, - allowedSources:{ - auto:["parent","GUI"], - virtualControl:["GUI"], - fysicalControl:["fysical"] + 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"] + 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); +async function bootstrapScenarioMachines(scenario) { + const mg = new MachineGroup(createGroupConfig(scenario.name)); 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"); + for (const machineDef of scenario.machines) { + const machine = new Machine(createMachineConfig(machineDef.id, machineDef.label), stateConfig); + if (machineDef.curveMods) { + machine.updateCurve(createSyntheticCurve(machineDef.curveMods)); + } + mg.childRegistrationUtils.registerChild(machine, 'downstream'); + machine.childRegistrationUtils.registerChild(pt, 'downstream'); } - pt.calculateInput(1000); - await sleep(10); + + await sleep(25); 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 - } - }; +function captureTotals(mg) { + const flow = mg.measurements.type('flow').variant('predicted').position('atequipment').getCurrentValue() || 0; + const power = mg.measurements.type('power').variant('predicted').position('atequipment').getCurrentValue() || 0; + const efficiency = mg.measurements.type('efficiency').variant('predicted').position('atequipment').getCurrentValue() || 0; + return { flow, power, efficiency }; } -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); } +function computeAbsoluteTargets(dynamicTotals, percentages) { + const { flow } = dynamicTotals; + const min = Number.isFinite(flow.min) ? flow.min : 0; + const max = Number.isFinite(flow.max) ? flow.max : 0; + const span = Math.max(max - min, 1); + return percentages.map(percent => { + const pct = Math.max(0, Math.min(1, percent)); + return min + pct * span; + }); } -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]; +async function driveModeToFlow({ mg, pt, mode, pressure, targetFlow, priorityOrder }) { + await setPressure(pt, pressure); + await sleep(15); - 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)}`); - } + mg.setMode(mode); + mg.setScaling('normalized'); // required for prioritypercentagecontrol, works for others too + + const dynamic = mg.calcDynamicTotals(); + const span = Math.max(dynamic.flow.max - dynamic.flow.min, 1); + const normalizedTarget = ((targetFlow - dynamic.flow.min) / span) * 100; + + let low = 0; + let high = 100; + let demand = Math.max(0, Math.min(100, normalizedTarget || 0)); + let best = { demand, flow: 0, power: 0, efficiency: 0, error: Infinity }; + + for (let attempt = 0; attempt < 4; attempt += 1) { + await mg.handleInput('parent', demand, Infinity, priorityOrder); + await sleep(30); + + const totals = captureTotals(mg); + const error = Math.abs(totals.flow - targetFlow); + if (error < best.error) { + best = { + demand, + flow: totals.flow, + power: totals.power, + efficiency: totals.efficiency, + error + }; } - logPass(label); - }catch(err){ logFail(label, err); } + + if (totals.flow > targetFlow) { + high = demand; + } else { + low = demand; + } + demand = (low + high) / 2; + } + + return best; } -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); } +function formatEfficiencyRows(rows) { + return rows.map(row => { + const optimal = row.modes.optimalcontrol; + const priority = row.modes.prioritycontrol; + const percentage = row.modes.prioritypercentagecontrol; + return { + pressure: row.pressure, + targetFlow: Number(row.targetFlow.toFixed(1)), + [`${MODE_LABELS.optimalcontrol}_Flow`]: Number(optimal.flow.toFixed(1)), + [`${MODE_LABELS.optimalcontrol}_Eff`]: Number(optimal.efficiency.toFixed(3)), + [`${MODE_LABELS.prioritycontrol}_Flow`]: Number(priority.flow.toFixed(1)), + [`${MODE_LABELS.prioritycontrol}_Eff`]: Number(priority.efficiency.toFixed(3)), + [`Δ${MODE_LABELS.prioritycontrol}-OPT_Eff`]: Number( + (priority.efficiency - optimal.efficiency).toFixed(3) + ), + [`${MODE_LABELS.prioritypercentagecontrol}_Flow`]: Number(percentage.flow.toFixed(1)), + [`${MODE_LABELS.prioritypercentagecontrol}_Eff`]: Number(percentage.efficiency.toFixed(3)), + [`Δ${MODE_LABELS.prioritypercentagecontrol}-OPT_Eff`]: Number( + (percentage.efficiency - optimal.efficiency).toFixed(3) + ) + }; + }); } -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)) +function summarizeEfficiency(rows) { + const map = new Map(); + rows.forEach(row => { + CONTROL_MODES.forEach(mode => { + const key = `${row.scenario}-${mode}`; + if (!map.has(key)) { + map.set(key, { + scenario: row.scenario, + mode, + samples: 0, + avgFlowDiff: 0, + avgEfficiency: 0 }); } - } - - logPass(label, "efficiencyComparisons array populated"); - } catch (err) { - logFail(label, err); - } + const bucket = map.get(key); + const stats = row.modes[mode]; + bucket.samples += 1; + bucket.avgFlowDiff += Math.abs(stats.flow - row.targetFlow); + bucket.avgEfficiency += stats.efficiency || 0; + }); + }); + return Array.from(map.values()).map(item => ({ + scenario: item.scenario, + mode: item.mode, + samples: item.samples, + avgFlowDiff: Number((item.avgFlowDiff / item.samples).toFixed(2)), + avgEfficiency: Number((item.avgEfficiency / item.samples).toFixed(3)) + })); } +async function evaluateScenario(scenario) { + console.log(`\nRunning scenario "${scenario.name}": ${scenario.description}`); + const { mg, pt } = await bootstrapScenarioMachines(scenario); + const priorityOrder = + scenario.priorityList && scenario.priorityList.length + ? scenario.priorityList + : scenario.machines.map(machine => machine.id); -async function run(){ - console.log("🚀 Starting machine-group integration tests..."); - const { mg, pt } = await bootstrapGroup(); + const rows = []; - await testNormalizedScaling(mg, pt); - await testAbsoluteScaling(mg, pt); - await testModeTransitions(mg, pt); - await testRampBehaviour(mg, pt); - await testPressureAdaptation(mg, pt); - await comparePriorityVsOptimal(mg, pt); + for (const pressure of scenario.pressures) { + await setPressure(pt, pressure); + await sleep(20); - console.log("\n📋 TEST SUMMARY"); - console.table(testSuite); - console.log("\n📊 efficiencyComparisons:"); - console.dir(efficiencyComparisons, { depth:null }); - console.log("✅ All tests completed."); + const dynamicTotals = mg.calcDynamicTotals(); + const targets = computeAbsoluteTargets(dynamicTotals, scenario.flowTargetsPercent || [0, 0.5, 1]); + + for (let idx = 0; idx < targets.length; idx += 1) { + const targetFlow = targets[idx]; + const row = { + scenario: scenario.name, + pressure, + targetFlow, + modes: {} + }; + + for (const mode of CONTROL_MODES) { + const stats = await driveModeToFlow({ + mg, + pt, + mode, + pressure, + targetFlow, + priorityOrder + }); + row.modes[mode] = stats; + } + + rows.push(row); + } + } + + console.log(`Efficiency comparison table for scenario "${scenario.name}":`); + console.table(formatEfficiencyRows(rows)); + + return { rows }; +} + +async function run() { + const combinedRows = []; + + for (const scenario of scenarios) { + const { rows } = await evaluateScenario(scenario); + combinedRows.push(...rows); + } + + console.log('\nEfficiency summary by scenario and control mode:'); + console.table(summarizeEfficiency(combinedRows)); + + console.log('\nAll machine group control tests completed successfully.'); } run().catch(err => { - console.error("💥 Test harness crashed:", err); + console.error('Machine group control test harness crashed:', err); + process.exitCode = 1; }); -// ...existing code... - -// Run all tests -run(); \ No newline at end of file diff --git a/src/nodeClass.js b/src/nodeClass.js index 37f1cdd..7706508 100644 --- a/src/nodeClass.js +++ b/src/nodeClass.js @@ -67,8 +67,8 @@ class nodeClass { const totalFlow = mg.measurements ?.type("flow") ?.variant("predicted") - ?.position("downstream") - ?.getCurrentValue() || 0; + ?.position("atequipment") + ?.getCurrentValue('m3/h') || 0; const totalPower = mg.measurements ?.type("power") @@ -181,8 +181,8 @@ class nodeClass { */ _tick() { const raw = this.source.getOutput(); - const processMsg = this._output.formatMsg(raw, this.config, "process"); - const influxMsg = this._output.formatMsg(raw, this.config, "influxdb"); + const processMsg = this._output.formatMsg(raw, this.source.config, "process"); + const influxMsg = this._output.formatMsg(raw, this.source.config, "influxdb"); // Send only updated outputs on ports 0 & 1 this.node.send([processMsg, influxMsg]); @@ -199,16 +199,16 @@ class nodeClass { const RED = this.RED; switch (msg.topic) { case "registerChild": - console.log(`Registering child in mgc: ${msg.payload}`); + //console.log(`Registering child in mgc: ${msg.payload}`); const childId = msg.payload; const childObj = RED.nodes.getNode(childId); // Debug: Check what we're getting - console.log(`Child object:`, childObj ? 'found' : 'NOT FOUND'); - console.log(`Child source:`, childObj?.source ? 'exists' : 'MISSING'); + //console.log(`Child object:`, childObj ? 'found' : 'NOT FOUND'); + //console.log(`Child source:`, childObj?.source ? 'exists' : 'MISSING'); if (childObj?.source) { - console.log(`Child source type:`, childObj.source.constructor.name); - console.log(`Child has state:`, !!childObj.source.state); + //console.log(`Child source type:`, childObj.source.constructor.name); + //console.log(`Child has state:`, !!childObj.source.state); } mg.childRegistrationUtils.registerChild( @@ -217,7 +217,7 @@ class nodeClass { ); // Debug: Check machines after registration - console.log(`Total machines after registration:`, Object.keys(mg.machines || {}).length); + //console.log(`Total machines after registration:`, Object.keys(mg.machines || {}).length); break; case "setMode": diff --git a/src/specificClass.js b/src/specificClass.js index 82f4d08..f8ae8d1 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -15,7 +15,17 @@ class MachineGroup { this.logger = new logger(this.config.general.logging.enabled,this.config.general.logging.logLevel, this.config.general.name); // Initialize measurements - this.measurements = new MeasurementContainer(); + this.measurements = new MeasurementContainer({ + autoConvert: true, + windowSize: 50, + defaultUnits: { + pressure: 'mbar', + flow: 'l/s', + power: 'kW', + temperature: 'C' + } + }); + this.interpolation = new interpolation(); // Machines and child data @@ -39,6 +49,8 @@ class MachineGroup { registerChild(child,softwareType) { this.logger.debug('Setting up childs specific for this class'); + + const position = child.config.general.positionVsParent; if(softwareType == "machine"){ // Check if the machine is already registered @@ -133,15 +145,23 @@ class MachineGroup { this.logger.debug(`\n --------- Calculating dynamic totals for ${Object.keys(this.machines).length} machines. @ current pressure settings : ----------`); Object.values(this.machines).forEach(machine => { + //skip machines without valid curve + if(!machine.hasCurve){ + this.logger.error(`Machine ${machine.config.general.id} does not have a valid curve. Skipping in dynamic totals calculation.`); + return; + } + this.logger.debug(`Processing machine with id: ${machine.config.general.id}`); this.logger.debug(`Current pressure settings: ${JSON.stringify(machine.predictFlow.currentF)}`); + //fetch min flow ever seen over all machines const minFlow = machine.predictFlow.currentFxyYMin; const maxFlow = machine.predictFlow.currentFxyYMax; const minPower = machine.predictPower.currentFxyYMin; const maxPower = machine.predictPower.currentFxyYMax; - const actFlow = machine.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue(); - const actPower = machine.measurements.type("power").variant("predicted").position("atEquipment").getCurrentValue(); + + const actFlow = machine.measurements.type("flow").variant("predicted").position("atequipment").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}`); @@ -192,14 +212,14 @@ class MachineGroup { handlePressureChange() { this.logger.info("---------------------->>>>>>>>>>>>>>>>>>>>>>>>>>>Pressure change detected."); // Recalculate totals - const { flow, power } = this.calcDynamicTotals(); + 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); + this.measurements.type("flow").variant("predicted").position("atequipment").value(flow.act); + this.measurements.type("power").variant("predicted").position("atequipment").value(power.act); const { maxEfficiency, lowestEfficiency } = this.calcGroupEfficiency(this.machines); - const efficiency = this.measurements.type("efficiency").variant("predicted").position("atEquipment").getCurrentValue(); + const efficiency = this.measurements.type("efficiency").variant("predicted").position("atequipment").getCurrentValue(); this.calcDistanceBEP(efficiency,maxEfficiency,lowestEfficiency); } @@ -238,8 +258,8 @@ class MachineGroup { if(machine.measurements.type("flow").variant("measured").position("downstream").getCurrentValue()){ flow = machine.measurements.type("flow").variant("measured").position("downstream").getCurrentValue(); } - else if(machine.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue()){ - flow = machine.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue(); + else if(machine.measurements.type("flow").variant("predicted").position("atequipment").getCurrentValue()){ + flow = machine.measurements.type("flow").variant("predicted").position("atequipment").getCurrentValue(); } else{ this.logger.error("Dont perform calculation at all seeing that there is a machine working but we dont know the flow its producing"); @@ -264,7 +284,7 @@ class MachineGroup { Object.keys(machines).forEach(machineId => { const state = machines[machineId].state.getCurrentState(); - const validActionForMode = machines[machineId].isValidActionForMode("execSequence", "auto"); + const validActionForMode = machines[machineId].isValidActionForMode("execsequence", "auto"); // Reasons why a machine is not valid for the combination @@ -313,42 +333,71 @@ class MachineGroup { calcBestCombination(combinations, Qd) { let bestCombination = null; - - //keep track of totals let bestPower = Infinity; let bestFlow = 0; let bestCog = 0; - - combinations.forEach(combination => { - let flowDistribution = []; // Stores the flow distribution for the best combination + combinations.forEach(combination => { + let flowDistribution = []; let totalCoG = 0; let totalPower = 0; - let totalFlow = 0; - // Calculate the total CoG for the current combination - combination.forEach(machineId => { totalCoG += ( Math.round(this.machines[machineId].NCog * 100 ) /100 ) ; }); - - // Calculate the total power for the current combination + // Sum normalized CoG for the combination combination.forEach(machineId => { - let flow = 0; - - // Prevent division by zero - if (totalCoG === 0) { - // Distribute flow equally among all pumps - flow = Qd / combination.length; - - } else { - // Normal CoG-based distribution - flow = (this.machines[machineId].NCog / totalCoG) * Qd ; - this.logger.debug(`Machine Normalized CoG-based distribution ${machineId} flow: ${flow}`); - } - totalFlow += flow; - totalPower += this.machines[machineId].inputFlowCalcPower(flow); - flowDistribution.push({ machineId: machineId,flow: flow }); + totalCoG += Math.round((this.machines[machineId].NCog || 0) * 100) / 100; + }); + + // Initial CoG-based distribution + combination.forEach(machineId => { + let flow = 0; + + if (totalCoG === 0) { + flow = Qd / combination.length; + } else { + flow = ((this.machines[machineId].NCog || 0) / totalCoG) * Qd; + this.logger.debug(`Machine Normalized CoG-based distribution ${machineId} flow: ${flow}`); + } + + flowDistribution.push({ machineId, flow }); + }); + + // Clamp to min/max and spill leftover once + const clamped = flowDistribution.map(entry => { + const machine = this.machines[entry.machineId]; + const min = machine.predictFlow.currentFxyYMin; + const max = machine.predictFlow.currentFxyYMax; + const clampedFlow = Math.min(max, Math.max(min, entry.flow)); + return { ...entry, flow: clampedFlow, min, max, desired: entry.flow }; + }); + + let remainder = Qd - clamped.reduce((sum, entry) => sum + entry.flow, 0); + + if (Math.abs(remainder) > 1e-6) { + const adjustable = clamped.filter(entry => + remainder > 0 ? entry.flow < entry.max : entry.flow > entry.min + ); + const weightSum = adjustable.reduce((sum, entry) => sum + entry.desired, 0) || adjustable.length; + + adjustable.forEach(entry => { + const weight = entry.desired / weightSum || 1 / adjustable.length; + const delta = remainder * weight; + const next = remainder > 0 + ? Math.min(entry.max, entry.flow + delta) + : Math.max(entry.min, entry.flow + delta); + + remainder -= (next - entry.flow); + entry.flow = next; + }); + } + + flowDistribution = clamped; + + let totalFlow = 0; + flowDistribution.forEach(({ machineId, flow }) => { + totalFlow += flow; + totalPower += this.machines[machineId].inputFlowCalcPower(flow); }); - // Update the best combination if the current one is better if (totalPower < bestPower) { this.logger.debug(`New best combination found: ${totalPower} < ${bestPower}`); this.logger.debug(`combination ${JSON.stringify(flowDistribution)}`); @@ -362,6 +411,177 @@ class MachineGroup { return { bestCombination, bestPower, bestFlow, bestCog }; } + + // Estimate the local dP/dQ slopes around the BEP for the provided machine. + estimateSlopesAtBEP(machine, Q_BEP, delta = 1.0) { + const fallback = { + slopeLeft: 0, + slopeRight: 0, + alpha: 1, + Q_BEP: Q_BEP || 0, + P_BEP: 0 + }; + + const minFlow = machine.predictFlow.currentFxyYMin; + const maxFlow = machine.predictFlow.currentFxyYMax; + const span = Math.max(0, maxFlow - minFlow); + const normalizedCog = Math.max(0, Math.min(1, machine.NCog || 0)); + const targetBEP = Q_BEP ?? (minFlow + span * normalizedCog); + const clampFlow = (flow) => Math.min(maxFlow, Math.max(minFlow, flow)); // ensure within bounds using small helper function + const center = clampFlow(targetBEP); + const deltaSafe = Math.max(delta, 0.01); + const leftFlow = clampFlow(center - deltaSafe); + const rightFlow = clampFlow(center + deltaSafe); + const powerAt = (flow) => machine.inputFlowCalcPower(flow); // helper to get power at a given flow + const P_center = powerAt(center); + const P_left = powerAt(leftFlow); + const P_right = powerAt(rightFlow); + const slopeLeft = (P_center - P_left) / Math.max(1e-6, center - leftFlow); + const slopeRight = (P_right - P_center) / Math.max(1e-6, rightFlow - center); + const alpha = Math.max(1e-6, (Math.abs(slopeLeft) + Math.abs(slopeRight)) / 2); + + return { + slopeLeft, + slopeRight, + alpha, + Q_BEP: center, + P_BEP: P_center + }; + + } + + //Redistribute remaining demand using slope-based weights so flatter curves attract more flow. + redistributeFlowBySlope(pumpInfos, flowDistribution, delta, directional = true) { + const tolerance = 1e-3; // Small tolerance to avoid infinite loops + let remaining = delta; // Remaining flow to distribute + const entryMap = new Map(flowDistribution.map(entry => [entry.machineId, entry])); // Map for quick access + + // Loop until remaining flow is within tolerance + while (Math.abs(remaining) > tolerance) { + const increasing = remaining > 0; // Determine if we are increasing or decreasing flow + // Build candidates with capacity and weight + const candidates = pumpInfos.map(info => { + const entry = entryMap.get(info.id); + if (!entry) { return null; } + const capacity = increasing ? info.maxFlow - entry.flow : entry.flow - info.minFlow; // Calculate available capacity based on direction + if (capacity <= tolerance) { return null; } + + const slope = increasing + ? (directional ? info.slopes.slopeRight : info.slopes.alpha) + : (directional ? info.slopes.slopeLeft : info.slopes.alpha); + + const weight = 1 / Math.max(1e-6, Math.abs(slope) || info.slopes.alpha || 1); + return { entry, capacity, weight }; + }).filter(Boolean); + + if (!candidates.length) { break; } // No candidates available, exit loop + + const weightSum = candidates.reduce((sum, candidate) => sum + candidate.weight * candidate.capacity, 0); // weighted sum of capacities + if (weightSum <= 0) { break; } // Avoid division by zero + + let progress = 0; + // Distribute remaining flow among candidates based on their weights and capacities + candidates.forEach(candidate => { + let share = (candidate.weight * candidate.capacity / weightSum) * Math.abs(remaining); + share = Math.min(share, candidate.capacity); // Ensure we don't exceed capacity + if (share <= 0) { return; } // Skip if no share to allocate + if (increasing) { + candidate.entry.flow += share; + } else { + candidate.entry.flow -= share; + } + progress += share; // Track total progress made in this iteration + }); + + if (progress <= tolerance) { break; } + remaining += increasing ? -progress : progress; // Update remaining flow to distribute + } + } + + // BEP-gravitation based combination finder that biases allocation around each pump's BEP. + calcBestCombinationBEPGravitation(combinations, Qd, method = "BEP-Gravitation-Directional") { + let bestCombination = null; + let bestPower = Infinity; + let bestFlow = 0; + let bestCog = 0; + let bestDeviation = Infinity; + const directional = method === "BEP-Gravitation-Directional"; + + combinations.forEach(combination => { + const pumpInfos = combination.map(machineId => { + const machine = this.machines[machineId]; + const minFlow = machine.predictFlow.currentFxyYMin; + const maxFlow = machine.predictFlow.currentFxyYMax; + const span = Math.max(0, maxFlow - minFlow); + const NCog = Math.max(0, Math.min(1, machine.NCog || 0)); + const estimatedBEP = minFlow + span * NCog; // Estimated BEP flow based on current curve + const slopes = this.estimateSlopesAtBEP(machine, estimatedBEP); + return { + id: machineId, + machine, + minFlow, + maxFlow, + NCog, + Q_BEP: slopes.Q_BEP, + slopes + }; + }); + + // Skip if no pumps in combination + if (pumpInfos.length === 0) { return; } + + // Start at BEP flows + const flowDistribution = pumpInfos.map(info => ({ + machineId: info.id, + flow: Math.min(info.maxFlow, Math.max(info.minFlow, info.Q_BEP)) + })); + + let totalFlow = flowDistribution.reduce((sum, entry) => sum + entry.flow, 0); // Initial total flow + const delta = Qd - totalFlow; // Difference to target demand + if (Math.abs(delta) > 1e-6) { + this.redistributeFlowBySlope(pumpInfos, flowDistribution, delta, directional); + } + + let totalPower = 0; + totalFlow = 0; + flowDistribution.forEach(entry => { + const info = pumpInfos.find(info => info.id === entry.machineId); + const flow = Math.min(info.maxFlow, Math.max(info.minFlow, entry.flow)); + entry.flow = flow; + totalFlow += flow; + totalPower += info.machine.inputFlowCalcPower(flow); + }); + + const totalCog = pumpInfos.reduce((sum, info) => sum + info.NCog, 0); + const deviation = pumpInfos.reduce((sum, info) => { + const entry = flowDistribution.find(item => item.machineId === info.id); + const deltaFlow = entry ? (entry.flow - info.Q_BEP) : 0; + return sum + (deltaFlow * deltaFlow) * (info.slopes.alpha || 1); + }, 0); + + const shouldUpdate = totalPower < bestPower || + (totalPower === bestPower && deviation < bestDeviation); + + if (shouldUpdate) { + bestCombination = flowDistribution.map(entry => ({ ...entry })); + bestPower = totalPower; + bestFlow = totalFlow; + bestCog = totalCog; + bestDeviation = deviation; + } + }); + + return { + bestCombination, + bestPower, + bestFlow, + bestCog, + bestDeviation, + method + }; + } + + // -------- Mode and Input Management -------- // isValidActionForMode(action, mode) { const allowedActionsSet = this.config.mode.allowedActions[mode] || []; @@ -440,7 +660,26 @@ class MachineGroup { // fetch all valid combinations that meet expectations const combinations = this.validPumpCombinations(this.machines, Qd, powerCap); - const bestResult = this.calcBestCombination(combinations, Qd); + + if (!combinations || combinations.length === 0) { + this.logger.warn(`Demand: ${Qd.toFixed(2)} -> No valid combination found (empty set).`); + return; + } + + // Decide which optimization routine we run. Defaults to BEP-based gravitation with directionality. + const optimizationMethod = this.config.optimization?.method || "BEP-Gravitation-Directional"; + let bestResult; + if (optimizationMethod === "NCog") { + bestResult = this.calcBestCombination(combinations, Qd); + } else if ( + optimizationMethod === "BEP-Gravitation" || + optimizationMethod === "BEP-Gravitation-Directional" + ) { + bestResult = this.calcBestCombinationBEPGravitation(combinations, Qd, optimizationMethod); + } else { + this.logger.warn(`Unknown optimization method '${optimizationMethod}', falling back to BEP-Gravitation-Directional.`); + bestResult = this.calcBestCombinationBEPGravitation(combinations, Qd, "BEP-Gravitation-Directional"); + } if(bestResult.bestCombination === null){ this.logger.warn(`Demand: ${Qd.toFixed(2)} -> No valid combination found => not updating control `); @@ -451,10 +690,10 @@ class MachineGroup { this.logger.debug(`Moving to demand: ${Qd.toFixed(2)} -> Pumps: [${debugInfo}] => Total Power: ${bestResult.bestPower.toFixed(2)}`); //store the total delivered power - this.measurements.type("power").variant("predicted").position("atEquipment").value(bestResult.bestPower); - this.measurements.type("flow").variant("predicted").position("downstream").value(bestResult.bestFlow); - this.measurements.type("efficiency").variant("predicted").position("atEquipment").value(bestResult.bestFlow / bestResult.bestPower); - this.measurements.type("Ncog").variant("predicted").position("atEquipment").value(bestResult.bestCog); + this.measurements.type("power").variant("predicted").position("atequipment").value(bestResult.bestPower); + this.measurements.type("flow").variant("predicted").position("atequipment").value(bestResult.bestFlow); + this.measurements.type("efficiency").variant("predicted").position("atequipment").value(bestResult.bestFlow / bestResult.bestPower); + this.measurements.type("Ncog").variant("predicted").position("atequipment").value(bestResult.bestCog); await Promise.all(Object.entries(this.machines).map(async ([machineId, machine]) => { // Find the flow for this machine in the best combination @@ -469,16 +708,16 @@ class MachineGroup { } if( (flow <= 0 ) && ( machineStates[machineId] === "operational" || machineStates[machineId] === "accelerating" || machineStates[machineId] === "decelerating" ) ){ - await machine.handleInput("parent", "execSequence", "shutdown"); + await machine.handleInput("parent", "execsequence", "shutdown"); } if(machineStates[machineId] === "idle" && flow > 0){ - await machine.handleInput("parent", "execSequence", "startup"); - await machine.handleInput("parent", "flowMovement", flow); + await machine.handleInput("parent", "execsequence", "startup"); + await machine.handleInput("parent", "flowmovement", flow); } if(machineStates[machineId] === "operational" && flow > 0 ){ - await machine.handleInput("parent", "flowMovement", flow); + await machine.handleInput("parent", "flowmovement", flow); } })); } @@ -551,7 +790,7 @@ class MachineGroup { filterOutUnavailableMachines(list) { const newList = list.filter(({ id, machine }) => { const state = machine.state.getCurrentState(); - const validActionForMode = machine.isValidActionForMode("execSequence", "auto"); + const validActionForMode = machine.isValidActionForMode("execsequence", "auto"); return !(state === "off" || state === "coolingdown" || state === "stopping" || state === "emergencystop" || !validActionForMode); }); @@ -681,10 +920,10 @@ class MachineGroup { this.logger.debug(`Priority control for demand: ${totalFlow.toFixed(2)} -> Active pumps: [${debugInfo}] => Total Power: ${totalPower.toFixed(2)}`); // Store measurements - this.measurements.type("power").variant("predicted").position("atEquipment").value(totalPower); - this.measurements.type("flow").variant("predicted").position("downstream").value(totalFlow); - this.measurements.type("efficiency").variant("predicted").position("atEquipment").value(totalFlow / totalPower); - this.measurements.type("Ncog").variant("predicted").position("atEquipment").value(totalCog); + this.measurements.type("power").variant("predicted").position("atequipment").value(totalPower); + this.measurements.type("flow").variant("predicted").position("atequipment").value(totalFlow); + this.measurements.type("efficiency").variant("predicted").position("atequipment").value(totalFlow / totalPower); + this.measurements.type("Ncog").variant("predicted").position("atequipment").value(totalCog); this.logger.debug(`Flow distribution: ${JSON.stringify(flowDistribution)}`); // Apply the flow distribution to machines @@ -694,13 +933,13 @@ class MachineGroup { const currentState = this.machines[machineId].state.getCurrentState(); if (flow <= 0 && (currentState === "operational" || currentState === "accelerating" || currentState === "decelerating")) { - await machine.handleInput("parent", "execSequence", "shutdown"); + await machine.handleInput("parent", "execsequence", "shutdown"); } else if (currentState === "idle" && flow > 0) { - await machine.handleInput("parent", "execSequence", "startup"); + await machine.handleInput("parent", "execsequence", "startup"); } else if (currentState === "operational" && flow > 0) { - await machine.handleInput("parent", "flowMovement", flow); + await machine.handleInput("parent", "flowmovement", flow); } })); } @@ -716,7 +955,7 @@ class MachineGroup { if(input < 0 ){ //turn all machines off await Promise.all(Object.entries(this.machines).map(async ([machineId, machine]) => { - if (this.isMachineActive(machineId)) { await machine.handleInput("parent", "execSequence", "shutdown"); } + if (this.isMachineActive(machineId)) { await machine.handleInput("parent", "execsequence", "shutdown"); } })); return; } @@ -780,13 +1019,13 @@ class MachineGroup { const currentState = this.machines[machineId].state.getCurrentState(); if (ctrl < 0 && (currentState === "operational" || currentState === "accelerating" || currentState === "decelerating")) { - await machine.handleInput("parent", "execSequence", "shutdown"); + await machine.handleInput("parent", "execsequence", "shutdown"); } else if (currentState === "idle" && ctrl >= 0) { - await machine.handleInput("parent", "execSequence", "startup"); + await machine.handleInput("parent", "execsequence", "startup"); } else if (currentState === "operational" && ctrl > 0) { - await machine.handleInput("parent", "execMovement", ctrl); + await machine.handleInput("parent", "execmovement", ctrl); } })); @@ -796,8 +1035,8 @@ class MachineGroup { // fetch and store measurements Object.entries(this.machines).forEach(([machineId, machine]) => { - const powerValue = machine.measurements.type("power").variant("predicted").position("atEquipment").getCurrentValue(); - const flowValue = machine.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue(); + const powerValue = machine.measurements.type("power").variant("predicted").position("atequipment").getCurrentValue(); + const flowValue = machine.measurements.type("flow").variant("predicted").position("atequipment").getCurrentValue(); if (powerValue !== null) { totalPower.push(powerValue); @@ -807,11 +1046,11 @@ class MachineGroup { } }); - 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("power").variant("predicted").position("atequipment").value(totalPower.reduce((a, b) => a + b, 0)); + this.measurements.type("flow").variant("predicted").position("atequipment").value(totalFlow.reduce((a, b) => a + b, 0)); if(totalPower.reduce((a, b) => a + b, 0) > 0){ - this.measurements.type("efficiency").variant("predicted").position("atEquipment").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)); } } @@ -821,6 +1060,13 @@ class MachineGroup { } async handleInput(source, demand, powerCap = Infinity, priorityList = null) { + + const demandQ = parseFloat(demand); + + if(!Number.isFinite(demandQ)){ + this.logger.error(`Invalid flow demand input: ${demand}. Must be a finite number.`); + return; + } //abort current movements await this.abortActiveMovements("new demand received"); @@ -828,7 +1074,6 @@ class MachineGroup { const scaling = this.scaling; const mode = this.mode; 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}`); @@ -857,7 +1102,6 @@ class MachineGroup { break; case "normalized": - 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`); @@ -875,7 +1119,6 @@ class MachineGroup { } - // Execute control based on mode switch(mode) { case "prioritycontrol": @@ -911,7 +1154,7 @@ class MachineGroup { async turnOffAllMachines(){ await Promise.all(Object.entries(this.machines).map(async ([machineId, machine]) => { - if (this.isMachineActive(machineId)) { await machine.handleInput("parent", "execSequence", "shutdown"); } + if (this.isMachineActive(machineId)) { await machine.handleInput("parent", "execsequence", "shutdown"); } })); } @@ -929,7 +1172,7 @@ class MachineGroup { this.measurements.getVariants(type).forEach(variant => { const downstreamVal = this.measurements.type(type).variant(variant).position("downstream").getCurrentValue(); - const atEquipmentVal = this.measurements.type(type).variant(variant).position("atEquipment").getCurrentValue(); + const atEquipmentVal = this.measurements.type(type).variant(variant).position("atequipment").getCurrentValue(); const upstreamVal = this.measurements.type(type).variant(variant).position("upstream").getCurrentValue(); if (downstreamVal != null) { @@ -939,7 +1182,7 @@ class MachineGroup { output[`upstream_${variant}_${type}`] = upstreamVal; } if (atEquipmentVal != null) { - output[`atEquipment_${variant}_${type}`] = atEquipmentVal; + output[`atequipment${variant}_${type}`] = atEquipmentVal; } if (downstreamVal != null && upstreamVal != null) { const diffVal = this.measurements.type(type).variant(variant).difference().value; @@ -965,7 +1208,7 @@ class MachineGroup { module.exports = MachineGroup; /* - +const {coolprop} = require('generalFunctions'); const Machine = require('../../rotatingMachine/src/specificClass'); const Measurement = require('../../measurement/src/specificClass'); const specs = require('../../generalFunctions/datasets/assetData/curves/hidrostal-H05K-S03R.json'); @@ -993,9 +1236,9 @@ function createBaseMachineConfig(machineNum, name,specs) { mode: { current: "auto", allowedActions: { - auto: ["execSequence", "execMovement", "statusCheck"], - virtualControl: ["execMovement", "statusCheck"], - fysicalControl: ["statusCheck"] + auto: ["execsequence", "execmovement", "statuscheck"], + virtualControl: ["execmovement", "statuscheck"], + fysicalControl: ["statuscheck"] }, allowedSources: { auto: ["parent", "GUI"], @@ -1103,7 +1346,7 @@ async function makeMachines(){ const percMax = 100; try{ - /* + for(let demand = mg.dynamicTotals.flow.min ; demand <= mg.dynamicTotals.flow.max ; demand += 2){ //set pressure @@ -1155,4 +1398,4 @@ async function makeMachines(){ makeMachines(); -//*/ \ No newline at end of file +//*/