FA-87071 / Physics integrator stability / Open access
Leapfrog starts without offsetting velocity by half a step · case 01
The orbit phase is shifted and energy oscillation is biased from the first step.
ROOT CAUSE
vh is initialised to v0 instead of v0+a(x0)*dt/2.
VERIFIED REPAIR
Stagger the stored velocity with an initial half kick.
Unsuccessful approach: A full initial kick overshoots the staggered grid.
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
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.125, 0.39, 17.455, 1.62, 0.05, 1], [0.139748, 0.193471]], [[-0.631, -0.047, 16.626, 1.62, 0.01, 4], [-0.62579, 0.306844]], [[0.919, -1.08, 12.114, 1.62, 0.2, 6], [-0.289748, 3.564566]], [[1.555, -1.45, 14.106, 0.0, 0.2, 2], [-0.368627, -5.454665]], [[1.7, 0.206, 28.764, 0.0, 0.05, 1], [1.649177, -2.202393]], [[-1.14, -0.439, 14.103, 1.62, 0.05, 7], [-0.486563, 3.598832]], [[-0.761, 0.719, 22.476, 0.0, 0.05, 2], [-0.6068, 2.278351]], [[-0.776, -1.002, 7.733, 0.0, 0.2, 5], [0.616431, 1.609137]]], [[[1.3, 1.873, 5.499, 0.0, 0.01, 4], [1.369102, 1.57925]], [[-0.167, 1.506, 3.694, -3.0, 0.1, 7], [1.362931, 2.160182]], [[0.221, 0.724, 15.178, -3.0, 0.01, 4], [0.249567, 0.701111]], [[-1.39, 0.449, 7.635, 0.0, 0.1, 2], [-1.095426, 2.384282]], [[1.036, 0.495, 13.095, -3.0, 0.05, 2], [1.03229, -0.567985]], [[-0.141, -1.302, 4.434, 0.0, 0.2, 3], [-0.646031, -0.104617]], [[-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.277, -0.541, 11.271, 1.62, 0.1, 4], [-0.552191, 3.54015]], [[-1.849, 1.1, 5.352, 1.62, 0.2, 0], [-1.849, 1.1]], [[1.032, -0.358, 14.758, 9.81, 0.2, 3], [-1.946637, -3.958747]], [[0.444, -1.128, 10.972, 1.62, 0.05, 0], [0.444, -1.128]], [[-0.73, 0.772, 21.128, 0.0, 0.01, 14], [-0.483162, 2.630496]], [[0.296, -0.729, 7.819, 1.62, 0.01, 4], [0.263753, -0.881483]], [[0.094, -1.444, 12.401, -3.0, 0.01, 1], [0.079652, -1.424767]], [[0.187, -1.035, 0.81, 0.0, 0.2, 5], [-0.789309, -0.773803]]], [[[-1.506, -0.728, 20.263, 0.0, 0.2, 8], [-0.730988, 5.34242]], [[0.348, -1.537, 1.936, 1.62, 0.05, 6], [-0.20195, -2.073008]], [[0.136, 1.165, 29.739, 0.0, 0.1, 10], [-0.054367, 1.336045]], [[-0.361, 1.302, 18.884, 0.0, 0.2, 2], [0.405487, 1.082999]], [[-0.299, 0.546, 29.023, 0.0, 0.1, 10], [-0.279669, -0.774088]], [[-1.071, -1.25, 16.531, 0.0, 0.05, 9], [-0.021098, 4.507887]], [[1.5, -1.381, 20.519, 1.62, 0.2, 16], [-1.532726, -2.845764]], [[-1.412, -0.807, 16.554, 9.81, 0.01, 12], [-1.409313, 0.850886]]], [[[1.537, -0.061, 3.471, 9.81, 0.01, 14], [1.380971, -2.155206]], [[-1.959, 0.876, 22.549, 9.81, 0.2, 18], [-1.391304, -5.035599]], [[0.439, 1.285, 6.359, 0.0, 0.1, 20], [-0.335243, 1.467632]], [[-0.042, -1.542, 14.783, 0.0, 0.05, 2], [-0.190275, -1.368698]], [[1.53, 0.671, 11.673, -3.0, 0.01, 19], [1.389791, -2.094408]], [[-1.842, 1.748, 8.887, 9.81, 0.01, 2], [-1.805744, 1.876003]], [[0.292, 1.816, 25.353, 0.0, 0.05, 9], [0.089465, -2.285955]], [[1.75, -0.119, 5.587, -3.0, 0.01, 2], [1.746265, -0.254355]]]]
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.1445, 0.286444] | [0.139748, 0.193471] | Failed |
| case 1 | [-0.627557, 0.263077] | [-0.62579, 0.306844] | Failed |
| case 2 | [-0.642276, 3.013905] | [-0.289748, 3.564566] | Failed |
| case 3 | [0.261236, -5.387318] | [-0.368627, -5.454665] | Failed |
| case 4 | [1.7103, -1.023877] | [1.649177, -2.202393] | Failed |
| case 5 | [-0.580121, 3.507847] | [-0.486563, 3.598832] | Failed |
| case 6 | [-0.648359, 1.898124] | [-0.6068, 2.278351] | Failed |
| case 7 | [0.544992, 2.178068] | [0.616431, 1.609137] | Failed |
SHA-256 / 28905ddbb479ce245e61741e7cc268d4927cad928c78b958028c959190595dc9
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
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.125, 0.39, 17.455, 1.62, 0.05, 1], [0.139748, 0.193471]], [[-0.631, -0.047, 16.626, 1.62, 0.01, 4], [-0.62579, 0.306844]], [[0.919, -1.08, 12.114, 1.62, 0.2, 6], [-0.289748, 3.564566]], [[1.555, -1.45, 14.106, 0.0, 0.2, 2], [-0.368627, -5.454665]], [[1.7, 0.206, 28.764, 0.0, 0.05, 1], [1.649177, -2.202393]], [[-1.14, -0.439, 14.103, 1.62, 0.05, 7], [-0.486563, 3.598832]], [[-0.761, 0.719, 22.476, 0.0, 0.05, 2], [-0.6068, 2.278351]], [[-0.776, -1.002, 7.733, 0.0, 0.2, 5], [0.616431, 1.609137]]], [[[1.3, 1.873, 5.499, 0.0, 0.01, 4], [1.369102, 1.57925]], [[-0.167, 1.506, 3.694, -3.0, 0.1, 7], [1.362931, 2.160182]], [[0.221, 0.724, 15.178, -3.0, 0.01, 4], [0.249567, 0.701111]], [[-1.39, 0.449, 7.635, 0.0, 0.1, 2], [-1.095426, 2.384282]], [[1.036, 0.495, 13.095, -3.0, 0.05, 2], [1.03229, -0.567985]], [[-0.141, -1.302, 4.434, 0.0, 0.2, 3], [-0.646031, -0.104617]], [[-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.277, -0.541, 11.271, 1.62, 0.1, 4], [-0.552191, 3.54015]], [[-1.849, 1.1, 5.352, 1.62, 0.2, 0], [-1.849, 1.1]], [[1.032, -0.358, 14.758, 9.81, 0.2, 3], [-1.946637, -3.958747]], [[0.444, -1.128, 10.972, 1.62, 0.05, 0], [0.444, -1.128]], [[-0.73, 0.772, 21.128, 0.0, 0.01, 14], [-0.483162, 2.630496]], [[0.296, -0.729, 7.819, 1.62, 0.01, 4], [0.263753, -0.881483]], [[0.094, -1.444, 12.401, -3.0, 0.01, 1], [0.079652, -1.424767]], [[0.187, -1.035, 0.81, 0.0, 0.2, 5], [-0.789309, -0.773803]]], [[[-1.506, -0.728, 20.263, 0.0, 0.2, 8], [-0.730988, 5.34242]], [[0.348, -1.537, 1.936, 1.62, 0.05, 6], [-0.20195, -2.073008]], [[0.136, 1.165, 29.739, 0.0, 0.1, 10], [-0.054367, 1.336045]], [[-0.361, 1.302, 18.884, 0.0, 0.2, 2], [0.405487, 1.082999]], [[-0.299, 0.546, 29.023, 0.0, 0.1, 10], [-0.279669, -0.774088]], [[-1.071, -1.25, 16.531, 0.0, 0.05, 9], [-0.021098, 4.507887]], [[1.5, -1.381, 20.519, 1.62, 0.2, 16], [-1.532726, -2.845764]], [[-1.412, -0.807, 16.554, 9.81, 0.01, 12], [-1.409313, 0.850886]]], [[[1.537, -0.061, 3.471, 9.81, 0.01, 14], [1.380971, -2.155206]], [[-1.959, 0.876, 22.549, 9.81, 0.2, 18], [-1.391304, -5.035599]], [[0.439, 1.285, 6.359, 0.0, 0.1, 20], [-0.335243, 1.467632]], [[-0.042, -1.542, 14.783, 0.0, 0.05, 2], [-0.190275, -1.368698]], [[1.53, 0.671, 11.673, -3.0, 0.01, 19], [1.389791, -2.094408]], [[-1.842, 1.748, 8.887, 9.81, 0.01, 2], [-1.805744, 1.876003]], [[0.292, 1.816, 25.353, 0.0, 0.05, 9], [0.089465, -2.285955]], [[1.75, -0.119, 5.587, -3.0, 0.01, 2], [1.746265, -0.254355]]]]
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.134995, 0.100498] | [0.139748, 0.193471] | Failed |
| case 1 | [-0.624023, 0.35061] | [-0.62579, 0.306844] | Failed |
| case 2 | [0.06278, 4.115226] | [-0.289748, 3.564566] | Failed |
| case 3 | [-0.99849, -5.522013] | [-0.368627, -5.454665] | Failed |
| case 4 | [1.588053, -3.380909] | [1.649177, -2.202393] | Failed |
| case 5 | [-0.393005, 3.689816] | [-0.486563, 3.598832] | Failed |
| case 6 | [-0.565241, 2.658578] | [-0.6068, 2.278351] | Failed |
| case 7 | [0.687871, 1.040205] | [0.616431, 1.609137] | Failed |
SHA-256 / db458e85d5102f3d5fef484c1591653da16d6eabae5827e0be8be697a3fbd1fc
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.125, 0.39, 17.455, 1.62, 0.05, 1], [0.139748, 0.193471]], [[-0.631, -0.047, 16.626, 1.62, 0.01, 4], [-0.62579, 0.306844]], [[0.919, -1.08, 12.114, 1.62, 0.2, 6], [-0.289748, 3.564566]], [[1.555, -1.45, 14.106, 0.0, 0.2, 2], [-0.368627, -5.454665]], [[1.7, 0.206, 28.764, 0.0, 0.05, 1], [1.649177, -2.202393]], [[-1.14, -0.439, 14.103, 1.62, 0.05, 7], [-0.486563, 3.598832]], [[-0.761, 0.719, 22.476, 0.0, 0.05, 2], [-0.6068, 2.278351]], [[-0.776, -1.002, 7.733, 0.0, 0.2, 5], [0.616431, 1.609137]]], [[[1.3, 1.873, 5.499, 0.0, 0.01, 4], [1.369102, 1.57925]], [[-0.167, 1.506, 3.694, -3.0, 0.1, 7], [1.362931, 2.160182]], [[0.221, 0.724, 15.178, -3.0, 0.01, 4], [0.249567, 0.701111]], [[-1.39, 0.449, 7.635, 0.0, 0.1, 2], [-1.095426, 2.384282]], [[1.036, 0.495, 13.095, -3.0, 0.05, 2], [1.03229, -0.567985]], [[-0.141, -1.302, 4.434, 0.0, 0.2, 3], [-0.646031, -0.104617]], [[-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.277, -0.541, 11.271, 1.62, 0.1, 4], [-0.552191, 3.54015]], [[-1.849, 1.1, 5.352, 1.62, 0.2, 0], [-1.849, 1.1]], [[1.032, -0.358, 14.758, 9.81, 0.2, 3], [-1.946637, -3.958747]], [[0.444, -1.128, 10.972, 1.62, 0.05, 0], [0.444, -1.128]], [[-0.73, 0.772, 21.128, 0.0, 0.01, 14], [-0.483162, 2.630496]], [[0.296, -0.729, 7.819, 1.62, 0.01, 4], [0.263753, -0.881483]], [[0.094, -1.444, 12.401, -3.0, 0.01, 1], [0.079652, -1.424767]], [[0.187, -1.035, 0.81, 0.0, 0.2, 5], [-0.789309, -0.773803]]], [[[-1.506, -0.728, 20.263, 0.0, 0.2, 8], [-0.730988, 5.34242]], [[0.348, -1.537, 1.936, 1.62, 0.05, 6], [-0.20195, -2.073008]], [[0.136, 1.165, 29.739, 0.0, 0.1, 10], [-0.054367, 1.336045]], [[-0.361, 1.302, 18.884, 0.0, 0.2, 2], [0.405487, 1.082999]], [[-0.299, 0.546, 29.023, 0.0, 0.1, 10], [-0.279669, -0.774088]], [[-1.071, -1.25, 16.531, 0.0, 0.05, 9], [-0.021098, 4.507887]], [[1.5, -1.381, 20.519, 1.62, 0.2, 16], [-1.532726, -2.845764]], [[-1.412, -0.807, 16.554, 9.81, 0.01, 12], [-1.409313, 0.850886]]], [[[1.537, -0.061, 3.471, 9.81, 0.01, 14], [1.380971, -2.155206]], [[-1.959, 0.876, 22.549, 9.81, 0.2, 18], [-1.391304, -5.035599]], [[0.439, 1.285, 6.359, 0.0, 0.1, 20], [-0.335243, 1.467632]], [[-0.042, -1.542, 14.783, 0.0, 0.05, 2], [-0.190275, -1.368698]], [[1.53, 0.671, 11.673, -3.0, 0.01, 19], [1.389791, -2.094408]], [[-1.842, 1.748, 8.887, 9.81, 0.01, 2], [-1.805744, 1.876003]], [[0.292, 1.816, 25.353, 0.0, 0.05, 9], [0.089465, -2.285955]], [[1.75, -0.119, 5.587, -3.0, 0.01, 2], [1.746265, -0.254355]]]]
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.139748, 0.193471] | [0.139748, 0.193471] | Passed |
| case 1 | [-0.62579, 0.306844] | [-0.62579, 0.306844] | Passed |
| case 2 | [-0.289748, 3.564566] | [-0.289748, 3.564566] | Passed |
| case 3 | [-0.368627, -5.454665] | [-0.368627, -5.454665] | Passed |
| case 4 | [1.649177, -2.202393] | [1.649177, -2.202393] | Passed |
| case 5 | [-0.486563, 3.598832] | [-0.486563, 3.598832] | Passed |
| case 6 | [-0.6068, 2.278351] | [-0.6068, 2.278351] | Passed |
| case 7 | [0.616431, 1.609137] | [0.616431, 1.609137] | Passed |
SHA-256 / a73d1f0289862a59dfe68e4a9a892181f32709120387f4d7908d1248d3c501aa
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.373521+00:00.
Case digest / 9108722a1d6f8442984259e78e05351e7de414cf05b2a8ad9aec269172bf98de