FA-87076 / Physics integrator stability / Open access
Leapfrog reports the half-step velocity as the current velocity · case 01
Velocity output is offset by half a step, corrupting energy and contact logic.
ROOT CAUSE
The final velocity is returned without the back half-kick.
VERIFIED REPAIR
Subtract a(x)*dt/2 from the staggered velocity.
Unsuccessful approach: Adding the half kick synchronizes to the next half step instead.
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
vh = vh + acc(x) * dt
v = vh
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.7, 0.206, 28.764, 0.0, 0.05, 1], [1.649177, -2.202393]], [[1.673, -0.912, 25.576, 1.62, 0.01, 0], [1.673, -0.912]], [[-1.14, -0.439, 14.103, 1.62, 0.05, 7], [-0.486563, 3.598832]], [[0.546, 0.718, 9.779, 1.62, 0.01, 4], [0.569089, 0.434764]], [[-0.116, 0.63, 5.023, 1.62, 0.01, 1], [-0.109752, 0.61947]], [[-0.776, -1.002, 7.733, 0.0, 0.2, 5], [0.616431, 1.609137]], [[0.454, 1.015, 13.329, 1.62, 0.1, 8], [-0.627698, -1.413232]], [[1.929, 0.884, 10.735, -3.0, 0.1, 6], [-0.117431, -5.249921]]], [[[-0.141, -1.302, 4.434, 0.0, 0.2, 3], [-0.646031, -0.104617]], [[0.221, 0.724, 15.178, -3.0, 0.01, 4], [0.249567, 0.701111]], [[-0.398, 0.635, 6.904, 1.62, 0.2, 2], [-0.095078, 0.670473]], [[-1.537, -0.458, 19.191, 0.0, 0.1, 1], [-1.435317, 2.394087]], [[1.542, -0.412, 13.302, -3.0, 0.05, 5], [0.940546, -4.03579]], [[-1.933, 0.742, 17.091, 9.81, 0.1, 0], [-1.933, 0.742]], [[-1.3, -1.601, 29.614, 0.0, 0.01, 8], [-1.302842, 1.5311]], [[-1.39, 0.449, 7.635, 0.0, 0.1, 2], [-1.095426, 2.384282]]], [[[-1.614, -0.301, 28.452, 0.0, 0.1, 3], [0.020608, 8.302094]], [[-1.735, -0.587, 6.94, 9.81, 0.01, 1], [-1.740758, -0.564491]], [[-0.575, 0.793, 13.055, 0.0, 0.05, 9], [0.252971, 2.020225]], [[-0.73, 0.772, 21.128, 0.0, 0.01, 14], [-0.483162, 2.630496]], [[1.361, 1.476, 18.508, -3.0, 0.1, 14], [1.259752, 2.50659]], [[0.094, -1.444, 12.401, -3.0, 0.01, 1], [0.079652, -1.424767]], [[0.444, -1.128, 10.972, 1.62, 0.05, 0], [0.444, -1.128]], [[1.032, -0.358, 14.758, 9.81, 0.2, 3], [-1.946637, -3.958747]]], [[[0.12, -1.504, 16.817, 9.81, 0.2, 10], [-1.311284, -1.330163]], [[-1.412, -0.807, 16.554, 9.81, 0.01, 12], [-1.409313, 0.850886]], [[-0.848, -0.478, 27.166, 0.0, 0.1, 10], [-0.370416, -3.868163]], [[-0.361, 1.302, 18.884, 0.0, 0.2, 2], [0.405487, 1.082999]], [[-0.711, -0.993, 29.278, 0.0, 0.05, 4], [-0.495489, 2.9085]], [[-0.367, -0.664, 29.52, 9.81, 0.01, 10], [-0.425216, -0.470963]], [[0.348, -1.537, 1.936, 1.62, 0.05, 6], [-0.20195, -2.073008]], [[1.5, -1.381, 20.519, 1.62, 0.2, 16], [-1.532726, -2.845764]]], [[[0.485, -1.561, 16.759, -3.0, 0.1, 5], [-0.308777, -0.345923]], [[-1.8, -0.658, 27.149, -3.0, 0.05, 1], [-1.768065, 1.913735]], [[1.537, -0.061, 3.471, 9.81, 0.01, 14], [1.380971, -2.155206]], [[-0.623, 0.986, 1.922, -3.0, 0.1, 14], [3.018244, 2.45605]], [[-1.842, 1.748, 8.887, 9.81, 0.01, 2], [-1.805744, 1.876003]], [[-0.042, -1.542, 14.783, 0.0, 0.05, 2], [-0.190275, -1.368698]], [[-1.884, -1.434, 28.956, -3.0, 0.05, 11], [2.015248, 3.236951]], [[-0.026, -0.891, 12.663, -3.0, 0.2, 4], [0.431498, 1.067509]]]]
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.649177, -3.388316] | [1.649177, -2.202393] | Failed |
| case 1 | [1.673, -1.134043] | [1.673, -0.912] | Failed |
| case 2 | [-0.486563, 3.729882] | [-0.486563, 3.598832] | Failed |
| case 3 | [0.569089, 0.398839] | [0.569089, 0.434764] | Failed |
| case 4 | [-0.109752, 0.614126] | [-0.109752, 0.61947] | Failed |
| case 5 | [0.616431, 1.13245] | [0.616431, 1.609137] | Failed |
| case 6 | [-0.627698, -1.075903] | [-0.627698, -1.413232] | Failed |
| case 7 | [-0.117431, -5.03689] | [-0.117431, -5.249921] | Failed |
SHA-256 / bcf5528f8f44b4fc949e60095ca6a5497a57afdf9393ce2eaf8bc42567f58686
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 * 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.7, 0.206, 28.764, 0.0, 0.05, 1], [1.649177, -2.202393]], [[1.673, -0.912, 25.576, 1.62, 0.01, 0], [1.673, -0.912]], [[-1.14, -0.439, 14.103, 1.62, 0.05, 7], [-0.486563, 3.598832]], [[0.546, 0.718, 9.779, 1.62, 0.01, 4], [0.569089, 0.434764]], [[-0.116, 0.63, 5.023, 1.62, 0.01, 1], [-0.109752, 0.61947]], [[-0.776, -1.002, 7.733, 0.0, 0.2, 5], [0.616431, 1.609137]], [[0.454, 1.015, 13.329, 1.62, 0.1, 8], [-0.627698, -1.413232]], [[1.929, 0.884, 10.735, -3.0, 0.1, 6], [-0.117431, -5.249921]]], [[[-0.141, -1.302, 4.434, 0.0, 0.2, 3], [-0.646031, -0.104617]], [[0.221, 0.724, 15.178, -3.0, 0.01, 4], [0.249567, 0.701111]], [[-0.398, 0.635, 6.904, 1.62, 0.2, 2], [-0.095078, 0.670473]], [[-1.537, -0.458, 19.191, 0.0, 0.1, 1], [-1.435317, 2.394087]], [[1.542, -0.412, 13.302, -3.0, 0.05, 5], [0.940546, -4.03579]], [[-1.933, 0.742, 17.091, 9.81, 0.1, 0], [-1.933, 0.742]], [[-1.3, -1.601, 29.614, 0.0, 0.01, 8], [-1.302842, 1.5311]], [[-1.39, 0.449, 7.635, 0.0, 0.1, 2], [-1.095426, 2.384282]]], [[[-1.614, -0.301, 28.452, 0.0, 0.1, 3], [0.020608, 8.302094]], [[-1.735, -0.587, 6.94, 9.81, 0.01, 1], [-1.740758, -0.564491]], [[-0.575, 0.793, 13.055, 0.0, 0.05, 9], [0.252971, 2.020225]], [[-0.73, 0.772, 21.128, 0.0, 0.01, 14], [-0.483162, 2.630496]], [[1.361, 1.476, 18.508, -3.0, 0.1, 14], [1.259752, 2.50659]], [[0.094, -1.444, 12.401, -3.0, 0.01, 1], [0.079652, -1.424767]], [[0.444, -1.128, 10.972, 1.62, 0.05, 0], [0.444, -1.128]], [[1.032, -0.358, 14.758, 9.81, 0.2, 3], [-1.946637, -3.958747]]], [[[0.12, -1.504, 16.817, 9.81, 0.2, 10], [-1.311284, -1.330163]], [[-1.412, -0.807, 16.554, 9.81, 0.01, 12], [-1.409313, 0.850886]], [[-0.848, -0.478, 27.166, 0.0, 0.1, 10], [-0.370416, -3.868163]], [[-0.361, 1.302, 18.884, 0.0, 0.2, 2], [0.405487, 1.082999]], [[-0.711, -0.993, 29.278, 0.0, 0.05, 4], [-0.495489, 2.9085]], [[-0.367, -0.664, 29.52, 9.81, 0.01, 10], [-0.425216, -0.470963]], [[0.348, -1.537, 1.936, 1.62, 0.05, 6], [-0.20195, -2.073008]], [[1.5, -1.381, 20.519, 1.62, 0.2, 16], [-1.532726, -2.845764]]], [[[0.485, -1.561, 16.759, -3.0, 0.1, 5], [-0.308777, -0.345923]], [[-1.8, -0.658, 27.149, -3.0, 0.05, 1], [-1.768065, 1.913735]], [[1.537, -0.061, 3.471, 9.81, 0.01, 14], [1.380971, -2.155206]], [[-0.623, 0.986, 1.922, -3.0, 0.1, 14], [3.018244, 2.45605]], [[-1.842, 1.748, 8.887, 9.81, 0.01, 2], [-1.805744, 1.876003]], [[-0.042, -1.542, 14.783, 0.0, 0.05, 2], [-0.190275, -1.368698]], [[-1.884, -1.434, 28.956, -3.0, 0.05, 11], [2.015248, 3.236951]], [[-0.026, -0.891, 12.663, -3.0, 0.2, 4], [0.431498, 1.067509]]]]
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.649177, -4.574238] | [1.649177, -2.202393] | Failed |
| case 1 | [1.673, -1.356086] | [1.673, -0.912] | Failed |
| case 2 | [-0.486563, 3.860931] | [-0.486563, 3.598832] | Failed |
| case 3 | [0.569089, 0.362913] | [0.569089, 0.434764] | Failed |
| case 4 | [-0.109752, 0.608783] | [-0.109752, 0.61947] | Failed |
| case 5 | [0.616431, 0.655764] | [0.616431, 1.609137] | Failed |
| case 6 | [-0.627698, -0.738574] | [-0.627698, -1.413232] | Failed |
| case 7 | [-0.117431, -4.823859] | [-0.117431, -5.249921] | Failed |
SHA-256 / e6bf8b787a4456e31252de2abb89f676c8f5d95b09be4bde078c72a91b265a37
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.7, 0.206, 28.764, 0.0, 0.05, 1], [1.649177, -2.202393]], [[1.673, -0.912, 25.576, 1.62, 0.01, 0], [1.673, -0.912]], [[-1.14, -0.439, 14.103, 1.62, 0.05, 7], [-0.486563, 3.598832]], [[0.546, 0.718, 9.779, 1.62, 0.01, 4], [0.569089, 0.434764]], [[-0.116, 0.63, 5.023, 1.62, 0.01, 1], [-0.109752, 0.61947]], [[-0.776, -1.002, 7.733, 0.0, 0.2, 5], [0.616431, 1.609137]], [[0.454, 1.015, 13.329, 1.62, 0.1, 8], [-0.627698, -1.413232]], [[1.929, 0.884, 10.735, -3.0, 0.1, 6], [-0.117431, -5.249921]]], [[[-0.141, -1.302, 4.434, 0.0, 0.2, 3], [-0.646031, -0.104617]], [[0.221, 0.724, 15.178, -3.0, 0.01, 4], [0.249567, 0.701111]], [[-0.398, 0.635, 6.904, 1.62, 0.2, 2], [-0.095078, 0.670473]], [[-1.537, -0.458, 19.191, 0.0, 0.1, 1], [-1.435317, 2.394087]], [[1.542, -0.412, 13.302, -3.0, 0.05, 5], [0.940546, -4.03579]], [[-1.933, 0.742, 17.091, 9.81, 0.1, 0], [-1.933, 0.742]], [[-1.3, -1.601, 29.614, 0.0, 0.01, 8], [-1.302842, 1.5311]], [[-1.39, 0.449, 7.635, 0.0, 0.1, 2], [-1.095426, 2.384282]]], [[[-1.614, -0.301, 28.452, 0.0, 0.1, 3], [0.020608, 8.302094]], [[-1.735, -0.587, 6.94, 9.81, 0.01, 1], [-1.740758, -0.564491]], [[-0.575, 0.793, 13.055, 0.0, 0.05, 9], [0.252971, 2.020225]], [[-0.73, 0.772, 21.128, 0.0, 0.01, 14], [-0.483162, 2.630496]], [[1.361, 1.476, 18.508, -3.0, 0.1, 14], [1.259752, 2.50659]], [[0.094, -1.444, 12.401, -3.0, 0.01, 1], [0.079652, -1.424767]], [[0.444, -1.128, 10.972, 1.62, 0.05, 0], [0.444, -1.128]], [[1.032, -0.358, 14.758, 9.81, 0.2, 3], [-1.946637, -3.958747]]], [[[0.12, -1.504, 16.817, 9.81, 0.2, 10], [-1.311284, -1.330163]], [[-1.412, -0.807, 16.554, 9.81, 0.01, 12], [-1.409313, 0.850886]], [[-0.848, -0.478, 27.166, 0.0, 0.1, 10], [-0.370416, -3.868163]], [[-0.361, 1.302, 18.884, 0.0, 0.2, 2], [0.405487, 1.082999]], [[-0.711, -0.993, 29.278, 0.0, 0.05, 4], [-0.495489, 2.9085]], [[-0.367, -0.664, 29.52, 9.81, 0.01, 10], [-0.425216, -0.470963]], [[0.348, -1.537, 1.936, 1.62, 0.05, 6], [-0.20195, -2.073008]], [[1.5, -1.381, 20.519, 1.62, 0.2, 16], [-1.532726, -2.845764]]], [[[0.485, -1.561, 16.759, -3.0, 0.1, 5], [-0.308777, -0.345923]], [[-1.8, -0.658, 27.149, -3.0, 0.05, 1], [-1.768065, 1.913735]], [[1.537, -0.061, 3.471, 9.81, 0.01, 14], [1.380971, -2.155206]], [[-0.623, 0.986, 1.922, -3.0, 0.1, 14], [3.018244, 2.45605]], [[-1.842, 1.748, 8.887, 9.81, 0.01, 2], [-1.805744, 1.876003]], [[-0.042, -1.542, 14.783, 0.0, 0.05, 2], [-0.190275, -1.368698]], [[-1.884, -1.434, 28.956, -3.0, 0.05, 11], [2.015248, 3.236951]], [[-0.026, -0.891, 12.663, -3.0, 0.2, 4], [0.431498, 1.067509]]]]
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.649177, -2.202393] | [1.649177, -2.202393] | Passed |
| case 1 | [1.673, -0.912] | [1.673, -0.912] | Passed |
| case 2 | [-0.486563, 3.598832] | [-0.486563, 3.598832] | Passed |
| case 3 | [0.569089, 0.434764] | [0.569089, 0.434764] | Passed |
| case 4 | [-0.109752, 0.61947] | [-0.109752, 0.61947] | Passed |
| case 5 | [0.616431, 1.609137] | [0.616431, 1.609137] | Passed |
| case 6 | [-0.627698, -1.413232] | [-0.627698, -1.413232] | Passed |
| case 7 | [-0.117431, -5.249921] | [-0.117431, -5.249921] | Passed |
SHA-256 / 7ab00579776a5e5c267594b9e18c952f05a548c28b3ce38172e77df588c5161c
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.371200+00:00.
Case digest / b6267f7509953feb0048c32a317550bfe26e542a592bcceeb60a2263a3a29a4a