FA-86981 / Physics integrator stability / Open access
Leftover whole steps are carried into the next frame after the step budget · case 01
After a long frame the accumulator keeps backlog and every later frame runs at the budget limit.
ROOT CAUSE
The guard that drops whole remaining steps after max_steps is missing.
VERIFIED REPAIR
Drop whole multiples of dt beyond the budget and keep only the fractional remainder.
Unsuccessful approach: Dropping the fractional remainder too makes the interpolation alpha jump to zero.
Case contract
solve(frames, dt, max_frame, max_steps): game loop accumulator. Each frame delta is clamped to max_frame and added; run steps while acc >= dt up to max_steps; if a full dt still remains, whole multiples of dt are dropped (spiral-of-death guard). Return [steps per frame, acc/dt after last frame, dropped time].
Why this case matters
Game and robotics physics loops depend on integrator update order, step control and stabilization terms; a wrong decision point turns a stable simulation into drifting or exploding motion.
1 / The failure
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(frames, dt, max_frame, max_steps):
acc = 0.0
dropped = 0.0
steps = []
for delta in frames:
acc += min(delta, max_frame)
n = 0
while acc >= dt and n < max_steps:
acc -= dt
n += 1
steps.append(n)
return [steps, acc / dt, dropped]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[[[[0.125, 0.046875, 0.625, 0.03125], 0.03125, 0.25, 1], [[1, 1, 1, 1], 0.5, 0.3125]], [[[0.0], 0.015625, 0.25, 2], [[0], 0.0, 0.0]], [[[0.046875, 0.125, 0.0625, 0.0, 0.03125, 0.046875], 0.03125, 0.25, 5], [[1, 4, 2, 0, 1, 2], 0.0, 0.0]], [[[0.046875, 0.0, 0.25, 0.015625], 0.015625, 0.125, 4], [[3, 0, 4, 1], 0.0, 0.0625]], [[[0.625, 0.015625], 0.015625, 0.25, 2], [[2, 1], 0.0, 0.21875]], [[[0.25, 0.078125, 0.0625, 0.25, 0.125], 0.0625, 0.25, 1], [[1, 1, 1, 1, 1], 0.25, 0.4375]], [[[0.015625], 0.015625, 0.25, 2], [[1], 0.0, 0.0]], [[[0.0, 0.125, 0.0625], 0.03125, 0.125, 2], [[0, 2, 2], 0.0, 0.0625]]], [[[[0.015625, 0.25, 0.25], 0.03125, 0.125, 3], [[0, 3, 3], 0.5, 0.0625]], [[[0.015625], 0.015625, 0.25, 2], [[1], 0.0, 0.0]], [[[0.0625, 0.125, 0.015625, 0.125], 0.03125, 0.25, 2], [[2, 2, 0, 2], 0.5, 0.125]], [[[0.015625, 0.625, 0.0625, 0.0625, 0.03125], 0.0625, 0.25, 3], [[0, 3, 1, 1, 0], 0.75, 0.0625]], [[[0.046875, 0.625, 0.046875], 0.015625, 0.25, 4], [[3, 4, 3], 0.0, 0.1875]], [[[0.0], 0.015625, 0.25, 2], [[0], 0.0, 0.0]], [[[0.0625, 0.625], 0.015625, 0.125, 1], [[1, 1], 0.0, 0.15625]], [[[0.0625, 0.0, 0.078125, 0.0625], 0.03125, 0.25, 3], [[2, 0, 2, 2], 0.5, 0.0]]], [[[[0.125], 0.0625, 0.125, 5], [[2], 0.0, 0.0]], [[[0.078125, 0.625, 0.125, 0.25, 0.0, 0.25], 0.015625, 0.25, 5], [[5, 5, 5, 5, 0, 5], 0.0, 0.5625]], [[[0.015625], 0.015625, 0.25, 2], [[1], 0.0, 0.0]], [[[0.0625, 0.046875, 0.078125, 0.078125, 0.625], 0.03125, 0.25, 5], [[2, 1, 3, 2, 5], 0.5, 0.09375]], [[[0.125, 0.625], 0.015625, 0.25, 5], [[5, 5], 0.0, 0.21875]], [[[0.078125, 0.046875, 0.25], 0.015625, 0.125, 5], [[5, 3, 5], 0.0, 0.046875]], [[[0.046875, 0.078125, 0.078125, 0.0, 0.25], 0.03125, 0.25, 5], [[1, 3, 2, 0, 5], 0.5, 0.09375]], [[[0.0], 0.015625, 0.25, 2], [[0], 0.0, 0.0]]], [[[[0.25, 0.625, 0.03125], 0.0625, 0.125, 3], [[2, 2, 0], 0.5, 0.0]], [[[0.046875, 0.0, 0.0625, 0.125, 0.125, 0.078125], 0.03125, 0.25, 3], [[1, 0, 2, 3, 3, 3], 0.0, 0.0625]], [[[0.015625], 0.015625, 0.25, 5], [[1], 0.0, 0.0]], [[[0.0], 0.015625, 0.25, 2], [[0], 0.0, 0.0]], [[[0.078125, 0.015625, 0.03125, 0.25, 0.0], 0.015625, 0.125, 3], [[3, 1, 2, 3, 0], 0.0, 0.109375]], [[[0.0, 0.078125, 0.078125], 0.015625, 0.125, 2], [[0, 2, 2], 0.0, 0.09375]], [[[0.015625], 0.015625, 0.25, 2], [[1], 0.0, 0.0]], [[[0.125, 0.625, 0.046875, 0.0625, 0.0, 0.625], 0.0625, 0.25, 2], [[2, 2, 0, 1, 0, 2], 0.75, 0.25]]], [[[[0.25], 0.03125, 0.125, 3], [[3], 0.0, 0.03125]], [[[0.0625, 0.0, 0.078125, 0.046875, 0.046875], 0.015625, 0.25, 1], [[1, 0, 1, 1, 1], 0.0, 0.171875]], [[[0.0], 0.015625, 0.25, 2], [[0], 0.0, 0.0]], [[[0.015625], 0.015625, 0.25, 2], [[1], 0.0, 0.0]], [[[0.078125], 0.0625, 0.25, 3], [[1], 0.25, 0.0]], [[[0.046875, 0.125, 0.078125], 0.03125, 0.125, 2], [[1, 2, 2], 0.0, 0.09375]], [[[0.0625, 0.25, 0.015625, 0.625], 0.0625, 0.25, 3], [[1, 3, 0, 3], 0.25, 0.125]], [[[0.078125, 0.015625], 0.015625, 0.125, 5], [[5, 1], 0.0, 0.0]]]]
for i, (args, expected) in enumerate(fixtures[N-1]):
check("case %d" % i, solve(*args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
| Boundary fixture | Actual | Expected | Outcome |
|---|---|---|---|
| case 0 | [[1, 1, 1, 1], 10.5, 0.0] | [[1, 1, 1, 1], 0.5, 0.3125] | Failed |
| case 1 | [[0], 0.0, 0.0] | [[0], 0.0, 0.0] | Passed |
| case 2 | [[1, 4, 2, 0, 1, 2], 0.0, 0.0] | [[1, 4, 2, 0, 1, 2], 0.0, 0.0] | Passed |
| case 3 | [[3, 0, 4, 4], 1.0, 0.0] | [[3, 0, 4, 1], 0.0, 0.0625] | Failed |
| case 4 | [[2, 2], 13.0, 0.0] | [[2, 1], 0.0, 0.21875] | Failed |
| case 5 | [[1, 1, 1, 1, 1], 7.25, 0.0] | [[1, 1, 1, 1, 1], 0.25, 0.4375] | Failed |
| case 6 | [[1], 0.0, 0.0] | [[1], 0.0, 0.0] | Passed |
| case 7 | [[0, 2, 2], 2.0, 0.0] | [[0, 2, 2], 0.0, 0.0625] | Failed |
SHA-256 / d0805986c695549f837cd368ccfea15982cdef89b127ee5a9e960929b9437828
2 / The unsuccessful fix
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(frames, dt, max_frame, max_steps):
acc = 0.0
dropped = 0.0
steps = []
for delta in frames:
acc += min(delta, max_frame)
n = 0
while acc >= dt and n < max_steps:
acc -= dt
n += 1
if acc >= dt:
keep = 0.0
dropped += acc - keep
acc = keep
steps.append(n)
return [steps, acc / dt, dropped]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[[[[0.125, 0.046875, 0.625, 0.03125], 0.03125, 0.25, 1], [[1, 1, 1, 1], 0.5, 0.3125]], [[[0.0], 0.015625, 0.25, 2], [[0], 0.0, 0.0]], [[[0.046875, 0.125, 0.0625, 0.0, 0.03125, 0.046875], 0.03125, 0.25, 5], [[1, 4, 2, 0, 1, 2], 0.0, 0.0]], [[[0.046875, 0.0, 0.25, 0.015625], 0.015625, 0.125, 4], [[3, 0, 4, 1], 0.0, 0.0625]], [[[0.625, 0.015625], 0.015625, 0.25, 2], [[2, 1], 0.0, 0.21875]], [[[0.25, 0.078125, 0.0625, 0.25, 0.125], 0.0625, 0.25, 1], [[1, 1, 1, 1, 1], 0.25, 0.4375]], [[[0.015625], 0.015625, 0.25, 2], [[1], 0.0, 0.0]], [[[0.0, 0.125, 0.0625], 0.03125, 0.125, 2], [[0, 2, 2], 0.0, 0.0625]]], [[[[0.015625, 0.25, 0.25], 0.03125, 0.125, 3], [[0, 3, 3], 0.5, 0.0625]], [[[0.015625], 0.015625, 0.25, 2], [[1], 0.0, 0.0]], [[[0.0625, 0.125, 0.015625, 0.125], 0.03125, 0.25, 2], [[2, 2, 0, 2], 0.5, 0.125]], [[[0.015625, 0.625, 0.0625, 0.0625, 0.03125], 0.0625, 0.25, 3], [[0, 3, 1, 1, 0], 0.75, 0.0625]], [[[0.046875, 0.625, 0.046875], 0.015625, 0.25, 4], [[3, 4, 3], 0.0, 0.1875]], [[[0.0], 0.015625, 0.25, 2], [[0], 0.0, 0.0]], [[[0.0625, 0.625], 0.015625, 0.125, 1], [[1, 1], 0.0, 0.15625]], [[[0.0625, 0.0, 0.078125, 0.0625], 0.03125, 0.25, 3], [[2, 0, 2, 2], 0.5, 0.0]]], [[[[0.125], 0.0625, 0.125, 5], [[2], 0.0, 0.0]], [[[0.078125, 0.625, 0.125, 0.25, 0.0, 0.25], 0.015625, 0.25, 5], [[5, 5, 5, 5, 0, 5], 0.0, 0.5625]], [[[0.015625], 0.015625, 0.25, 2], [[1], 0.0, 0.0]], [[[0.0625, 0.046875, 0.078125, 0.078125, 0.625], 0.03125, 0.25, 5], [[2, 1, 3, 2, 5], 0.5, 0.09375]], [[[0.125, 0.625], 0.015625, 0.25, 5], [[5, 5], 0.0, 0.21875]], [[[0.078125, 0.046875, 0.25], 0.015625, 0.125, 5], [[5, 3, 5], 0.0, 0.046875]], [[[0.046875, 0.078125, 0.078125, 0.0, 0.25], 0.03125, 0.25, 5], [[1, 3, 2, 0, 5], 0.5, 0.09375]], [[[0.0], 0.015625, 0.25, 2], [[0], 0.0, 0.0]]], [[[[0.25, 0.625, 0.03125], 0.0625, 0.125, 3], [[2, 2, 0], 0.5, 0.0]], [[[0.046875, 0.0, 0.0625, 0.125, 0.125, 0.078125], 0.03125, 0.25, 3], [[1, 0, 2, 3, 3, 3], 0.0, 0.0625]], [[[0.015625], 0.015625, 0.25, 5], [[1], 0.0, 0.0]], [[[0.0], 0.015625, 0.25, 2], [[0], 0.0, 0.0]], [[[0.078125, 0.015625, 0.03125, 0.25, 0.0], 0.015625, 0.125, 3], [[3, 1, 2, 3, 0], 0.0, 0.109375]], [[[0.0, 0.078125, 0.078125], 0.015625, 0.125, 2], [[0, 2, 2], 0.0, 0.09375]], [[[0.015625], 0.015625, 0.25, 2], [[1], 0.0, 0.0]], [[[0.125, 0.625, 0.046875, 0.0625, 0.0, 0.625], 0.0625, 0.25, 2], [[2, 2, 0, 1, 0, 2], 0.75, 0.25]]], [[[[0.25], 0.03125, 0.125, 3], [[3], 0.0, 0.03125]], [[[0.0625, 0.0, 0.078125, 0.046875, 0.046875], 0.015625, 0.25, 1], [[1, 0, 1, 1, 1], 0.0, 0.171875]], [[[0.0], 0.015625, 0.25, 2], [[0], 0.0, 0.0]], [[[0.015625], 0.015625, 0.25, 2], [[1], 0.0, 0.0]], [[[0.078125], 0.0625, 0.25, 3], [[1], 0.25, 0.0]], [[[0.046875, 0.125, 0.078125], 0.03125, 0.125, 2], [[1, 2, 2], 0.0, 0.09375]], [[[0.0625, 0.25, 0.015625, 0.625], 0.0625, 0.25, 3], [[1, 3, 0, 3], 0.25, 0.125]], [[[0.078125, 0.015625], 0.015625, 0.125, 5], [[5, 1], 0.0, 0.0]]]]
for i, (args, expected) in enumerate(fixtures[N-1]):
check("case %d" % i, solve(*args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
| Boundary fixture | Actual | Expected | Outcome |
|---|---|---|---|
| case 0 | [[1, 1, 1, 1], 0.0, 0.328125] | [[1, 1, 1, 1], 0.5, 0.3125] | Failed |
| case 1 | [[0], 0.0, 0.0] | [[0], 0.0, 0.0] | Passed |
| case 2 | [[1, 4, 2, 0, 1, 2], 0.0, 0.0] | [[1, 4, 2, 0, 1, 2], 0.0, 0.0] | Passed |
| case 3 | [[3, 0, 4, 1], 0.0, 0.0625] | [[3, 0, 4, 1], 0.0, 0.0625] | Passed |
| case 4 | [[2, 1], 0.0, 0.21875] | [[2, 1], 0.0, 0.21875] | Passed |
| case 5 | [[1, 1, 1, 1, 1], 0.0, 0.453125] | [[1, 1, 1, 1, 1], 0.25, 0.4375] | Failed |
| case 6 | [[1], 0.0, 0.0] | [[1], 0.0, 0.0] | Passed |
| case 7 | [[0, 2, 2], 0.0, 0.0625] | [[0, 2, 2], 0.0, 0.0625] | Passed |
SHA-256 / 841a9229e29d38cb576dae1e37d489e8b9996396bc7912ddcaf4af9b65c797ab
3 / The verified repair
Exit 0"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(frames, dt, max_frame, max_steps):
acc = 0.0
dropped = 0.0
steps = []
for delta in frames:
acc += min(delta, max_frame)
n = 0
while acc >= dt and n < max_steps:
acc -= dt
n += 1
if acc >= dt:
keep = acc % dt
dropped += acc - keep
acc = keep
steps.append(n)
return [steps, acc / dt, dropped]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[[[[0.125, 0.046875, 0.625, 0.03125], 0.03125, 0.25, 1], [[1, 1, 1, 1], 0.5, 0.3125]], [[[0.0], 0.015625, 0.25, 2], [[0], 0.0, 0.0]], [[[0.046875, 0.125, 0.0625, 0.0, 0.03125, 0.046875], 0.03125, 0.25, 5], [[1, 4, 2, 0, 1, 2], 0.0, 0.0]], [[[0.046875, 0.0, 0.25, 0.015625], 0.015625, 0.125, 4], [[3, 0, 4, 1], 0.0, 0.0625]], [[[0.625, 0.015625], 0.015625, 0.25, 2], [[2, 1], 0.0, 0.21875]], [[[0.25, 0.078125, 0.0625, 0.25, 0.125], 0.0625, 0.25, 1], [[1, 1, 1, 1, 1], 0.25, 0.4375]], [[[0.015625], 0.015625, 0.25, 2], [[1], 0.0, 0.0]], [[[0.0, 0.125, 0.0625], 0.03125, 0.125, 2], [[0, 2, 2], 0.0, 0.0625]]], [[[[0.015625, 0.25, 0.25], 0.03125, 0.125, 3], [[0, 3, 3], 0.5, 0.0625]], [[[0.015625], 0.015625, 0.25, 2], [[1], 0.0, 0.0]], [[[0.0625, 0.125, 0.015625, 0.125], 0.03125, 0.25, 2], [[2, 2, 0, 2], 0.5, 0.125]], [[[0.015625, 0.625, 0.0625, 0.0625, 0.03125], 0.0625, 0.25, 3], [[0, 3, 1, 1, 0], 0.75, 0.0625]], [[[0.046875, 0.625, 0.046875], 0.015625, 0.25, 4], [[3, 4, 3], 0.0, 0.1875]], [[[0.0], 0.015625, 0.25, 2], [[0], 0.0, 0.0]], [[[0.0625, 0.625], 0.015625, 0.125, 1], [[1, 1], 0.0, 0.15625]], [[[0.0625, 0.0, 0.078125, 0.0625], 0.03125, 0.25, 3], [[2, 0, 2, 2], 0.5, 0.0]]], [[[[0.125], 0.0625, 0.125, 5], [[2], 0.0, 0.0]], [[[0.078125, 0.625, 0.125, 0.25, 0.0, 0.25], 0.015625, 0.25, 5], [[5, 5, 5, 5, 0, 5], 0.0, 0.5625]], [[[0.015625], 0.015625, 0.25, 2], [[1], 0.0, 0.0]], [[[0.0625, 0.046875, 0.078125, 0.078125, 0.625], 0.03125, 0.25, 5], [[2, 1, 3, 2, 5], 0.5, 0.09375]], [[[0.125, 0.625], 0.015625, 0.25, 5], [[5, 5], 0.0, 0.21875]], [[[0.078125, 0.046875, 0.25], 0.015625, 0.125, 5], [[5, 3, 5], 0.0, 0.046875]], [[[0.046875, 0.078125, 0.078125, 0.0, 0.25], 0.03125, 0.25, 5], [[1, 3, 2, 0, 5], 0.5, 0.09375]], [[[0.0], 0.015625, 0.25, 2], [[0], 0.0, 0.0]]], [[[[0.25, 0.625, 0.03125], 0.0625, 0.125, 3], [[2, 2, 0], 0.5, 0.0]], [[[0.046875, 0.0, 0.0625, 0.125, 0.125, 0.078125], 0.03125, 0.25, 3], [[1, 0, 2, 3, 3, 3], 0.0, 0.0625]], [[[0.015625], 0.015625, 0.25, 5], [[1], 0.0, 0.0]], [[[0.0], 0.015625, 0.25, 2], [[0], 0.0, 0.0]], [[[0.078125, 0.015625, 0.03125, 0.25, 0.0], 0.015625, 0.125, 3], [[3, 1, 2, 3, 0], 0.0, 0.109375]], [[[0.0, 0.078125, 0.078125], 0.015625, 0.125, 2], [[0, 2, 2], 0.0, 0.09375]], [[[0.015625], 0.015625, 0.25, 2], [[1], 0.0, 0.0]], [[[0.125, 0.625, 0.046875, 0.0625, 0.0, 0.625], 0.0625, 0.25, 2], [[2, 2, 0, 1, 0, 2], 0.75, 0.25]]], [[[[0.25], 0.03125, 0.125, 3], [[3], 0.0, 0.03125]], [[[0.0625, 0.0, 0.078125, 0.046875, 0.046875], 0.015625, 0.25, 1], [[1, 0, 1, 1, 1], 0.0, 0.171875]], [[[0.0], 0.015625, 0.25, 2], [[0], 0.0, 0.0]], [[[0.015625], 0.015625, 0.25, 2], [[1], 0.0, 0.0]], [[[0.078125], 0.0625, 0.25, 3], [[1], 0.25, 0.0]], [[[0.046875, 0.125, 0.078125], 0.03125, 0.125, 2], [[1, 2, 2], 0.0, 0.09375]], [[[0.0625, 0.25, 0.015625, 0.625], 0.0625, 0.25, 3], [[1, 3, 0, 3], 0.25, 0.125]], [[[0.078125, 0.015625], 0.015625, 0.125, 5], [[5, 1], 0.0, 0.0]]]]
for i, (args, expected) in enumerate(fixtures[N-1]):
check("case %d" % i, solve(*args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
| Boundary fixture | Actual | Expected | Outcome |
|---|---|---|---|
| case 0 | [[1, 1, 1, 1], 0.5, 0.3125] | [[1, 1, 1, 1], 0.5, 0.3125] | Passed |
| case 1 | [[0], 0.0, 0.0] | [[0], 0.0, 0.0] | Passed |
| case 2 | [[1, 4, 2, 0, 1, 2], 0.0, 0.0] | [[1, 4, 2, 0, 1, 2], 0.0, 0.0] | Passed |
| case 3 | [[3, 0, 4, 1], 0.0, 0.0625] | [[3, 0, 4, 1], 0.0, 0.0625] | Passed |
| case 4 | [[2, 1], 0.0, 0.21875] | [[2, 1], 0.0, 0.21875] | Passed |
| case 5 | [[1, 1, 1, 1, 1], 0.25, 0.4375] | [[1, 1, 1, 1, 1], 0.25, 0.4375] | Passed |
| case 6 | [[1], 0.0, 0.0] | [[1], 0.0, 0.0] | Passed |
| case 7 | [[0, 2, 2], 0.0, 0.0625] | [[0, 2, 2], 0.0, 0.0625] | Passed |
SHA-256 / 753787e96fcd30e30d780cb6d2d817912f58b0846fed8c6de34838f73980cfa9
Verification & scope
A deterministic bounded teaching model with stipulated toy conventions and rounded float output; not a production physics engine or a proof of numerical stability. This reproducer isolates one failure mechanism. Results cover the supplied fixtures. Variants within a family share a test contract and should remain grouped when constructing evaluation splits. Related mechanisms with a shared evaluation_group must also remain together; these controlled models are not independent production incidents.
Observations recorded using Python 3.12.14 at 2026-09-29T14:50:54.621050+00:00.
Case digest / be255c739e75d3409013b52347f3b82387a74e3afb048a4b4f3f51bd3379186a