const test = require('node:test'); const assert = require('node:assert/strict'); const { calcBestCombinationBEPGravitation, estimateSlopesAtBEP, redistributeFlowBySlope, } = require('../../src/optimizer/bepGravitation'); const optimizerIndex = require('../../src/optimizer'); function makeMachine({ id, fMin = 0, fMax = 100, NCog = 0.5, costFn } = {}) { return { config: { general: { id } }, NCog, predictFlow: { currentFxyYMin: fMin, currentFxyYMax: fMax }, predictPower: { currentFxyYMin: 0, currentFxyYMax: fMax * 2 }, // Default: convex cost so marginal-cost refinement has a clear winner. inputFlowCalcPower: costFn ?? ((f) => 0.001 * f * f + f), }; } function mkCtx(machines) { return { machines, groupCurves: { groupFlow: (m) => m.predictFlow, groupPower: (m) => m.predictPower, groupNCog: (m) => m.NCog ?? 0, groupCalcPower: (m, f) => m.inputFlowCalcPower(f), }, logger: { debug: () => {} }, }; } test('estimateSlopesAtBEP: returns finite slopes/alpha/Q_BEP/P_BEP for a typical machine', () => { const machine = makeMachine({ id: 'a', fMin: 10, fMax: 90, NCog: 0.5 }); const ctx = mkCtx({ a: machine }); const slopes = estimateSlopesAtBEP(machine, 50, ctx); assert.ok(Number.isFinite(slopes.slopeLeft)); assert.ok(Number.isFinite(slopes.slopeRight)); assert.ok(Number.isFinite(slopes.alpha)); assert.ok(slopes.alpha > 0); assert.ok(Number.isFinite(slopes.Q_BEP)); assert.equal(slopes.Q_BEP, 50); assert.ok(Number.isFinite(slopes.P_BEP)); }); test('redistributeFlowBySlope: redistributes within capacity, never exceeding min/max', () => { const pumpInfos = [ { id: 'a', minFlow: 0, maxFlow: 50, slopes: { slopeLeft: 1, slopeRight: 1, alpha: 1 } }, { id: 'b', minFlow: 0, maxFlow: 50, slopes: { slopeLeft: 1, slopeRight: 1, alpha: 1 } }, ]; const flowDist = [{ machineId: 'a', flow: 10 }, { machineId: 'b', flow: 10 }]; redistributeFlowBySlope(pumpInfos, flowDist, 30); // add 30 across 2 pumps const total = flowDist.reduce((s, e) => s + e.flow, 0); assert.ok(Math.abs(total - 50) < 1e-2, `expected total ~50, got ${total}`); for (const e of flowDist) { assert.ok(e.flow <= 50 + 1e-6 && e.flow >= 0 - 1e-6); } }); test('marginal-cost refinement bounded (no infinite loop on a flat-curve scenario)', () => { // Flat cost everywhere -> marginal cost identical -> loop must exit cleanly. const machines = { a: makeMachine({ id: 'a', fMin: 0, fMax: 100, costFn: (f) => f }), b: makeMachine({ id: 'b', fMin: 0, fMax: 100, costFn: (f) => f }), }; const ctx = mkCtx(machines); const start = Date.now(); const res = calcBestCombinationBEPGravitation([['a', 'b']], 30, ctx); const elapsed = Date.now() - start; assert.ok(elapsed < 1000, `refinement should be fast, took ${elapsed}ms`); assert.ok(res.bestCombination); const total = res.bestCombination.reduce((s, e) => s + e.flow, 0); assert.ok(Math.abs(total - 30) < 1e-2, `total should be ~Qd, got ${total}`); }); test('method selection: directional uses slopeRight/slopeLeft; non-directional uses alpha', () => { // Asymmetric slopes so the two methods produce different allocations. const pumpInfos = [ { id: 'a', minFlow: 0, maxFlow: 100, slopes: { slopeLeft: 10, slopeRight: 0.1, alpha: 5 } }, { id: 'b', minFlow: 0, maxFlow: 100, slopes: { slopeLeft: 0.1, slopeRight: 10, alpha: 5 } }, ]; const distDir = [{ machineId: 'a', flow: 10 }, { machineId: 'b', flow: 10 }]; const distAlpha = [{ machineId: 'a', flow: 10 }, { machineId: 'b', flow: 10 }]; // Increase by 30 -> directional should prefer 'a' (shallow right slope). redistributeFlowBySlope(pumpInfos, distDir, 30, true); // Alpha mode: same slope-weight per pump -> roughly equal split. redistributeFlowBySlope(pumpInfos, distAlpha, 30, false); const aDir = distDir.find(e => e.machineId === 'a').flow; const bDir = distDir.find(e => e.machineId === 'b').flow; const aAlpha = distAlpha.find(e => e.machineId === 'a').flow; const bAlpha = distAlpha.find(e => e.machineId === 'b').flow; assert.ok(aDir > bDir, `directional should send more to a (got a=${aDir}, b=${bDir})`); assert.ok(Math.abs(aAlpha - bAlpha) < 1e-2, `alpha mode should split evenly (got a=${aAlpha}, b=${bAlpha})`); // pickOptimizer wires the right module. assert.equal(optimizerIndex.pickOptimizer('BEP-Gravitation-Directional').calcBestCombinationBEPGravitation, calcBestCombinationBEPGravitation); assert.equal(optimizerIndex.pickOptimizer('BEP-Gravitation').calcBestCombinationBEPGravitation, calcBestCombinationBEPGravitation); assert.ok(optimizerIndex.pickOptimizer('CoG').calcBestCombination); });