FA-87121 / Physics integrator stability / Open access
Backward Euler numerator applies the spring force with the wrong sign · case 01
The implicit solve pushes the mass away from rest.
ROOT CAUSE
The numerator is v+dt*k*x/m.
VERIFIED REPAIR
Use v-dt*k*x/m.
Unsuccessful approach: Dropping the mass division scales the spring force wrongly.
Case contract
solve(x0, v0, k, m, c, dt, steps): backward Euler for x'=v, m v'=-k x-c v in closed form v1=(v-dt*k*x/m)/(1+dt*c/m+dt^2*k/m), x1=x+dt*v1. Energy E=0.5 m v^2+0.5 k x^2; report the peak E_n/E_0 including step 0 (0 when E_0=0). Return [x, v, peak] 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, k, m, c, dt, steps):
x, v = x0, v0
def energy(p, q):
return 0.5 * m * q * q + 0.5 * k * p * p
e0 = energy(x, v)
peak = 1.0 if e0 > 0 else 0.0
for _ in range(steps):
v = (v + dt * k * x / m) / (1 + dt * c / m + dt * dt * k / m)
x = x + dt * v
if e0 > 0:
peak = max(peak, energy(x, v) / e0)
return [round(x, 6), round(v, 6), round(peak, 6)]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[[[0.191, 1.945, 34.85, 2.123, 1.81, 0.5, 2], [0.064229, -0.321773, 1.0]], [[-1.795, -0.563, 97.971, 1.205, 2.366, 0.01, 5], [-1.618428, 6.062195, 1.0]], [[0.0, 0.0, 5.0, 1.0, 0.5, 0.1, 3], [0.0, 0.0, 0.0]], [[-0.221, -1.064, 98.514, 1.675, 2.729, 0.05, 6], [-0.014738, 1.136191, 1.0]], [[1.261, 0.83, 48.017, 0.99, 1.646, 0.1, 5], [-0.320762, -0.982525, 1.0]], [[-1.173, 0.504, 23.099, 0.515, 1.355, 0.01, 5], [-1.077545, 2.77492, 1.0]], [[-0.792, 1.489, 8.768, 2.262, 2.81, 0.01, 2], [-0.761883, 1.511237, 1.0]], [[-1.37, -0.31, 86.176, 2.116, 2.597, 0.05, 3], [-0.794438, 5.334862, 1.0]]], [[[0.194, -0.92, 25.988, 2.712, 0.886, 0.1, 8], [-0.209628, 0.163927, 1.0]], [[1.723, -0.188, 11.545, 1.405, 2.146, 0.01, 2], [1.715173, -0.458297, 1.0]], [[-0.645, 1.055, 48.194, 1.466, 2.383, 0.01, 5], [-0.565671, 1.91353, 1.0]], [[0.421, 0.744, 85.01, 1.471, 2.008, 0.05, 3], [0.24471, -1.858224, 1.0]], [[0.285, 0.068, 25.144, 2.544, 0.635, 0.05, 8], [0.107212, -0.715572, 1.0]], [[-0.368, -0.287, 23.934, 2.087, 1.05, 0.05, 2], [-0.364916, 0.133775, 1.0]], [[0.0, 0.0, 5.0, 1.0, 0.5, 0.1, 3], [0.0, 0.0, 0.0]], [[-0.519, -0.968, 62.61, 2.039, 0.152, 0.1, 6], [0.217976, 0.56902, 1.0]]], [[[-0.504, 1.004, 53.161, 1.37, 0.128, 0.01, 2], [-0.478281, 1.378125, 1.0]], [[0.0, 0.0, 5.0, 1.0, 0.5, 0.1, 3], [0.0, 0.0, 0.0]], [[-1.291, 0.844, 51.11, 0.463, 2.435, 0.1, 3], [0.082694, 2.804545, 1.0]], [[-1.724, -0.957, 58.152, 0.412, 2.15, 0.01, 9], [-0.983354, 13.094621, 1.0]], [[1.295, 1.182, 85.267, 2.457, 0.514, 0.5, 4], [0.000372, 0.08085, 1.0]], [[-1.201, -0.558, 89.531, 1.586, 0.182, 0.1, 5], [0.386107, -0.013533, 1.0]], [[-0.643, 1.644, 71.947, 1.526, 2.926, 0.01, 9], [-0.398185, 3.378185, 1.0]], [[-1.096, -0.407, 20.232, 0.596, 1.678, 0.1, 9], [0.100413, -0.614384, 1.0]]], [[[0.0, 0.0, 5.0, 1.0, 0.5, 0.1, 3], [0.0, 0.0, 0.0]], [[-0.969, 1.013, 41.406, 1.249, 0.992, 0.1, 10], [-0.075433, -0.873216, 1.0]], [[0.283, 1.441, 38.896, 1.952, 0.391, 0.05, 8], [0.217372, -1.212269, 1.0]], [[0.48, 0.698, 50.79, 1.825, 2.065, 0.05, 5], [0.243232, -1.683845, 1.0]], [[1.258, 0.72, 24.592, 2.071, 0.163, 0.5, 3], [-0.155797, -0.107234, 1.0]], [[-1.031, -0.971, 69.316, 1.698, 1.184, 0.01, 12], [-0.833114, 3.639745, 1.0]], [[0.777, 1.994, 57.363, 2.434, 0.585, 0.01, 3], [0.82519, 1.409422, 1.0]], [[-0.659, 0.567, 60.81, 2.3, 1.191, 0.5, 12], [-0.0, 1.4e-05, 1.0]]], [[[-1.133, 1.016, 46.913, 2.142, 2.129, 0.05, 9], [0.363941, 2.748968, 1.0]], [[0.539, 1.387, 93.276, 0.984, 1.857, 0.5, 7], [-4e-06, -4.4e-05, 1.0]], [[-1.397, -0.537, 14.554, 2.905, 0.576, 0.1, 10], [0.392502, 2.064847, 1.0]], [[0.537, 1.491, 17.106, 2.255, 2.958, 0.05, 7], [0.657535, -0.41846, 1.0]], [[1.48, -1.5, 47.647, 0.462, 0.24, 0.5, 7], [-1.4e-05, 2e-05, 1.0]], [[0.544, 1.609, 65.688, 1.374, 0.719, 0.01, 10], [0.552951, -1.128422, 1.0]], [[0.0, 0.0, 5.0, 1.0, 0.5, 0.1, 3], [0.0, 0.0, 0.0]], [[-1.039, -1.09, 32.245, 0.581, 1.221, 0.1, 8], [0.030932, -0.820883, 1.0]]]]
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.943445, 0.869702, 3.507075] | [0.064229, -0.321773, 1.0] | Failed |
| case 1 | [-2.031096, -7.476512, 1.491938] | [-1.618428, 6.062195, 1.0] | Failed |
| case 2 | [0.0, 0.0, 0.0] | [0.0, 0.0, 0.0] | Passed |
| case 3 | [-1.141578, -5.461462, 26.587722] | [-0.014738, 1.136191, 1.0] | Failed |
| case 4 | [8.0038, 26.42678, 48.90513] | [-0.320762, -0.982525, 1.0] | Failed |
| case 5 | [-1.223892, -1.993419, 1.148318] | [-1.077545, 2.77492, 1.0] | Failed |
| case 6 | [-0.763683, 1.391884, 1.0] | [-0.761883, 1.511237, 1.0] | Failed |
| case 7 | [-2.114103, -7.294553, 3.073542] | [-0.794438, 5.334862, 1.0] | Failed |
SHA-256 / f4ad7c0d042431d933ea6e39cd9df07864f37d3658b479f27bec3fac252ae290
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, k, m, c, dt, steps):
x, v = x0, v0
def energy(p, q):
return 0.5 * m * q * q + 0.5 * k * p * p
e0 = energy(x, v)
peak = 1.0 if e0 > 0 else 0.0
for _ in range(steps):
v = (v - dt * k * x) / (1 + dt * c / m + dt * dt * k / m)
x = x + dt * v
if e0 > 0:
peak = max(peak, energy(x, v) / e0)
return [round(x, 6), round(v, 6), round(peak, 6)]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[[[0.191, 1.945, 34.85, 2.123, 1.81, 0.5, 2], [0.064229, -0.321773, 1.0]], [[-1.795, -0.563, 97.971, 1.205, 2.366, 0.01, 5], [-1.618428, 6.062195, 1.0]], [[0.0, 0.0, 5.0, 1.0, 0.5, 0.1, 3], [0.0, 0.0, 0.0]], [[-0.221, -1.064, 98.514, 1.675, 2.729, 0.05, 6], [-0.014738, 1.136191, 1.0]], [[1.261, 0.83, 48.017, 0.99, 1.646, 0.1, 5], [-0.320762, -0.982525, 1.0]], [[-1.173, 0.504, 23.099, 0.515, 1.355, 0.01, 5], [-1.077545, 2.77492, 1.0]], [[-0.792, 1.489, 8.768, 2.262, 2.81, 0.01, 2], [-0.761883, 1.511237, 1.0]], [[-1.37, -0.31, 86.176, 2.116, 2.597, 0.05, 3], [-0.794438, 5.334862, 1.0]]], [[[0.194, -0.92, 25.988, 2.712, 0.886, 0.1, 8], [-0.209628, 0.163927, 1.0]], [[1.723, -0.188, 11.545, 1.405, 2.146, 0.01, 2], [1.715173, -0.458297, 1.0]], [[-0.645, 1.055, 48.194, 1.466, 2.383, 0.01, 5], [-0.565671, 1.91353, 1.0]], [[0.421, 0.744, 85.01, 1.471, 2.008, 0.05, 3], [0.24471, -1.858224, 1.0]], [[0.285, 0.068, 25.144, 2.544, 0.635, 0.05, 8], [0.107212, -0.715572, 1.0]], [[-0.368, -0.287, 23.934, 2.087, 1.05, 0.05, 2], [-0.364916, 0.133775, 1.0]], [[0.0, 0.0, 5.0, 1.0, 0.5, 0.1, 3], [0.0, 0.0, 0.0]], [[-0.519, -0.968, 62.61, 2.039, 0.152, 0.1, 6], [0.217976, 0.56902, 1.0]]], [[[-0.504, 1.004, 53.161, 1.37, 0.128, 0.01, 2], [-0.478281, 1.378125, 1.0]], [[0.0, 0.0, 5.0, 1.0, 0.5, 0.1, 3], [0.0, 0.0, 0.0]], [[-1.291, 0.844, 51.11, 0.463, 2.435, 0.1, 3], [0.082694, 2.804545, 1.0]], [[-1.724, -0.957, 58.152, 0.412, 2.15, 0.01, 9], [-0.983354, 13.094621, 1.0]], [[1.295, 1.182, 85.267, 2.457, 0.514, 0.5, 4], [0.000372, 0.08085, 1.0]], [[-1.201, -0.558, 89.531, 1.586, 0.182, 0.1, 5], [0.386107, -0.013533, 1.0]], [[-0.643, 1.644, 71.947, 1.526, 2.926, 0.01, 9], [-0.398185, 3.378185, 1.0]], [[-1.096, -0.407, 20.232, 0.596, 1.678, 0.1, 9], [0.100413, -0.614384, 1.0]]], [[[0.0, 0.0, 5.0, 1.0, 0.5, 0.1, 3], [0.0, 0.0, 0.0]], [[-0.969, 1.013, 41.406, 1.249, 0.992, 0.1, 10], [-0.075433, -0.873216, 1.0]], [[0.283, 1.441, 38.896, 1.952, 0.391, 0.05, 8], [0.217372, -1.212269, 1.0]], [[0.48, 0.698, 50.79, 1.825, 2.065, 0.05, 5], [0.243232, -1.683845, 1.0]], [[1.258, 0.72, 24.592, 2.071, 0.163, 0.5, 3], [-0.155797, -0.107234, 1.0]], [[-1.031, -0.971, 69.316, 1.698, 1.184, 0.01, 12], [-0.833114, 3.639745, 1.0]], [[0.777, 1.994, 57.363, 2.434, 0.585, 0.01, 3], [0.82519, 1.409422, 1.0]], [[-0.659, 0.567, 60.81, 2.3, 1.191, 0.5, 12], [-0.0, 1.4e-05, 1.0]]], [[[-1.133, 1.016, 46.913, 2.142, 2.129, 0.05, 9], [0.363941, 2.748968, 1.0]], [[0.539, 1.387, 93.276, 0.984, 1.857, 0.5, 7], [-4e-06, -4.4e-05, 1.0]], [[-1.397, -0.537, 14.554, 2.905, 0.576, 0.1, 10], [0.392502, 2.064847, 1.0]], [[0.537, 1.491, 17.106, 2.255, 2.958, 0.05, 7], [0.657535, -0.41846, 1.0]], [[1.48, -1.5, 47.647, 0.462, 0.24, 0.5, 7], [-1.4e-05, 2e-05, 1.0]], [[0.544, 1.609, 65.688, 1.374, 0.719, 0.01, 10], [0.552951, -1.128422, 1.0]], [[0.0, 0.0, 5.0, 1.0, 0.5, 0.1, 3], [0.0, 0.0, 0.0]], [[-1.039, -1.09, 32.245, 0.581, 1.221, 0.1, 8], [0.030932, -0.820883, 1.0]]]]
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.060561, -0.253006, 1.0] | [0.064229, -0.321773, 1.0] | Failed |
| case 1 | [-1.577839, 7.353168, 1.0] | [-1.618428, 6.062195, 1.0] | Failed |
| case 2 | [0.0, 0.0, 0.0] | [0.0, 0.0, 0.0] | Passed |
| case 3 | [0.092256, 0.947269, 1.0] | [-0.014738, 1.136191, 1.0] | Failed |
| case 4 | [-0.317004, -1.020761, 1.0] | [-0.320762, -0.982525, 1.0] | Failed |
| case 5 | [-1.112486, 1.649625, 1.0] | [-1.077545, 2.77492, 1.0] | Failed |
| case 6 | [-0.760748, 1.586501, 1.024043] | [-0.761883, 1.511237, 1.0] | Failed |
| case 7 | [-0.223925, 9.794906, 1.280236] | [-0.794438, 5.334862, 1.0] | Failed |
SHA-256 / 22e47d1dedf793536a2a1645f2056507fd4adfe08ad8f27f2f004ba30aa56a12
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, k, m, c, dt, steps):
x, v = x0, v0
def energy(p, q):
return 0.5 * m * q * q + 0.5 * k * p * p
e0 = energy(x, v)
peak = 1.0 if e0 > 0 else 0.0
for _ in range(steps):
v = (v - dt * k * x / m) / (1 + dt * c / m + dt * dt * k / m)
x = x + dt * v
if e0 > 0:
peak = max(peak, energy(x, v) / e0)
return [round(x, 6), round(v, 6), round(peak, 6)]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[[[0.191, 1.945, 34.85, 2.123, 1.81, 0.5, 2], [0.064229, -0.321773, 1.0]], [[-1.795, -0.563, 97.971, 1.205, 2.366, 0.01, 5], [-1.618428, 6.062195, 1.0]], [[0.0, 0.0, 5.0, 1.0, 0.5, 0.1, 3], [0.0, 0.0, 0.0]], [[-0.221, -1.064, 98.514, 1.675, 2.729, 0.05, 6], [-0.014738, 1.136191, 1.0]], [[1.261, 0.83, 48.017, 0.99, 1.646, 0.1, 5], [-0.320762, -0.982525, 1.0]], [[-1.173, 0.504, 23.099, 0.515, 1.355, 0.01, 5], [-1.077545, 2.77492, 1.0]], [[-0.792, 1.489, 8.768, 2.262, 2.81, 0.01, 2], [-0.761883, 1.511237, 1.0]], [[-1.37, -0.31, 86.176, 2.116, 2.597, 0.05, 3], [-0.794438, 5.334862, 1.0]]], [[[0.194, -0.92, 25.988, 2.712, 0.886, 0.1, 8], [-0.209628, 0.163927, 1.0]], [[1.723, -0.188, 11.545, 1.405, 2.146, 0.01, 2], [1.715173, -0.458297, 1.0]], [[-0.645, 1.055, 48.194, 1.466, 2.383, 0.01, 5], [-0.565671, 1.91353, 1.0]], [[0.421, 0.744, 85.01, 1.471, 2.008, 0.05, 3], [0.24471, -1.858224, 1.0]], [[0.285, 0.068, 25.144, 2.544, 0.635, 0.05, 8], [0.107212, -0.715572, 1.0]], [[-0.368, -0.287, 23.934, 2.087, 1.05, 0.05, 2], [-0.364916, 0.133775, 1.0]], [[0.0, 0.0, 5.0, 1.0, 0.5, 0.1, 3], [0.0, 0.0, 0.0]], [[-0.519, -0.968, 62.61, 2.039, 0.152, 0.1, 6], [0.217976, 0.56902, 1.0]]], [[[-0.504, 1.004, 53.161, 1.37, 0.128, 0.01, 2], [-0.478281, 1.378125, 1.0]], [[0.0, 0.0, 5.0, 1.0, 0.5, 0.1, 3], [0.0, 0.0, 0.0]], [[-1.291, 0.844, 51.11, 0.463, 2.435, 0.1, 3], [0.082694, 2.804545, 1.0]], [[-1.724, -0.957, 58.152, 0.412, 2.15, 0.01, 9], [-0.983354, 13.094621, 1.0]], [[1.295, 1.182, 85.267, 2.457, 0.514, 0.5, 4], [0.000372, 0.08085, 1.0]], [[-1.201, -0.558, 89.531, 1.586, 0.182, 0.1, 5], [0.386107, -0.013533, 1.0]], [[-0.643, 1.644, 71.947, 1.526, 2.926, 0.01, 9], [-0.398185, 3.378185, 1.0]], [[-1.096, -0.407, 20.232, 0.596, 1.678, 0.1, 9], [0.100413, -0.614384, 1.0]]], [[[0.0, 0.0, 5.0, 1.0, 0.5, 0.1, 3], [0.0, 0.0, 0.0]], [[-0.969, 1.013, 41.406, 1.249, 0.992, 0.1, 10], [-0.075433, -0.873216, 1.0]], [[0.283, 1.441, 38.896, 1.952, 0.391, 0.05, 8], [0.217372, -1.212269, 1.0]], [[0.48, 0.698, 50.79, 1.825, 2.065, 0.05, 5], [0.243232, -1.683845, 1.0]], [[1.258, 0.72, 24.592, 2.071, 0.163, 0.5, 3], [-0.155797, -0.107234, 1.0]], [[-1.031, -0.971, 69.316, 1.698, 1.184, 0.01, 12], [-0.833114, 3.639745, 1.0]], [[0.777, 1.994, 57.363, 2.434, 0.585, 0.01, 3], [0.82519, 1.409422, 1.0]], [[-0.659, 0.567, 60.81, 2.3, 1.191, 0.5, 12], [-0.0, 1.4e-05, 1.0]]], [[[-1.133, 1.016, 46.913, 2.142, 2.129, 0.05, 9], [0.363941, 2.748968, 1.0]], [[0.539, 1.387, 93.276, 0.984, 1.857, 0.5, 7], [-4e-06, -4.4e-05, 1.0]], [[-1.397, -0.537, 14.554, 2.905, 0.576, 0.1, 10], [0.392502, 2.064847, 1.0]], [[0.537, 1.491, 17.106, 2.255, 2.958, 0.05, 7], [0.657535, -0.41846, 1.0]], [[1.48, -1.5, 47.647, 0.462, 0.24, 0.5, 7], [-1.4e-05, 2e-05, 1.0]], [[0.544, 1.609, 65.688, 1.374, 0.719, 0.01, 10], [0.552951, -1.128422, 1.0]], [[0.0, 0.0, 5.0, 1.0, 0.5, 0.1, 3], [0.0, 0.0, 0.0]], [[-1.039, -1.09, 32.245, 0.581, 1.221, 0.1, 8], [0.030932, -0.820883, 1.0]]]]
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.064229, -0.321773, 1.0] | [0.064229, -0.321773, 1.0] | Passed |
| case 1 | [-1.618428, 6.062195, 1.0] | [-1.618428, 6.062195, 1.0] | Passed |
| case 2 | [0.0, 0.0, 0.0] | [0.0, 0.0, 0.0] | Passed |
| case 3 | [-0.014738, 1.136191, 1.0] | [-0.014738, 1.136191, 1.0] | Passed |
| case 4 | [-0.320762, -0.982525, 1.0] | [-0.320762, -0.982525, 1.0] | Passed |
| case 5 | [-1.077545, 2.77492, 1.0] | [-1.077545, 2.77492, 1.0] | Passed |
| case 6 | [-0.761883, 1.511237, 1.0] | [-0.761883, 1.511237, 1.0] | Passed |
| case 7 | [-0.794438, 5.334862, 1.0] | [-0.794438, 5.334862, 1.0] | Passed |
SHA-256 / 440f1e36add67f97885b3fcb18f14d16717ec6a13a966c8d69e63afed96a0b08
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.915442+00:00.
Case digest / fec893d6fa92a93a00e3450bfd5253f1367375ad603b5b60aa76ccaf89d3c074