FAILURE MAP
← Case archive

FA-87091 / Physics integrator stability / Open access

Leapfrog drift adds a Taylor acceleration term on top of the staggered velocity · case 01

Acceleration is counted twice per step and trajectories overshoot.

Verified by executionVariant 1 · 8 checks per implementationDownload source bundle ↓JSON ↗

ROOT CAUSE

The drift adds 0.5*a*dt^2 although vh already contains the half kick.

VERIFIED REPAIR

Drift is only x += vh*dt in staggered storage.

Unsuccessful approach: Adding half a kick to vh inside the drift double-counts the same term.

Case contract

solve(x0, v0, w2, g, dt, steps): acceleration -w2*x-g. The engine stores velocity at half steps: vh=v0+a(x0)*dt/2, then per step x+=vh*dt; vh+=a(x)*dt. Output synchronizes velocity back to the integer step: [x, vh-a(x)*dt/2] rounded to 6.

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(x0, v0, w2, g, dt, steps):
    def acc(p):
        return -w2 * p - g
    vh = v0 + acc(x0) * dt / 2
    x = x0
    for _ in range(steps):
        x = x + vh * dt + 0.5 * acc(x) * dt * dt
        vh = vh + acc(x) * dt
    v = vh - acc(x) * dt / 2
    return [round(x, 6), round(v, 6)]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[[[0.315, 1.535, 8.697, 9.81, 0.01, 3], [0.355353, 1.153028]], [[-1.521, -0.162, 6.527, 1.62, 0.2, 0], [-1.521, -0.162]], [[-1.964, 1.259, 20.124, 1.62, 0.01, 6], [-1.821517, 3.460132]], [[-1.43, -0.993, 22.057, 9.81, 0.2, 8], [-0.715348, 4.051605]], [[-1.752, 1.593, 27.377, -3.0, 0.1, 1], [-1.337877, 6.122579]], [[1.77, 1.16, 4.948, -3.0, 0.05, 0], [1.77, 1.16]], [[1.943, -1.206, 16.521, 9.81, 0.2, 0], [1.943, -1.206]], [[-0.165, 1.245, 27.29, 1.62, 0.01, 7], [-0.072755, 1.35995]]], [[[1.474, 0.119, 6.474, 0.0, 0.1, 4], [0.81097, -3.108625]], [[-1.229, 1.947, 11.183, 0.0, 0.2, 8], [-1.286518, -1.534185]], [[-0.279, 1.126, 22.611, 9.81, 0.01, 11], [-0.181377, 0.607648]], [[-0.296, 0.103, 20.419, 9.81, 0.05, 10], [-0.580675, -0.702686]], [[-1.933, 0.742, 17.091, 9.81, 0.1, 0], [-1.933, 0.742]], [[1.3, 1.873, 5.499, 0.0, 0.01, 4], [1.369102, 1.57925]], [[0.267, 1.417, 23.02, 9.81, 0.1, 0], [0.267, 1.417]], [[-0.524, 1.359, 8.765, 0.0, 0.05, 0], [-0.524, 1.359]]], [[[1.158, -1.304, 2.267, 0.0, 0.01, 6], [1.075141, -1.455976]], [[1.294, 0.75, 29.753, 0.0, 0.1, 10], [0.840908, 5.215563]], [[-1.849, 1.1, 5.352, 1.62, 0.2, 0], [-1.849, 1.1]], [[-1.735, -0.587, 6.94, 9.81, 0.01, 1], [-1.740758, -0.564491]], [[-1.162, 1.245, 26.726, 1.62, 0.1, 0], [-1.162, 1.245]], [[-1.071, 0.887, 9.026, 1.62, 0.01, 6], [-1.003614, 1.352791]], [[-0.611, -0.509, 8.029, 0.0, 0.1, 0], [-0.611, -0.509]], [[1.032, -0.358, 14.758, 9.81, 0.2, 3], [-1.946637, -3.958747]]], [[[1.44, 1.336, 22.594, 1.62, 0.01, 0], [1.44, 1.336]], [[-1.071, -1.25, 16.531, 0.0, 0.05, 9], [-0.021098, 4.507887]], [[-1.034, -1.918, 17.266, -3.0, 0.05, 0], [-1.034, -1.918]], [[0.836, 1.036, 17.688, 0.0, 0.01, 11], [0.858079, -0.641862]], [[-1.553, 1.413, 15.661, 9.81, 0.01, 11], [-1.315966, 2.827324]], [[0.607, 0.875, 20.434, 9.81, 0.1, 0], [0.607, 0.875]], [[0.614, 0.721, 8.281, 0.0, 0.05, 8], [0.479152, -1.316898]], [[1.342, 1.185, 16.929, 9.81, 0.01, 8], [1.331954, -1.433819]]], [[[-1.351, 1.779, 6.456, 0.0, 0.1, 10], [1.514677, 0.430776]], [[0.32, -1.935, 12.544, 0.0, 0.05, 0], [0.32, -1.935]], [[-0.026, -0.891, 12.663, -3.0, 0.2, 4], [0.431498, 1.067509]], [[-1.842, 1.748, 8.887, 9.81, 0.01, 2], [-1.805744, 1.876003]], [[-0.644, -1.525, 25.392, 9.81, 0.01, 0], [-0.644, -1.525]], [[-1.906, 1.981, 26.331, 0.0, 0.05, 19], [-0.716782, -9.203301]], [[1.283, -1.185, 20.785, 1.62, 0.1, 19], [-1.300286, -2.908473]], [[0.411, 0.376, 7.973, -3.0, 0.05, 17], [0.440574, -0.343716]]]]
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 fixtureActualExpectedOutcome
case 0[0.353455, 1.153274][0.355353, 1.153028]Failed
case 1[-1.521, -0.162][-1.521, -0.162]Passed
case 2[-1.8106, 3.453417][-1.821517, 3.460132]Failed
case 3[-0.364685, 0.265711][-0.715348, 4.051605]Failed
case 4[-1.083055, 5.773765][-1.337877, 6.122579]Failed
case 5[1.77, 1.16][1.77, 1.16]Passed
case 6[1.943, -1.206][1.943, -1.206]Passed
case 7[-0.072139, 1.359215][-0.072755, 1.35995]Failed

SHA-256 / 680fbdaaa4fcd2bf3584fbab91a6b1a5392b5d257b4f1462750aa6ca6ade53a5

2 / The unsuccessful fix

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(x0, v0, w2, g, dt, steps):
    def acc(p):
        return -w2 * p - g
    vh = v0 + acc(x0) * dt / 2
    x = x0
    for _ in range(steps):
        x = x + (vh + acc(x) * dt / 2) * dt
        vh = vh + acc(x) * dt
    v = vh - acc(x) * dt / 2
    return [round(x, 6), round(v, 6)]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[[[0.315, 1.535, 8.697, 9.81, 0.01, 3], [0.355353, 1.153028]], [[-1.521, -0.162, 6.527, 1.62, 0.2, 0], [-1.521, -0.162]], [[-1.964, 1.259, 20.124, 1.62, 0.01, 6], [-1.821517, 3.460132]], [[-1.43, -0.993, 22.057, 9.81, 0.2, 8], [-0.715348, 4.051605]], [[-1.752, 1.593, 27.377, -3.0, 0.1, 1], [-1.337877, 6.122579]], [[1.77, 1.16, 4.948, -3.0, 0.05, 0], [1.77, 1.16]], [[1.943, -1.206, 16.521, 9.81, 0.2, 0], [1.943, -1.206]], [[-0.165, 1.245, 27.29, 1.62, 0.01, 7], [-0.072755, 1.35995]]], [[[1.474, 0.119, 6.474, 0.0, 0.1, 4], [0.81097, -3.108625]], [[-1.229, 1.947, 11.183, 0.0, 0.2, 8], [-1.286518, -1.534185]], [[-0.279, 1.126, 22.611, 9.81, 0.01, 11], [-0.181377, 0.607648]], [[-0.296, 0.103, 20.419, 9.81, 0.05, 10], [-0.580675, -0.702686]], [[-1.933, 0.742, 17.091, 9.81, 0.1, 0], [-1.933, 0.742]], [[1.3, 1.873, 5.499, 0.0, 0.01, 4], [1.369102, 1.57925]], [[0.267, 1.417, 23.02, 9.81, 0.1, 0], [0.267, 1.417]], [[-0.524, 1.359, 8.765, 0.0, 0.05, 0], [-0.524, 1.359]]], [[[1.158, -1.304, 2.267, 0.0, 0.01, 6], [1.075141, -1.455976]], [[1.294, 0.75, 29.753, 0.0, 0.1, 10], [0.840908, 5.215563]], [[-1.849, 1.1, 5.352, 1.62, 0.2, 0], [-1.849, 1.1]], [[-1.735, -0.587, 6.94, 9.81, 0.01, 1], [-1.740758, -0.564491]], [[-1.162, 1.245, 26.726, 1.62, 0.1, 0], [-1.162, 1.245]], [[-1.071, 0.887, 9.026, 1.62, 0.01, 6], [-1.003614, 1.352791]], [[-0.611, -0.509, 8.029, 0.0, 0.1, 0], [-0.611, -0.509]], [[1.032, -0.358, 14.758, 9.81, 0.2, 3], [-1.946637, -3.958747]]], [[[1.44, 1.336, 22.594, 1.62, 0.01, 0], [1.44, 1.336]], [[-1.071, -1.25, 16.531, 0.0, 0.05, 9], [-0.021098, 4.507887]], [[-1.034, -1.918, 17.266, -3.0, 0.05, 0], [-1.034, -1.918]], [[0.836, 1.036, 17.688, 0.0, 0.01, 11], [0.858079, -0.641862]], [[-1.553, 1.413, 15.661, 9.81, 0.01, 11], [-1.315966, 2.827324]], [[0.607, 0.875, 20.434, 9.81, 0.1, 0], [0.607, 0.875]], [[0.614, 0.721, 8.281, 0.0, 0.05, 8], [0.479152, -1.316898]], [[1.342, 1.185, 16.929, 9.81, 0.01, 8], [1.331954, -1.433819]]], [[[-1.351, 1.779, 6.456, 0.0, 0.1, 10], [1.514677, 0.430776]], [[0.32, -1.935, 12.544, 0.0, 0.05, 0], [0.32, -1.935]], [[-0.026, -0.891, 12.663, -3.0, 0.2, 4], [0.431498, 1.067509]], [[-1.842, 1.748, 8.887, 9.81, 0.01, 2], [-1.805744, 1.876003]], [[-0.644, -1.525, 25.392, 9.81, 0.01, 0], [-0.644, -1.525]], [[-1.906, 1.981, 26.331, 0.0, 0.05, 19], [-0.716782, -9.203301]], [[1.283, -1.185, 20.785, 1.62, 0.1, 19], [-1.300286, -2.908473]], [[0.411, 0.376, 7.973, -3.0, 0.05, 17], [0.440574, -0.343716]]]]
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 fixtureActualExpectedOutcome
case 0[0.353455, 1.153274][0.355353, 1.153028]Failed
case 1[-1.521, -0.162][-1.521, -0.162]Passed
case 2[-1.8106, 3.453417][-1.821517, 3.460132]Failed
case 3[-0.364685, 0.265711][-0.715348, 4.051605]Failed
case 4[-1.083055, 5.773765][-1.337877, 6.122579]Failed
case 5[1.77, 1.16][1.77, 1.16]Passed
case 6[1.943, -1.206][1.943, -1.206]Passed
case 7[-0.072139, 1.359215][-0.072755, 1.35995]Failed

SHA-256 / 28ea36b907794fe9c1e53c8a4ce025131813e4962adab274d72b8fce2e1ebe47

3 / The verified repair

Exit 0
"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(x0, v0, w2, g, dt, steps):
    def acc(p):
        return -w2 * p - g
    vh = v0 + acc(x0) * dt / 2
    x = x0
    for _ in range(steps):
        x = x + vh * dt
        vh = vh + acc(x) * dt
    v = vh - acc(x) * dt / 2
    return [round(x, 6), round(v, 6)]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[[[0.315, 1.535, 8.697, 9.81, 0.01, 3], [0.355353, 1.153028]], [[-1.521, -0.162, 6.527, 1.62, 0.2, 0], [-1.521, -0.162]], [[-1.964, 1.259, 20.124, 1.62, 0.01, 6], [-1.821517, 3.460132]], [[-1.43, -0.993, 22.057, 9.81, 0.2, 8], [-0.715348, 4.051605]], [[-1.752, 1.593, 27.377, -3.0, 0.1, 1], [-1.337877, 6.122579]], [[1.77, 1.16, 4.948, -3.0, 0.05, 0], [1.77, 1.16]], [[1.943, -1.206, 16.521, 9.81, 0.2, 0], [1.943, -1.206]], [[-0.165, 1.245, 27.29, 1.62, 0.01, 7], [-0.072755, 1.35995]]], [[[1.474, 0.119, 6.474, 0.0, 0.1, 4], [0.81097, -3.108625]], [[-1.229, 1.947, 11.183, 0.0, 0.2, 8], [-1.286518, -1.534185]], [[-0.279, 1.126, 22.611, 9.81, 0.01, 11], [-0.181377, 0.607648]], [[-0.296, 0.103, 20.419, 9.81, 0.05, 10], [-0.580675, -0.702686]], [[-1.933, 0.742, 17.091, 9.81, 0.1, 0], [-1.933, 0.742]], [[1.3, 1.873, 5.499, 0.0, 0.01, 4], [1.369102, 1.57925]], [[0.267, 1.417, 23.02, 9.81, 0.1, 0], [0.267, 1.417]], [[-0.524, 1.359, 8.765, 0.0, 0.05, 0], [-0.524, 1.359]]], [[[1.158, -1.304, 2.267, 0.0, 0.01, 6], [1.075141, -1.455976]], [[1.294, 0.75, 29.753, 0.0, 0.1, 10], [0.840908, 5.215563]], [[-1.849, 1.1, 5.352, 1.62, 0.2, 0], [-1.849, 1.1]], [[-1.735, -0.587, 6.94, 9.81, 0.01, 1], [-1.740758, -0.564491]], [[-1.162, 1.245, 26.726, 1.62, 0.1, 0], [-1.162, 1.245]], [[-1.071, 0.887, 9.026, 1.62, 0.01, 6], [-1.003614, 1.352791]], [[-0.611, -0.509, 8.029, 0.0, 0.1, 0], [-0.611, -0.509]], [[1.032, -0.358, 14.758, 9.81, 0.2, 3], [-1.946637, -3.958747]]], [[[1.44, 1.336, 22.594, 1.62, 0.01, 0], [1.44, 1.336]], [[-1.071, -1.25, 16.531, 0.0, 0.05, 9], [-0.021098, 4.507887]], [[-1.034, -1.918, 17.266, -3.0, 0.05, 0], [-1.034, -1.918]], [[0.836, 1.036, 17.688, 0.0, 0.01, 11], [0.858079, -0.641862]], [[-1.553, 1.413, 15.661, 9.81, 0.01, 11], [-1.315966, 2.827324]], [[0.607, 0.875, 20.434, 9.81, 0.1, 0], [0.607, 0.875]], [[0.614, 0.721, 8.281, 0.0, 0.05, 8], [0.479152, -1.316898]], [[1.342, 1.185, 16.929, 9.81, 0.01, 8], [1.331954, -1.433819]]], [[[-1.351, 1.779, 6.456, 0.0, 0.1, 10], [1.514677, 0.430776]], [[0.32, -1.935, 12.544, 0.0, 0.05, 0], [0.32, -1.935]], [[-0.026, -0.891, 12.663, -3.0, 0.2, 4], [0.431498, 1.067509]], [[-1.842, 1.748, 8.887, 9.81, 0.01, 2], [-1.805744, 1.876003]], [[-0.644, -1.525, 25.392, 9.81, 0.01, 0], [-0.644, -1.525]], [[-1.906, 1.981, 26.331, 0.0, 0.05, 19], [-0.716782, -9.203301]], [[1.283, -1.185, 20.785, 1.62, 0.1, 19], [-1.300286, -2.908473]], [[0.411, 0.376, 7.973, -3.0, 0.05, 17], [0.440574, -0.343716]]]]
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 fixtureActualExpectedOutcome
case 0[0.355353, 1.153028][0.355353, 1.153028]Passed
case 1[-1.521, -0.162][-1.521, -0.162]Passed
case 2[-1.821517, 3.460132][-1.821517, 3.460132]Passed
case 3[-0.715348, 4.051605][-0.715348, 4.051605]Passed
case 4[-1.337877, 6.122579][-1.337877, 6.122579]Passed
case 5[1.77, 1.16][1.77, 1.16]Passed
case 6[1.943, -1.206][1.943, -1.206]Passed
case 7[-0.072755, 1.35995][-0.072755, 1.35995]Passed

SHA-256 / 952cfee95518c9b54aebb9ef72e4695a6d19b158a51a71a0373e09d0ee95503f

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:55.628401+00:00.

Case digest / 4141544b8cece60046daccc86d7d244c080115749564af2e677df578e5e7fb18