FAILURE MAP
← Case archive

FA-87081 / Physics integrator stability / Open access

Leapfrog kicks with the old position before drifting · case 01

The stored velocity lags a full step behind and the scheme loses time symmetry.

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

ROOT CAUSE

The loop updates vh from a(x) before moving x.

VERIFIED REPAIR

Drift with vh first, then kick with the acceleration at the new position.

Unsuccessful approach: Kicking with the saved pre-drift position after drifting still uses stale force.

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):
        vh = vh + acc(x) * dt
        x = x + vh * 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 = [[[[-1.401, -1.525, 6.432, 9.81, 0.1, 0], [-1.401, -1.525]], [[-1.521, -0.162, 6.527, 1.62, 0.2, 0], [-1.521, -0.162]], [[-1.436, 0.032, 5.193, 0.0, 0.01, 8], [-1.409656, 0.624717]], [[1.118, 0.885, 9.249, -3.0, 0.05, 0], [1.118, 0.885]], [[0.919, -1.08, 12.114, 1.62, 0.2, 6], [-0.289748, 3.564566]], [[1.046, -0.242, 4.156, 9.81, 0.05, 2], [0.951324, -1.641689]], [[-0.493, -1.517, 17.061, 0.0, 0.01, 7], [-0.577279, -0.8735]], [[-1.458, -1.788, 3.071, -3.0, 0.01, 6], [-1.55164, -1.330312]]], [[[-0.398, 0.635, 6.904, 1.62, 0.2, 2], [-0.095078, 0.670473]], [[0.267, 1.417, 23.02, 9.81, 0.1, 0], [0.267, 1.417]], [[0.993, -0.368, 11.989, 1.62, 0.05, 0], [0.993, -0.368]], [[1.3, 1.873, 5.499, 0.0, 0.01, 4], [1.369102, 1.57925]], [[0.51, 0.5, 18.606, 9.81, 0.1, 8], [-1.54552, 0.97061]], [[-0.524, 1.359, 8.765, 0.0, 0.05, 0], [-0.524, 1.359]], [[-1.057, 0.244, 12.747, 9.81, 0.01, 8], [-1.026095, 0.5232]], [[-0.784, -0.844, 28.335, 0.0, 0.01, 5], [-0.798112, 0.283127]]], [[[1.097, 0.569, 19.714, 0.0, 0.1, 0], [1.097, 0.569]], [[-0.531, 0.372, 6.858, 0.0, 0.05, 12], [0.143203, 1.386993]], [[1.147, -0.931, 6.394, 9.81, 0.05, 0], [1.147, -0.931]], [[-1.282, 0.842, 22.906, 0.0, 0.05, 3], [-0.847335, 4.648228]], [[0.444, -1.128, 10.972, 1.62, 0.05, 0], [0.444, -1.128]], [[1.781, 1.929, 19.092, -3.0, 0.01, 4], [1.833049, 0.665995]], [[1.294, 0.75, 29.753, 0.0, 0.1, 10], [0.840908, 5.215563]], [[0.351, 1.569, 26.852, 0.0, 0.2, 1], [0.476299, -0.652463]]], [[[-0.524, 0.032, 18.679, 1.62, 0.01, 12], [-0.462818, 0.964422]], [[-1.034, -1.918, 17.266, -3.0, 0.05, 0], [-1.034, -1.918]], [[1.44, 1.336, 22.594, 1.62, 0.01, 0], [1.44, 1.336]], [[-1.903, 0.319, 4.566, 9.81, 0.2, 15], [-1.879918, 0.223735]], [[-1.237, -1.128, 5.405, 0.0, 0.1, 4], [-1.129674, 1.62072]], [[0.607, 0.875, 20.434, 9.81, 0.1, 0], [0.607, 0.875]], [[-1.506, -0.728, 20.263, 0.0, 0.2, 8], [-0.730988, 5.34242]], [[0.287, -0.804, 3.405, 1.62, 0.1, 16], [-1.306007, 0.532531]]], [[[1.537, -0.061, 3.471, 9.81, 0.01, 14], [1.380971, -2.155206]], [[1.283, -1.185, 20.785, 1.62, 0.1, 19], [-1.300286, -2.908473]], [[-1.906, 1.981, 26.331, 0.0, 0.05, 19], [-0.716782, -9.203301]], [[-0.644, -1.525, 25.392, 9.81, 0.01, 0], [-0.644, -1.525]], [[-0.042, -1.542, 14.783, 0.0, 0.05, 2], [-0.190275, -1.368698]], [[0.95, -1.335, 21.477, 1.62, 0.05, 13], [-1.128613, 0.748224]], [[-1.842, 1.748, 8.887, 9.81, 0.01, 2], [-1.805744, 1.876003]], [[0.32, -1.935, 12.544, 0.0, 0.05, 0], [0.32, -1.935]]]]
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[-1.401, -1.525][-1.401, -1.525]Passed
case 1[-1.521, -0.162][-1.521, -0.162]Passed
case 2[-1.403723, 0.625157][-1.409656, 0.624717]Failed
case 3[1.118, 0.885][1.118, 0.885]Passed
case 4[0.415307, 5.996094][-0.289748, 3.564566]Failed
case 5[0.880906, -1.661325][0.951324, -1.641689]Failed
case 6[-0.571472, -0.89038][-0.577279, -0.8735]Failed
case 7[-1.547162, -1.333463][-1.55164, -1.330312]Failed

SHA-256 / e7aabfcc405b628124c965c13d2c91f9980437b43c1420c55400b4081fa44c31

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):
        xo = x
        x = x + vh * dt
        vh = vh + acc(xo) * 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 = [[[[-1.401, -1.525, 6.432, 9.81, 0.1, 0], [-1.401, -1.525]], [[-1.521, -0.162, 6.527, 1.62, 0.2, 0], [-1.521, -0.162]], [[-1.436, 0.032, 5.193, 0.0, 0.01, 8], [-1.409656, 0.624717]], [[1.118, 0.885, 9.249, -3.0, 0.05, 0], [1.118, 0.885]], [[0.919, -1.08, 12.114, 1.62, 0.2, 6], [-0.289748, 3.564566]], [[1.046, -0.242, 4.156, 9.81, 0.05, 2], [0.951324, -1.641689]], [[-0.493, -1.517, 17.061, 0.0, 0.01, 7], [-0.577279, -0.8735]], [[-1.458, -1.788, 3.071, -3.0, 0.01, 6], [-1.55164, -1.330312]]], [[[-0.398, 0.635, 6.904, 1.62, 0.2, 2], [-0.095078, 0.670473]], [[0.267, 1.417, 23.02, 9.81, 0.1, 0], [0.267, 1.417]], [[0.993, -0.368, 11.989, 1.62, 0.05, 0], [0.993, -0.368]], [[1.3, 1.873, 5.499, 0.0, 0.01, 4], [1.369102, 1.57925]], [[0.51, 0.5, 18.606, 9.81, 0.1, 8], [-1.54552, 0.97061]], [[-0.524, 1.359, 8.765, 0.0, 0.05, 0], [-0.524, 1.359]], [[-1.057, 0.244, 12.747, 9.81, 0.01, 8], [-1.026095, 0.5232]], [[-0.784, -0.844, 28.335, 0.0, 0.01, 5], [-0.798112, 0.283127]]], [[[1.097, 0.569, 19.714, 0.0, 0.1, 0], [1.097, 0.569]], [[-0.531, 0.372, 6.858, 0.0, 0.05, 12], [0.143203, 1.386993]], [[1.147, -0.931, 6.394, 9.81, 0.05, 0], [1.147, -0.931]], [[-1.282, 0.842, 22.906, 0.0, 0.05, 3], [-0.847335, 4.648228]], [[0.444, -1.128, 10.972, 1.62, 0.05, 0], [0.444, -1.128]], [[1.781, 1.929, 19.092, -3.0, 0.01, 4], [1.833049, 0.665995]], [[1.294, 0.75, 29.753, 0.0, 0.1, 10], [0.840908, 5.215563]], [[0.351, 1.569, 26.852, 0.0, 0.2, 1], [0.476299, -0.652463]]], [[[-0.524, 0.032, 18.679, 1.62, 0.01, 12], [-0.462818, 0.964422]], [[-1.034, -1.918, 17.266, -3.0, 0.05, 0], [-1.034, -1.918]], [[1.44, 1.336, 22.594, 1.62, 0.01, 0], [1.44, 1.336]], [[-1.903, 0.319, 4.566, 9.81, 0.2, 15], [-1.879918, 0.223735]], [[-1.237, -1.128, 5.405, 0.0, 0.1, 4], [-1.129674, 1.62072]], [[0.607, 0.875, 20.434, 9.81, 0.1, 0], [0.607, 0.875]], [[-1.506, -0.728, 20.263, 0.0, 0.2, 8], [-0.730988, 5.34242]], [[0.287, -0.804, 3.405, 1.62, 0.1, 16], [-1.306007, 0.532531]]], [[[1.537, -0.061, 3.471, 9.81, 0.01, 14], [1.380971, -2.155206]], [[1.283, -1.185, 20.785, 1.62, 0.1, 19], [-1.300286, -2.908473]], [[-1.906, 1.981, 26.331, 0.0, 0.05, 19], [-0.716782, -9.203301]], [[-0.644, -1.525, 25.392, 9.81, 0.01, 0], [-0.644, -1.525]], [[-0.042, -1.542, 14.783, 0.0, 0.05, 2], [-0.190275, -1.368698]], [[0.95, -1.335, 21.477, 1.62, 0.05, 13], [-1.128613, 0.748224]], [[-1.842, 1.748, 8.887, 9.81, 0.01, 2], [-1.805744, 1.876003]], [[0.32, -1.935, 12.544, 0.0, 0.05, 0], [0.32, -1.935]]]]
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[-1.401, -1.525][-1.401, -1.525]Passed
case 1[-1.521, -0.162][-1.521, -0.162]Passed
case 2[-1.409624, 0.626083][-1.409656, 0.624717]Failed
case 3[1.118, 0.885][1.118, 0.885]Passed
case 4[-2.069603, 10.212847][-0.289748, 3.564566]Failed
case 5[0.951014, -1.661395][0.951324, -1.641689]Failed
case 6[-0.577753, -0.887778][-0.577279, -0.8735]Failed
case 7[-1.551716, -1.333185][-1.55164, -1.330312]Failed

SHA-256 / ac90e04a02992959ec4b56ede1b4696827f79cbd6c2d5807b09dc1503c81e246

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 = [[[[-1.401, -1.525, 6.432, 9.81, 0.1, 0], [-1.401, -1.525]], [[-1.521, -0.162, 6.527, 1.62, 0.2, 0], [-1.521, -0.162]], [[-1.436, 0.032, 5.193, 0.0, 0.01, 8], [-1.409656, 0.624717]], [[1.118, 0.885, 9.249, -3.0, 0.05, 0], [1.118, 0.885]], [[0.919, -1.08, 12.114, 1.62, 0.2, 6], [-0.289748, 3.564566]], [[1.046, -0.242, 4.156, 9.81, 0.05, 2], [0.951324, -1.641689]], [[-0.493, -1.517, 17.061, 0.0, 0.01, 7], [-0.577279, -0.8735]], [[-1.458, -1.788, 3.071, -3.0, 0.01, 6], [-1.55164, -1.330312]]], [[[-0.398, 0.635, 6.904, 1.62, 0.2, 2], [-0.095078, 0.670473]], [[0.267, 1.417, 23.02, 9.81, 0.1, 0], [0.267, 1.417]], [[0.993, -0.368, 11.989, 1.62, 0.05, 0], [0.993, -0.368]], [[1.3, 1.873, 5.499, 0.0, 0.01, 4], [1.369102, 1.57925]], [[0.51, 0.5, 18.606, 9.81, 0.1, 8], [-1.54552, 0.97061]], [[-0.524, 1.359, 8.765, 0.0, 0.05, 0], [-0.524, 1.359]], [[-1.057, 0.244, 12.747, 9.81, 0.01, 8], [-1.026095, 0.5232]], [[-0.784, -0.844, 28.335, 0.0, 0.01, 5], [-0.798112, 0.283127]]], [[[1.097, 0.569, 19.714, 0.0, 0.1, 0], [1.097, 0.569]], [[-0.531, 0.372, 6.858, 0.0, 0.05, 12], [0.143203, 1.386993]], [[1.147, -0.931, 6.394, 9.81, 0.05, 0], [1.147, -0.931]], [[-1.282, 0.842, 22.906, 0.0, 0.05, 3], [-0.847335, 4.648228]], [[0.444, -1.128, 10.972, 1.62, 0.05, 0], [0.444, -1.128]], [[1.781, 1.929, 19.092, -3.0, 0.01, 4], [1.833049, 0.665995]], [[1.294, 0.75, 29.753, 0.0, 0.1, 10], [0.840908, 5.215563]], [[0.351, 1.569, 26.852, 0.0, 0.2, 1], [0.476299, -0.652463]]], [[[-0.524, 0.032, 18.679, 1.62, 0.01, 12], [-0.462818, 0.964422]], [[-1.034, -1.918, 17.266, -3.0, 0.05, 0], [-1.034, -1.918]], [[1.44, 1.336, 22.594, 1.62, 0.01, 0], [1.44, 1.336]], [[-1.903, 0.319, 4.566, 9.81, 0.2, 15], [-1.879918, 0.223735]], [[-1.237, -1.128, 5.405, 0.0, 0.1, 4], [-1.129674, 1.62072]], [[0.607, 0.875, 20.434, 9.81, 0.1, 0], [0.607, 0.875]], [[-1.506, -0.728, 20.263, 0.0, 0.2, 8], [-0.730988, 5.34242]], [[0.287, -0.804, 3.405, 1.62, 0.1, 16], [-1.306007, 0.532531]]], [[[1.537, -0.061, 3.471, 9.81, 0.01, 14], [1.380971, -2.155206]], [[1.283, -1.185, 20.785, 1.62, 0.1, 19], [-1.300286, -2.908473]], [[-1.906, 1.981, 26.331, 0.0, 0.05, 19], [-0.716782, -9.203301]], [[-0.644, -1.525, 25.392, 9.81, 0.01, 0], [-0.644, -1.525]], [[-0.042, -1.542, 14.783, 0.0, 0.05, 2], [-0.190275, -1.368698]], [[0.95, -1.335, 21.477, 1.62, 0.05, 13], [-1.128613, 0.748224]], [[-1.842, 1.748, 8.887, 9.81, 0.01, 2], [-1.805744, 1.876003]], [[0.32, -1.935, 12.544, 0.0, 0.05, 0], [0.32, -1.935]]]]
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[-1.401, -1.525][-1.401, -1.525]Passed
case 1[-1.521, -0.162][-1.521, -0.162]Passed
case 2[-1.409656, 0.624717][-1.409656, 0.624717]Passed
case 3[1.118, 0.885][1.118, 0.885]Passed
case 4[-0.289748, 3.564566][-0.289748, 3.564566]Passed
case 5[0.951324, -1.641689][0.951324, -1.641689]Passed
case 6[-0.577279, -0.8735][-0.577279, -0.8735]Passed
case 7[-1.55164, -1.330312][-1.55164, -1.330312]Passed

SHA-256 / cb600e3bee5cd83e88ebbab1cca091638329a4441682b1bfa3ced90ad4caf14f

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

Case digest / 323118084d40a9c967e502a3fab7a07772a8ce60c7db2e840898803f0279ba71