FAILURE MAP
← Case archive

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.

Verified by executionVariant 1 · 8 checks per implementationDownload source bundle ↓JSON ↗

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 fixtureActualExpectedOutcome
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 fixtureActualExpectedOutcome
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 fixtureActualExpectedOutcome
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