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.
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 fixture | Actual | Expected | Outcome |
|---|---|---|---|
| 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 fixture | Actual | Expected | Outcome |
|---|---|---|---|
| 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 fixture | Actual | Expected | Outcome |
|---|---|---|---|
| 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