// Empirical answer: does absDistFromPeak / relDistFromPeak move with demand? // Drives the live MGC + 3 identical pumps (same model as the dashboard demo) // across a demand sweep and records what each metric actually does. The test // asserts the expected qualitative shape, so any future change that // regresses BEP-distance sensitivity will fail loudly. const test = require('node:test'); const assert = require('node:assert/strict'); const RM = require('../../../rotatingMachine/src/specificClass'); const MGC = require('../../src/specificClass'); const { getOutput } = require('../../src/io/output'); const PUMP_MODEL = 'hidrostal-H05K-S03R'; const HEADER_DP_MBAR = 1100; // stateConfig.time = 0 for every transition so warmup/cooldown don't add real // seconds — without this the 4-demand sweep × 3 pumps takes >120s and the test // runner kills it. const INSTANT_STATE = { time: { starting: 0, warmingup: 0, operational: 0, accelerating: 0, decelerating: 0, stopping: 0, coolingdown: 0, idle: 0, maintenance: 0, emergencystop: 0, off: 0 }, }; function mkPump(id) { return new RM({ general: { id, name: id }, asset: { model: PUMP_MODEL, unit: 'm3/h' }, }, INSTANT_STATE); } async function buildGroupWithPressure() { const mgc = new MGC({ general: { id: 'mgc', name: 'mgc' }, functionality: { mode: { current: 'optimalControl' }, positionVsParent: 'atEquipment' }, }); const pumps = ['A','B','C'].map(l => mkPump(`pump-${l}`)); for (const p of pumps) { mgc.childRegistrationUtils?.registerChild?.(p, 'atEquipment'); } for (const p of pumps) { p.updateMeasuredPressure(0, 'upstream', { timestamp: Date.now(), unit: 'mbar', childName: 'sim-up' }); p.updateMeasuredPressure(HEADER_DP_MBAR, 'downstream', { timestamp: Date.now(), unit: 'mbar', childName: 'sim-dn' }); } // Let pressure events propagate through the emitter chain. await new Promise(r => setTimeout(r, 50)); return { mgc, pumps }; } async function sweepDemand(mgc, demands_m3h) { const rows = []; for (const Qd_m3h of demands_m3h) { const Qd = Qd_m3h / 3600; // m3/h → m3/s try { await mgc.handleInput('parent', Qd); } catch (e) { /* turnOff or no-combination paths are part of the contract */ } await new Promise(r => setTimeout(r, 30)); const out = getOutput(mgc); rows.push({ demand: Qd_m3h, flow: out.atEquipment_predicted_flow, eta: out.atEquipment_predicted_efficiency, absDist: out.absDistFromPeak, relDist: out.relDistFromPeak, ncog: out.atEquipment_predicted_Ncog, nAct: out.machineCountActive, }); } return rows; } test('absDistFromPeak rises when demand pushes pumps off BEP', async () => { const { mgc } = await buildGroupWithPressure(); // Sweep covers "comfortably within combined BEP" (low/mid) and "over the // group's BEP envelope, pumps must push" (high). For hidrostal-H05K-S03R // at 1100 mbar, single-pump max ≈ 230 m³/h, 3-pump max ≈ 680 m³/h. Demand // 600 m³/h forces each pump well past BEP. const rows = await sweepDemand(mgc, [100, 200, 300, 600]); // Sanity: pumps actually accepted the demand and flow is rising. assert.ok(rows[3].flow > rows[0].flow + 100, `flow should rise with demand, got ${JSON.stringify(rows.map(r => r.flow))}`); // absDist should be larger at over-capacity demand than at within-capacity. // Use a generous tolerance — the test asserts the QUALITATIVE shape, not // exact numbers (which depend on curve interpolation). const lowAbs = Math.min(rows[0].absDist, rows[1].absDist, rows[2].absDist); const highAbs = rows[3].absDist; assert.ok(highAbs > lowAbs + 0.005, `absDistFromPeak should be larger off-BEP than on-BEP. ` + `low (Qd∈{100,200,300}): min=${lowAbs}, high (Qd=600): ${highAbs}. ` + `Full rows: ${JSON.stringify(rows, null, 2)}`); }); test('absDistFromPeak ≈ 0 across the within-BEP demand range (working as designed)', async () => { const { mgc } = await buildGroupWithPressure(); const rows = await sweepDemand(mgc, [100, 200, 300]); // The BEP-Gravitation optimizer is supposed to KEEP us at BEP for demands // the group can absorb at BEP. So absDist staying near zero across the // "easy" range is the correct outcome — NOT a bug. This test pins that // behaviour so any future "fix" that introduces drift here fails. for (const r of rows) { assert.ok(r.absDist != null && r.absDist < 0.02, `at demand ${r.demand} m³/h, absDist=${r.absDist} should be near zero ` + `(optimizer holds BEP); only off-BEP demand should produce noticeable drift`); } }); test('relDistFromPeak is structurally ill-defined for homogeneous pump groups', async () => { const { mgc } = await buildGroupWithPressure(); const rows = await sweepDemand(mgc, [100, 200, 300, 600]); // 3 identical pumps → all cogs equal → max=mean=min in calcDistanceBEP. // The interpolation [max..min] → [0..1] collapses; the metric is // mathematically undefined here. Whatever value comes out is float-noise // dependent and MUST NOT be interpreted as "BEP distance percentage". // This test documents the limitation as a contract; it deliberately does // not assert a specific value — it asserts the metric does NOT move // monotonically with demand (which it shouldn't for identical pumps). const uniqueRel = new Set(rows.map(r => r.relDist)); assert.ok(uniqueRel.size <= 2, `relDistFromPeak is expected to be effectively constant for identical pumps. ` + `Distinct values across sweep: ${[...uniqueRel].join(', ')}. ` + `If you want this metric to track demand, configure pumps with different ` + `peak η (different models or different curve scaling).`); });