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