Lane B S16 pool: the frame-determinism bound is pinned — and it is 66.7 ms, not 83
ROADMAP carried '>83 ms determinism bound (documented, un-tested)'. Measured: the spiral guard fires whenever a step() consumes MAX_SUBSTEPS, dropping even a legal sub-SIM_DT residual — so the unconditionally-lossless bound is (MAX_SUBSTEPS-1)*SIM_DT = 66.7 ms; 67-83 ms may drop up to one SIM_DT by accumulator phase; >=83 ms always drops. Pinned AS MEASURED (no retune): frames <=66 ms converge on the fixed trace at equal sim clocks with zero wall time lost; one 120 ms hitch drops exactly its overflow (36.7 ms). Mutations red-then-green: MAX_SUBSTEPS shrunk (6 s went missing); guard deleted (deficit 3 ms vs expected 37). Test header is the documented waiver. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
edce5c2758
commit
e5df63128a
@ -467,6 +467,65 @@ test('determinism: variable frame dt matches fixed dt', () => {
|
||||
return 'ragged frame times converge on the fixed-dt trace';
|
||||
});
|
||||
|
||||
// SPRINT16 pool (B) — ROADMAP's "the >83 ms frame determinism bound
|
||||
// (documented, un-tested)", now tested — and MEASURED SHARPER than documented.
|
||||
// step() burns wall time in SIM_DT chunks up to MAX_SUBSTEPS(5) per call, and
|
||||
// `if (n === MAX_SUBSTEPS) this._acc = 0` fires whenever a call CONSUMES five
|
||||
// substeps — including when the leftover residual was a perfectly legal
|
||||
// sub-SIM_DT remainder. So the unconditionally-lossless frame bound is
|
||||
// (MAX_SUBSTEPS − 1) × SIM_DT ≈ 66.7 ms (a 67–83 ms frame may drop up to one
|
||||
// SIM_DT of residual depending on accumulator phase; ≥ 83 ms always drops).
|
||||
// Pinned AS MEASURED, not retuned: every trace ever recorded was made under
|
||||
// this guard, and no real frame lives in the 67–83 ms gap on purpose. If the
|
||||
// guard is ever refined to `&& this._acc >= SIM_DT` (making 83 ms the true
|
||||
// bound), move SAFE_MAX with it in the same commit — this header is the
|
||||
// documented waiver ROADMAP asked for, THREADS [B] SPRINT16 has the numbers.
|
||||
test('determinism bound: frames under 66.7 ms hold the trace; a hitch past 83 ms drops time by design', () => {
|
||||
const SAFE_MAX = 0.066; // just under (MAX_SUBSTEPS − 1) × SIM_DT — see header
|
||||
const w1 = makeStubWind({ seed: 5, stormLen: 20 });
|
||||
const fixed = rig(HEIGHTS_HYPAR);
|
||||
for (let i = 0; i < Math.round(20 / SIM_DT); i++) fixed.step(SIM_DT, w1, i * SIM_DT);
|
||||
|
||||
// Half 1: ragged frames spanning the WHOLE legal range converge on the
|
||||
// fixed trace (the existing 4–24 ms test's promise, extended to the bound).
|
||||
const w2 = makeStubWind({ seed: 5, stormLen: 20 });
|
||||
const ragged = rig(HEIGHTS_HYPAR);
|
||||
const rand = rng(31);
|
||||
let acc = 0;
|
||||
while (acc < 19.5) {
|
||||
const dt = 0.004 + rand() * (SAFE_MAX - 0.004);
|
||||
ragged.step(dt, w2, acc);
|
||||
acc += dt;
|
||||
}
|
||||
// Compare at equal SIM clocks, not equal wall clocks: a random frame sum
|
||||
// overshoots the target by up to SAFE_MAX — up to 3 extra substeps of honest
|
||||
// evolution, an off-by-N comparison rather than a determinism failure. Top
|
||||
// up with fixed steps until both rigs have burned identical sim time.
|
||||
while (ragged.t < fixed.t - 1e-9) { ragged.step(SIM_DT, w2, acc); acc += SIM_DT; }
|
||||
assert(ragged.t === fixed.t, `sim clocks differ after top-up: ${ragged.t} vs ${fixed.t}`);
|
||||
for (let k = 0; k < 4; k++) {
|
||||
const d = Math.abs(fixed.corners[k].load - ragged.corners[k].load);
|
||||
assert(d < 1e-6, `corner ${k} drifted ${d.toFixed(6)} N on frames inside the lossless bound — MAX_SUBSTEPS shrank?`);
|
||||
}
|
||||
assert(acc - ragged.t < SIM_DT + 1e-9,
|
||||
`${((acc - ragged.t) * 1000).toFixed(1)} ms of wall time went missing on legal frames — the guard is dropping time early`);
|
||||
|
||||
// Half 2: one 120 ms hitch loses exactly the time past 5 substeps — the sim
|
||||
// shrugs, it does not catch up, and the clock says so. This is the bound
|
||||
// BEING a bound; if this half goes red the spiral guard is gone.
|
||||
const w3 = makeStubWind({ seed: 5, stormLen: 20 });
|
||||
const hitched = rig(HEIGHTS_HYPAR);
|
||||
const HITCH = 0.120, dropped = HITCH - 5 * SIM_DT; // ≈ 36.7 ms lost
|
||||
let fed = 0;
|
||||
hitched.step(HITCH, w3, 0); fed += HITCH;
|
||||
while (fed < 20) { hitched.step(SIM_DT, w3, fed); fed += SIM_DT; }
|
||||
const deficit = fed - hitched.t;
|
||||
assert(deficit > dropped - SIM_DT - 1e-9 && deficit < dropped + SIM_DT + 1e-9,
|
||||
`fed ${fed.toFixed(3)} s of wall time, sim clock reads ${hitched.t.toFixed(3)} — deficit ${deficit.toFixed(4)} s, ` +
|
||||
`expected ≈${dropped.toFixed(4)} s (one hitch's overflow). The spiral guard moved`);
|
||||
return `frames ≤${(SAFE_MAX * 1000).toFixed(0)} ms: byte-faithful, no time lost; one 120 ms hitch drops ${(deficit * 1000).toFixed(1)} ms as designed`;
|
||||
});
|
||||
|
||||
test('re-attach resets the clock: night 2 flies the same storm as night 1', () => {
|
||||
// SPRINT13. main.js builds ONE SailRig at boot and re-attach()es it every
|
||||
// night, and step() runs in EVERY phase once rigged — so the clock ran on
|
||||
|
||||
Loading…
Reference in New Issue
Block a user