FA-87086 / Physics integrator stability / Open access
Constant field acceleration is added with the wrong sign · case 01
Bodies settle on the wrong side of the spring rest point.
ROOT CAUSE
The acceleration is -w2*x+g.
VERIFIED REPAIR
The contract field term is -g.
Unsuccessful approach: Folding g into the spring offset as -w2*(x-g) mixes units.
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 - 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.051, -1.98, 13.751, 9.81, 0.01, 1], [1.029987, -2.221178]], [[-0.493, -1.517, 17.061, 0.0, 0.01, 7], [-0.577279, -0.8735]], [[-0.631, -0.047, 16.626, 1.62, 0.01, 4], [-0.62579, 0.306844]], [[0.315, 1.535, 8.697, 9.81, 0.01, 3], [0.355353, 1.153028]], [[0.436, 1.25, 21.45, 9.81, 0.2, 5], [-0.668425, 3.776016]], [[-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.7, 0.206, 28.764, 0.0, 0.05, 1], [1.649177, -2.202393]]], [[[-1.873, -1.269, 8.691, 9.81, 0.1, 5], [-1.630061, 2.045253]], [[-0.281, 1.62, 5.784, 9.81, 0.1, 3], [-0.186184, -1.021531]], [[-1.933, 0.742, 17.091, 9.81, 0.1, 0], [-1.933, 0.742]], [[1.251, -1.263, 11.392, 0.0, 0.2, 1], [0.713372, -3.500813]], [[-1.512, -0.917, 2.156, -3.0, 0.1, 10], [0.475297, 4.13743]], [[0.207, 0.644, 21.918, 1.62, 0.05, 6], [0.108366, -1.184217]], [[-1.537, -0.458, 19.191, 0.0, 0.1, 1], [-1.435317, 2.394087]], [[-1.057, 0.244, 12.747, 9.81, 0.01, 8], [-1.026095, 0.5232]]], [[[1.893, -0.282, 22.513, 0.0, 0.01, 5], [1.825995, -2.384623]], [[-0.611, -0.509, 8.029, 0.0, 0.1, 0], [-0.611, -0.509]], [[-1.277, -0.541, 11.271, 1.62, 0.1, 4], [-0.552191, 3.54015]], [[1.361, 1.476, 18.508, -3.0, 0.1, 14], [1.259752, 2.50659]], [[0.191, -1.592, 15.855, -3.0, 0.2, 2], [-0.245776, 0.100628]], [[-0.665, -0.888, 20.198, -3.0, 0.01, 7], [-0.686227, 0.286733]], [[-1.003, 0.428, 3.825, -3.0, 0.2, 2], [-0.318898, 2.730839]], [[-0.73, 0.772, 21.128, 0.0, 0.01, 14], [-0.483162, 2.630496]]], [[[-0.163, -0.394, 12.347, 1.62, 0.2, 7], [-0.027135, -0.22127]], [[-0.299, 0.546, 29.023, 0.0, 0.1, 10], [-0.279669, -0.774088]], [[-0.731, -0.105, 23.504, 9.81, 0.2, 8], [-0.365533, 1.315743]], [[-0.245, 1.761, 12.022, 1.62, 0.2, 9], [-0.195747, 1.786149]], [[-1.034, -1.918, 17.266, -3.0, 0.05, 0], [-1.034, -1.918]], [[-0.029, -0.774, 20.819, -3.0, 0.01, 6], [-0.06843, -0.531674]], [[1.944, -1.577, 8.082, -3.0, 0.05, 4], [1.39648, -3.731695]], [[-1.606, 1.339, 5.46, 0.0, 0.2, 9], [0.197038, -3.860859]]], [[[0.95, -1.335, 21.477, 1.62, 0.05, 13], [-1.128613, 0.748224]], [[0.489, 1.733, 26.727, 0.0, 0.1, 9], [-0.349993, 2.431476]], [[1.558, -0.861, 10.719, -3.0, 0.05, 7], [0.564719, -4.155803]], [[0.416, 1.191, 1.568, -3.0, 0.05, 19], [2.239843, 2.182456]], [[-0.515, 0.855, 16.541, 0.0, 0.1, 3], [0.0295, 2.21876]], [[-0.028, -0.375, 18.368, -3.0, 0.01, 13], [-0.045341, 0.115239]], [[-0.325, 0.3, 29.992, 9.81, 0.05, 7], [-0.275898, -0.114066]], [[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.030968, -2.025046] | [1.029987, -2.221178] | Failed |
| case 1 | [-0.577279, -0.8735] | [-0.577279, -0.8735] | Passed |
| case 2 | [-0.623204, 0.435852] | [-0.62579, 0.306844] | Failed |
| case 3 | [0.364176, 1.740817] | [0.355353, 1.153028] | Failed |
| case 4 | [0.152237, 0.04135] | [-0.668425, 3.776016] | Failed |
| case 5 | [-1.521, -0.162] | [-1.521, -0.162] | Passed |
| case 6 | [-1.815719, 3.652161] | [-1.821517, 3.460132] | Failed |
| case 7 | [1.649177, -2.202393] | [1.649177, -2.202393] | Passed |
SHA-256 / d2284d39f75ddd7cb6aa811b161ccf59daa22ba5e7347386edd745f63b55ea72
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.051, -1.98, 13.751, 9.81, 0.01, 1], [1.029987, -2.221178]], [[-0.493, -1.517, 17.061, 0.0, 0.01, 7], [-0.577279, -0.8735]], [[-0.631, -0.047, 16.626, 1.62, 0.01, 4], [-0.62579, 0.306844]], [[0.315, 1.535, 8.697, 9.81, 0.01, 3], [0.355353, 1.153028]], [[0.436, 1.25, 21.45, 9.81, 0.2, 5], [-0.668425, 3.776016]], [[-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.7, 0.206, 28.764, 0.0, 0.05, 1], [1.649177, -2.202393]]], [[[-1.873, -1.269, 8.691, 9.81, 0.1, 5], [-1.630061, 2.045253]], [[-0.281, 1.62, 5.784, 9.81, 0.1, 3], [-0.186184, -1.021531]], [[-1.933, 0.742, 17.091, 9.81, 0.1, 0], [-1.933, 0.742]], [[1.251, -1.263, 11.392, 0.0, 0.2, 1], [0.713372, -3.500813]], [[-1.512, -0.917, 2.156, -3.0, 0.1, 10], [0.475297, 4.13743]], [[0.207, 0.644, 21.918, 1.62, 0.05, 6], [0.108366, -1.184217]], [[-1.537, -0.458, 19.191, 0.0, 0.1, 1], [-1.435317, 2.394087]], [[-1.057, 0.244, 12.747, 9.81, 0.01, 8], [-1.026095, 0.5232]]], [[[1.893, -0.282, 22.513, 0.0, 0.01, 5], [1.825995, -2.384623]], [[-0.611, -0.509, 8.029, 0.0, 0.1, 0], [-0.611, -0.509]], [[-1.277, -0.541, 11.271, 1.62, 0.1, 4], [-0.552191, 3.54015]], [[1.361, 1.476, 18.508, -3.0, 0.1, 14], [1.259752, 2.50659]], [[0.191, -1.592, 15.855, -3.0, 0.2, 2], [-0.245776, 0.100628]], [[-0.665, -0.888, 20.198, -3.0, 0.01, 7], [-0.686227, 0.286733]], [[-1.003, 0.428, 3.825, -3.0, 0.2, 2], [-0.318898, 2.730839]], [[-0.73, 0.772, 21.128, 0.0, 0.01, 14], [-0.483162, 2.630496]]], [[[-0.163, -0.394, 12.347, 1.62, 0.2, 7], [-0.027135, -0.22127]], [[-0.299, 0.546, 29.023, 0.0, 0.1, 10], [-0.279669, -0.774088]], [[-0.731, -0.105, 23.504, 9.81, 0.2, 8], [-0.365533, 1.315743]], [[-0.245, 1.761, 12.022, 1.62, 0.2, 9], [-0.195747, 1.786149]], [[-1.034, -1.918, 17.266, -3.0, 0.05, 0], [-1.034, -1.918]], [[-0.029, -0.774, 20.819, -3.0, 0.01, 6], [-0.06843, -0.531674]], [[1.944, -1.577, 8.082, -3.0, 0.05, 4], [1.39648, -3.731695]], [[-1.606, 1.339, 5.46, 0.0, 0.2, 9], [0.197038, -3.860859]]], [[[0.95, -1.335, 21.477, 1.62, 0.05, 13], [-1.128613, 0.748224]], [[0.489, 1.733, 26.727, 0.0, 0.1, 9], [-0.349993, 2.431476]], [[1.558, -0.861, 10.719, -3.0, 0.05, 7], [0.564719, -4.155803]], [[0.416, 1.191, 1.568, -3.0, 0.05, 19], [2.239843, 2.182456]], [[-0.515, 0.855, 16.541, 0.0, 0.1, 3], [0.0295, 2.21876]], [[-0.028, -0.375, 18.368, -3.0, 0.01, 13], [-0.045341, 0.115239]], [[-0.325, 0.3, 29.992, 9.81, 0.05, 7], [-0.275898, -0.114066]], [[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.037222, -0.774603] | [1.029987, -2.221178] | Failed |
| case 1 | [-0.577279, -0.8735] | [-0.577279, -0.8735] | Passed |
| case 2 | [-0.602994, 1.443793] | [-0.62579, 0.306844] | Failed |
| case 3 | [0.398135, 4.002926] | [0.355353, 1.153028] | Failed |
| case 4 | [8.543501, -38.145609] | [-0.668425, 3.776016] | Failed |
| case 5 | [-1.521, -0.162] | [-1.521, -0.162] | Passed |
| case 6 | [-1.76028, 5.488339] | [-1.821517, 3.460132] | Failed |
| case 7 | [1.649177, -2.202393] | [1.649177, -2.202393] | Passed |
SHA-256 / ed535446eee16a39d26c9d119640e4aa032d544fada71fdaaf8e2ab56c7bd623
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.051, -1.98, 13.751, 9.81, 0.01, 1], [1.029987, -2.221178]], [[-0.493, -1.517, 17.061, 0.0, 0.01, 7], [-0.577279, -0.8735]], [[-0.631, -0.047, 16.626, 1.62, 0.01, 4], [-0.62579, 0.306844]], [[0.315, 1.535, 8.697, 9.81, 0.01, 3], [0.355353, 1.153028]], [[0.436, 1.25, 21.45, 9.81, 0.2, 5], [-0.668425, 3.776016]], [[-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.7, 0.206, 28.764, 0.0, 0.05, 1], [1.649177, -2.202393]]], [[[-1.873, -1.269, 8.691, 9.81, 0.1, 5], [-1.630061, 2.045253]], [[-0.281, 1.62, 5.784, 9.81, 0.1, 3], [-0.186184, -1.021531]], [[-1.933, 0.742, 17.091, 9.81, 0.1, 0], [-1.933, 0.742]], [[1.251, -1.263, 11.392, 0.0, 0.2, 1], [0.713372, -3.500813]], [[-1.512, -0.917, 2.156, -3.0, 0.1, 10], [0.475297, 4.13743]], [[0.207, 0.644, 21.918, 1.62, 0.05, 6], [0.108366, -1.184217]], [[-1.537, -0.458, 19.191, 0.0, 0.1, 1], [-1.435317, 2.394087]], [[-1.057, 0.244, 12.747, 9.81, 0.01, 8], [-1.026095, 0.5232]]], [[[1.893, -0.282, 22.513, 0.0, 0.01, 5], [1.825995, -2.384623]], [[-0.611, -0.509, 8.029, 0.0, 0.1, 0], [-0.611, -0.509]], [[-1.277, -0.541, 11.271, 1.62, 0.1, 4], [-0.552191, 3.54015]], [[1.361, 1.476, 18.508, -3.0, 0.1, 14], [1.259752, 2.50659]], [[0.191, -1.592, 15.855, -3.0, 0.2, 2], [-0.245776, 0.100628]], [[-0.665, -0.888, 20.198, -3.0, 0.01, 7], [-0.686227, 0.286733]], [[-1.003, 0.428, 3.825, -3.0, 0.2, 2], [-0.318898, 2.730839]], [[-0.73, 0.772, 21.128, 0.0, 0.01, 14], [-0.483162, 2.630496]]], [[[-0.163, -0.394, 12.347, 1.62, 0.2, 7], [-0.027135, -0.22127]], [[-0.299, 0.546, 29.023, 0.0, 0.1, 10], [-0.279669, -0.774088]], [[-0.731, -0.105, 23.504, 9.81, 0.2, 8], [-0.365533, 1.315743]], [[-0.245, 1.761, 12.022, 1.62, 0.2, 9], [-0.195747, 1.786149]], [[-1.034, -1.918, 17.266, -3.0, 0.05, 0], [-1.034, -1.918]], [[-0.029, -0.774, 20.819, -3.0, 0.01, 6], [-0.06843, -0.531674]], [[1.944, -1.577, 8.082, -3.0, 0.05, 4], [1.39648, -3.731695]], [[-1.606, 1.339, 5.46, 0.0, 0.2, 9], [0.197038, -3.860859]]], [[[0.95, -1.335, 21.477, 1.62, 0.05, 13], [-1.128613, 0.748224]], [[0.489, 1.733, 26.727, 0.0, 0.1, 9], [-0.349993, 2.431476]], [[1.558, -0.861, 10.719, -3.0, 0.05, 7], [0.564719, -4.155803]], [[0.416, 1.191, 1.568, -3.0, 0.05, 19], [2.239843, 2.182456]], [[-0.515, 0.855, 16.541, 0.0, 0.1, 3], [0.0295, 2.21876]], [[-0.028, -0.375, 18.368, -3.0, 0.01, 13], [-0.045341, 0.115239]], [[-0.325, 0.3, 29.992, 9.81, 0.05, 7], [-0.275898, -0.114066]], [[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.029987, -2.221178] | [1.029987, -2.221178] | Passed |
| case 1 | [-0.577279, -0.8735] | [-0.577279, -0.8735] | Passed |
| case 2 | [-0.62579, 0.306844] | [-0.62579, 0.306844] | Passed |
| case 3 | [0.355353, 1.153028] | [0.355353, 1.153028] | Passed |
| case 4 | [-0.668425, 3.776016] | [-0.668425, 3.776016] | Passed |
| case 5 | [-1.521, -0.162] | [-1.521, -0.162] | Passed |
| case 6 | [-1.821517, 3.460132] | [-1.821517, 3.460132] | Passed |
| case 7 | [1.649177, -2.202393] | [1.649177, -2.202393] | Passed |
SHA-256 / 4ed8f82c2c2bacdff5b3f3593754feb0fef8b52815a0c7e3a777e57f241aa845
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.417247+00:00.
Case digest / 001a4e6dc449c1331cf029e7cc7ca686a867d650b71a9665697eaabc3348c22c