FA-87101 / Physics integrator stability / Open access
Backward Euler drift uses the old velocity · case 01
Position lags the implicit velocity and energy is no longer monotone.
ROOT CAUSE
x is advanced with the velocity from before the implicit solve.
VERIFIED REPAIR
Update x with the new velocity v_{n+1}.
Unsuccessful approach: Averaging old and new velocity gives trapezoidal drift, not backward Euler.
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):
vn = (v - dt * k * x / m) / (1 + dt * c / m + dt * dt * k / m)
x = x + dt * v
v = vn
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.83, -0.911, 78.986, 0.479, 0.776, 0.01, 5], [-0.685196, 5.17674, 1.0]], [[-0.669, 1.058, 55.913, 1.953, 2.159, 0.01, 1], [-0.656676, 1.232378, 1.0]], [[1.633, -0.225, 42.999, 2.61, 2.771, 0.5, 1], [0.422579, -2.420843, 1.0]], [[-0.221, -1.064, 98.514, 1.675, 2.729, 0.05, 6], [-0.014738, 1.136191, 1.0]], [[0.191, 1.945, 34.85, 2.123, 1.81, 0.5, 2], [0.064229, -0.321773, 1.0]], [[1.095, -1.653, 34.425, 1.885, 0.995, 0.01, 2], [1.056379, -2.022152, 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]]], [[[1.744, 0.001, 28.749, 1.845, 0.034, 0.1, 8], [-0.961649, -0.521718, 1.0]], [[-0.519, -0.968, 62.61, 2.039, 0.152, 0.1, 6], [0.217976, 0.56902, 1.0]], [[-1.228, -0.451, 23.36, 2.048, 1.522, 0.01, 2], [-1.232747, -0.166441, 1.0]], [[-1.497, 1.436, 6.083, 1.506, 1.735, 0.01, 3], [-1.451394, 1.561208, 1.0]], [[0.0, 0.0, 5.0, 1.0, 0.5, 0.1, 3], [0.0, 0.0, 0.0]], [[0.194, -0.92, 25.988, 2.712, 0.886, 0.1, 8], [-0.209628, 0.163927, 1.0]], [[-1.267, -0.184, 5.424, 1.543, 1.623, 0.01, 7], [-1.267247, 0.128498, 1.0]], [[-1.808, -1.676, 75.639, 1.772, 1.857, 0.5, 4], [0.001582, -0.08128, 1.0]]], [[[-0.632, -0.905, 99.547, 0.843, 1.221, 0.1, 2], [-0.051052, 2.989657, 1.0]], [[1.659, 0.2, 31.533, 0.495, 2.184, 0.1, 3], [0.104813, -4.59631, 1.0]], [[0.0, 0.0, 5.0, 1.0, 0.5, 0.1, 3], [0.0, 0.0, 0.0]], [[-1.724, -0.957, 58.152, 0.412, 2.15, 0.01, 9], [-0.983354, 13.094621, 1.0]], [[-0.643, 1.644, 71.947, 1.526, 2.926, 0.01, 9], [-0.398185, 3.378185, 1.0]], [[-1.649, 1.049, 22.524, 1.01, 0.758, 0.5, 10], [-1e-05, -0.000474, 1.0]], [[-1.79, 0.797, 47.159, 1.773, 0.615, 0.5, 8], [0.000479, -8.8e-05, 1.0]], [[0.655, -1.599, 73.17, 2.877, 2.964, 0.05, 4], [0.136804, -2.81673, 1.0]]], [[[0.48, 0.698, 50.79, 1.825, 2.065, 0.05, 5], [0.243232, -1.683845, 1.0]], [[-0.888, 1.687, 17.402, 1.095, 2.649, 0.5, 8], [-0.000407, 0.002256, 1.0]], [[-0.828, -0.185, 67.12, 0.517, 2.786, 0.5, 10], [-0.0, -0.0, 1.0]], [[-1.031, -0.971, 69.316, 1.698, 1.184, 0.01, 12], [-0.833114, 3.639745, 1.0]], [[-1.767, -1.709, 7.751, 0.748, 0.809, 0.5, 5], [0.035361, -0.158519, 1.0]], [[-1.413, -1.368, 50.375, 1.771, 2.743, 0.1, 11], [0.00437, -1.062657, 1.0]], [[1.425, -1.275, 29.071, 2.949, 0.195, 0.05, 2], [1.203396, -2.508465, 1.0]], [[0.0, 0.0, 5.0, 1.0, 0.5, 0.1, 3], [0.0, 0.0, 0.0]]], [[[1.48, -1.5, 47.647, 0.462, 0.24, 0.5, 7], [-1.4e-05, 2e-05, 1.0]], [[-1.472, -1.496, 89.833, 2.285, 2.932, 0.5, 13], [0.0, 1e-06, 1.0]], [[0.001, -0.283, 51.629, 2.856, 1.037, 0.01, 3], [-0.007389, -0.277456, 1.0]], [[0.681, 1.217, 45.377, 0.552, 0.71, 0.01, 8], [0.582858, -2.950135, 1.0]], [[0.0, 0.0, 5.0, 1.0, 0.5, 0.1, 3], [0.0, 0.0, 0.0]], [[-1.006, -1.056, 26.158, 1.366, 2.283, 0.05, 6], [-0.525039, 2.790185, 1.0]], [[1.452, 1.003, 62.635, 0.47, 0.091, 0.01, 7], [1.023334, -10.790614, 1.0]], [[0.544, 1.609, 65.688, 1.374, 0.719, 0.01, 10], [0.552951, -1.128422, 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.743991, 5.375192, 1.050157] | [-0.685196, 5.17674, 1.0] | Failed |
| case 1 | [-0.65842, 1.232378, 1.0] | [-0.656676, 1.232378, 1.0] | Failed |
| case 2 | [1.5205, -2.420843, 1.0] | [0.422579, -2.420843, 1.0] | Failed |
| case 3 | [-0.061614, 1.760867, 1.339749] | [-0.014738, 1.136191, 1.0] | Failed |
| case 4 | [1.197615, -1.714504, 6.043965] | [0.064229, -0.321773, 1.0] | Failed |
| case 5 | [1.060071, -2.022491, 1.0] | [1.056379, -2.022152, 1.0] | Failed |
| case 6 | [-1.683579, 6.168319, 1.023711] | [-1.618428, 6.062195, 1.0] | Failed |
| case 7 | [0.0, 0.0, 0.0] | [0.0, 0.0, 0.0] | Passed |
SHA-256 / e05bdcf7776500a8ebe9daebe639ce2b1d75dc3d56b5156d956e995d3bb7b43f
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):
vn = (v - dt * k * x / m) / (1 + dt * c / m + dt * dt * k / m)
x = x + dt * (v + vn) / 2
v = vn
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.83, -0.911, 78.986, 0.479, 0.776, 0.01, 5], [-0.685196, 5.17674, 1.0]], [[-0.669, 1.058, 55.913, 1.953, 2.159, 0.01, 1], [-0.656676, 1.232378, 1.0]], [[1.633, -0.225, 42.999, 2.61, 2.771, 0.5, 1], [0.422579, -2.420843, 1.0]], [[-0.221, -1.064, 98.514, 1.675, 2.729, 0.05, 6], [-0.014738, 1.136191, 1.0]], [[0.191, 1.945, 34.85, 2.123, 1.81, 0.5, 2], [0.064229, -0.321773, 1.0]], [[1.095, -1.653, 34.425, 1.885, 0.995, 0.01, 2], [1.056379, -2.022152, 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]]], [[[1.744, 0.001, 28.749, 1.845, 0.034, 0.1, 8], [-0.961649, -0.521718, 1.0]], [[-0.519, -0.968, 62.61, 2.039, 0.152, 0.1, 6], [0.217976, 0.56902, 1.0]], [[-1.228, -0.451, 23.36, 2.048, 1.522, 0.01, 2], [-1.232747, -0.166441, 1.0]], [[-1.497, 1.436, 6.083, 1.506, 1.735, 0.01, 3], [-1.451394, 1.561208, 1.0]], [[0.0, 0.0, 5.0, 1.0, 0.5, 0.1, 3], [0.0, 0.0, 0.0]], [[0.194, -0.92, 25.988, 2.712, 0.886, 0.1, 8], [-0.209628, 0.163927, 1.0]], [[-1.267, -0.184, 5.424, 1.543, 1.623, 0.01, 7], [-1.267247, 0.128498, 1.0]], [[-1.808, -1.676, 75.639, 1.772, 1.857, 0.5, 4], [0.001582, -0.08128, 1.0]]], [[[-0.632, -0.905, 99.547, 0.843, 1.221, 0.1, 2], [-0.051052, 2.989657, 1.0]], [[1.659, 0.2, 31.533, 0.495, 2.184, 0.1, 3], [0.104813, -4.59631, 1.0]], [[0.0, 0.0, 5.0, 1.0, 0.5, 0.1, 3], [0.0, 0.0, 0.0]], [[-1.724, -0.957, 58.152, 0.412, 2.15, 0.01, 9], [-0.983354, 13.094621, 1.0]], [[-0.643, 1.644, 71.947, 1.526, 2.926, 0.01, 9], [-0.398185, 3.378185, 1.0]], [[-1.649, 1.049, 22.524, 1.01, 0.758, 0.5, 10], [-1e-05, -0.000474, 1.0]], [[-1.79, 0.797, 47.159, 1.773, 0.615, 0.5, 8], [0.000479, -8.8e-05, 1.0]], [[0.655, -1.599, 73.17, 2.877, 2.964, 0.05, 4], [0.136804, -2.81673, 1.0]]], [[[0.48, 0.698, 50.79, 1.825, 2.065, 0.05, 5], [0.243232, -1.683845, 1.0]], [[-0.888, 1.687, 17.402, 1.095, 2.649, 0.5, 8], [-0.000407, 0.002256, 1.0]], [[-0.828, -0.185, 67.12, 0.517, 2.786, 0.5, 10], [-0.0, -0.0, 1.0]], [[-1.031, -0.971, 69.316, 1.698, 1.184, 0.01, 12], [-0.833114, 3.639745, 1.0]], [[-1.767, -1.709, 7.751, 0.748, 0.809, 0.5, 5], [0.035361, -0.158519, 1.0]], [[-1.413, -1.368, 50.375, 1.771, 2.743, 0.1, 11], [0.00437, -1.062657, 1.0]], [[1.425, -1.275, 29.071, 2.949, 0.195, 0.05, 2], [1.203396, -2.508465, 1.0]], [[0.0, 0.0, 5.0, 1.0, 0.5, 0.1, 3], [0.0, 0.0, 0.0]]], [[[1.48, -1.5, 47.647, 0.462, 0.24, 0.5, 7], [-1.4e-05, 2e-05, 1.0]], [[-1.472, -1.496, 89.833, 2.285, 2.932, 0.5, 13], [0.0, 1e-06, 1.0]], [[0.001, -0.283, 51.629, 2.856, 1.037, 0.01, 3], [-0.007389, -0.277456, 1.0]], [[0.681, 1.217, 45.377, 0.552, 0.71, 0.01, 8], [0.582858, -2.950135, 1.0]], [[0.0, 0.0, 5.0, 1.0, 0.5, 0.1, 3], [0.0, 0.0, 0.0]], [[-1.006, -1.056, 26.158, 1.366, 2.283, 0.05, 6], [-0.525039, 2.790185, 1.0]], [[1.452, 1.003, 62.635, 0.47, 0.091, 0.01, 7], [1.023334, -10.790614, 1.0]], [[0.544, 1.609, 65.688, 1.374, 0.719, 0.01, 10], [0.552951, -1.128422, 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.714105, 5.275153, 1.000067] | [-0.685196, 5.17674, 1.0] | Failed |
| case 1 | [-0.657548, 1.232378, 1.0] | [-0.656676, 1.232378, 1.0] | Failed |
| case 2 | [0.971539, -2.420843, 1.0] | [0.422579, -2.420843, 1.0] | Failed |
| case 3 | [-0.032583, 1.419804, 1.0] | [-0.014738, 1.136191, 1.0] | Failed |
| case 4 | [0.456831, -1.018139, 1.806974] | [0.064229, -0.321773, 1.0] | Failed |
| case 5 | [1.058224, -2.022321, 1.0] | [1.056379, -2.022152, 1.0] | Failed |
| case 6 | [-1.65074, 6.115043, 1.0] | [-1.618428, 6.062195, 1.0] | Failed |
| case 7 | [0.0, 0.0, 0.0] | [0.0, 0.0, 0.0] | Passed |
SHA-256 / 17958eecfc04f8fb85cf546d938ab5b786f6c0711ef495af0486f9331393720c
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.83, -0.911, 78.986, 0.479, 0.776, 0.01, 5], [-0.685196, 5.17674, 1.0]], [[-0.669, 1.058, 55.913, 1.953, 2.159, 0.01, 1], [-0.656676, 1.232378, 1.0]], [[1.633, -0.225, 42.999, 2.61, 2.771, 0.5, 1], [0.422579, -2.420843, 1.0]], [[-0.221, -1.064, 98.514, 1.675, 2.729, 0.05, 6], [-0.014738, 1.136191, 1.0]], [[0.191, 1.945, 34.85, 2.123, 1.81, 0.5, 2], [0.064229, -0.321773, 1.0]], [[1.095, -1.653, 34.425, 1.885, 0.995, 0.01, 2], [1.056379, -2.022152, 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]]], [[[1.744, 0.001, 28.749, 1.845, 0.034, 0.1, 8], [-0.961649, -0.521718, 1.0]], [[-0.519, -0.968, 62.61, 2.039, 0.152, 0.1, 6], [0.217976, 0.56902, 1.0]], [[-1.228, -0.451, 23.36, 2.048, 1.522, 0.01, 2], [-1.232747, -0.166441, 1.0]], [[-1.497, 1.436, 6.083, 1.506, 1.735, 0.01, 3], [-1.451394, 1.561208, 1.0]], [[0.0, 0.0, 5.0, 1.0, 0.5, 0.1, 3], [0.0, 0.0, 0.0]], [[0.194, -0.92, 25.988, 2.712, 0.886, 0.1, 8], [-0.209628, 0.163927, 1.0]], [[-1.267, -0.184, 5.424, 1.543, 1.623, 0.01, 7], [-1.267247, 0.128498, 1.0]], [[-1.808, -1.676, 75.639, 1.772, 1.857, 0.5, 4], [0.001582, -0.08128, 1.0]]], [[[-0.632, -0.905, 99.547, 0.843, 1.221, 0.1, 2], [-0.051052, 2.989657, 1.0]], [[1.659, 0.2, 31.533, 0.495, 2.184, 0.1, 3], [0.104813, -4.59631, 1.0]], [[0.0, 0.0, 5.0, 1.0, 0.5, 0.1, 3], [0.0, 0.0, 0.0]], [[-1.724, -0.957, 58.152, 0.412, 2.15, 0.01, 9], [-0.983354, 13.094621, 1.0]], [[-0.643, 1.644, 71.947, 1.526, 2.926, 0.01, 9], [-0.398185, 3.378185, 1.0]], [[-1.649, 1.049, 22.524, 1.01, 0.758, 0.5, 10], [-1e-05, -0.000474, 1.0]], [[-1.79, 0.797, 47.159, 1.773, 0.615, 0.5, 8], [0.000479, -8.8e-05, 1.0]], [[0.655, -1.599, 73.17, 2.877, 2.964, 0.05, 4], [0.136804, -2.81673, 1.0]]], [[[0.48, 0.698, 50.79, 1.825, 2.065, 0.05, 5], [0.243232, -1.683845, 1.0]], [[-0.888, 1.687, 17.402, 1.095, 2.649, 0.5, 8], [-0.000407, 0.002256, 1.0]], [[-0.828, -0.185, 67.12, 0.517, 2.786, 0.5, 10], [-0.0, -0.0, 1.0]], [[-1.031, -0.971, 69.316, 1.698, 1.184, 0.01, 12], [-0.833114, 3.639745, 1.0]], [[-1.767, -1.709, 7.751, 0.748, 0.809, 0.5, 5], [0.035361, -0.158519, 1.0]], [[-1.413, -1.368, 50.375, 1.771, 2.743, 0.1, 11], [0.00437, -1.062657, 1.0]], [[1.425, -1.275, 29.071, 2.949, 0.195, 0.05, 2], [1.203396, -2.508465, 1.0]], [[0.0, 0.0, 5.0, 1.0, 0.5, 0.1, 3], [0.0, 0.0, 0.0]]], [[[1.48, -1.5, 47.647, 0.462, 0.24, 0.5, 7], [-1.4e-05, 2e-05, 1.0]], [[-1.472, -1.496, 89.833, 2.285, 2.932, 0.5, 13], [0.0, 1e-06, 1.0]], [[0.001, -0.283, 51.629, 2.856, 1.037, 0.01, 3], [-0.007389, -0.277456, 1.0]], [[0.681, 1.217, 45.377, 0.552, 0.71, 0.01, 8], [0.582858, -2.950135, 1.0]], [[0.0, 0.0, 5.0, 1.0, 0.5, 0.1, 3], [0.0, 0.0, 0.0]], [[-1.006, -1.056, 26.158, 1.366, 2.283, 0.05, 6], [-0.525039, 2.790185, 1.0]], [[1.452, 1.003, 62.635, 0.47, 0.091, 0.01, 7], [1.023334, -10.790614, 1.0]], [[0.544, 1.609, 65.688, 1.374, 0.719, 0.01, 10], [0.552951, -1.128422, 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.685196, 5.17674, 1.0] | [-0.685196, 5.17674, 1.0] | Passed |
| case 1 | [-0.656676, 1.232378, 1.0] | [-0.656676, 1.232378, 1.0] | Passed |
| case 2 | [0.422579, -2.420843, 1.0] | [0.422579, -2.420843, 1.0] | Passed |
| case 3 | [-0.014738, 1.136191, 1.0] | [-0.014738, 1.136191, 1.0] | Passed |
| case 4 | [0.064229, -0.321773, 1.0] | [0.064229, -0.321773, 1.0] | Passed |
| case 5 | [1.056379, -2.022152, 1.0] | [1.056379, -2.022152, 1.0] | Passed |
| case 6 | [-1.618428, 6.062195, 1.0] | [-1.618428, 6.062195, 1.0] | Passed |
| case 7 | [0.0, 0.0, 0.0] | [0.0, 0.0, 0.0] | Passed |
SHA-256 / 8b96d23f0754fa39d18634ee1ef1e184d95f43cb05c4309a4c2e16ebfd2e1973
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.714506+00:00.
Case digest / 909332626f4d2db4a34cffa3b457262642874dc3c342bc8f7e6582232ccb8d8f