Files
machineGroupControl/test/basic/bepGravitation.basic.test.js

111 lines
4.9 KiB
JavaScript
Raw Normal View History

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);
});