fix(mgc): just-in-time startup in rendezvous planner (kill staging flow bump)
Delay a startup's execsequence by (t* − eta) instead of firing it at tick 0. Previously the ladder fired immediately for every starting pump; a faster-than-slowest startup then reached `operational` early and sat at its minimum flow (calcFlow at min position is non-zero) from warmup-end until its delayed ramp — leaking ~one pump's minimum flow into the group total before the rendezvous instant t* (the 207→309 staging bump observed live). Now the whole startup (ladder + ramp) is delayed: the ladder begins at (t* − eta), completes at (t* − rampS), then the queued flowmovement ramps to finish exactly at t*. The slowest pump (eta == t*) still fires at tick 0. Sum-of-flows is monotonic through the transition. Updated movementScheduler.basic.test.js mixed-speed multi-startup assertions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -242,34 +242,29 @@ test('plan: mixed-speed multi-startup — fast pumps wait so all land at tStar t
|
||||
// tStar = max(eta_A, eta_B, eta_C) = 130 s.
|
||||
assert.ok(Math.abs(out.tStarS - 130) < 0.01, `tStar should be 130; got ${out.tStarS}`);
|
||||
|
||||
// execsequence fires at 0 for ALL idle pumps (the ladder must start now).
|
||||
// Just-in-time: the WHOLE startup (ladder + ramp) is delayed by (tStar −
|
||||
// eta), so both execsequence and flowmovement fire at the same delayed
|
||||
// tick. eta_A = 30 + 33.33 ≈ 63.33, eta_B = 40, eta_C = 130.
|
||||
// A: round(130 − 63.33) = 67
|
||||
// B: round(130 − 40) = 90
|
||||
// C: round(130 − 130) = 0 (slowest — defines tStar, fires now)
|
||||
const delays = { A: Math.round(130 - (30 + 100 / 3)), B: 90, C: 0 };
|
||||
for (const id of ['A', 'B', 'C']) {
|
||||
const exec = out.commands.find((c) => c.machineId === id && c.action === 'execsequence');
|
||||
const flow = out.commands.find((c) => c.machineId === id && c.action === 'flowmovement');
|
||||
assert.ok(exec, `${id} execsequence present`);
|
||||
assert.equal(exec.fireAtTickN, 0, `${id} execsequence fires immediately`);
|
||||
assert.ok(flow, `${id} flowmovement present`);
|
||||
assert.equal(exec.fireAtTickN, delays[id], `${id} ladder delayed to land at tStar`);
|
||||
assert.equal(flow.fireAtTickN, delays[id], `${id} flowmovement fires with the ladder`);
|
||||
}
|
||||
|
||||
// flowmovement gating — each pump's ramp must FINISH at tStar=130.
|
||||
const flowA = out.commands.find((c) => c.machineId === 'A' && c.action === 'flowmovement');
|
||||
const flowB = out.commands.find((c) => c.machineId === 'B' && c.action === 'flowmovement');
|
||||
const flowC = out.commands.find((c) => c.machineId === 'C' && c.action === 'flowmovement');
|
||||
|
||||
// A (medium): rampStart = 130 − 33.33 ≈ 96.67 → fireAtTickN = 97.
|
||||
assert.equal(flowA.fireAtTickN, Math.round(130 - 100 / 3));
|
||||
// B (fast): rampStart = 130 − 10 = 120 → fireAtTickN = 120.
|
||||
assert.equal(flowB.fireAtTickN, 120);
|
||||
// C (slow, defines tStar): rendezvousRampStart = 130 − 100 = 30 == ladderS,
|
||||
// so no extra delay needed — fall back to fireAtTickN=0 and let
|
||||
// the pump's delayedMove fire it naturally at warmup-end.
|
||||
assert.equal(flowC.fireAtTickN, 0);
|
||||
|
||||
// Sanity: with these schedules, all three pumps' ramps end at the
|
||||
// same wall-clock instant (within rounding).
|
||||
// A: 97 + 100/3 ≈ 130.33
|
||||
// B: 120 + 10 = 130
|
||||
// C: 30 (delayedMove) + 100 = 130
|
||||
// Max spread ≈ 0.33 s — far better than the per-eta spread of
|
||||
// 130 − 40 = 90 s the planner would produce without this gating.
|
||||
// Sanity: with the ladder delayed, each pump reaches `operational` only at
|
||||
// (delay + ladderS) and its ramp ends at the same wall-clock instant ≈ 130.
|
||||
// A: 67 + 30 (op) + 33.33 ≈ 130.33
|
||||
// B: 90 + 30 (op) + 10 = 130
|
||||
// C: 0 + 30 (op) + 100 = 130
|
||||
// No pump sits at `operational` (and minimum flow) before its ramp — that
|
||||
// early min-flow was the staging bump this just-in-time start removes.
|
||||
});
|
||||
|
||||
test('plan: zero-velocity machine is demoted (infinite eta) but does not crash', () => {
|
||||
|
||||
Reference in New Issue
Block a user