FAILURE MAP
← Case archive

FA-87066 / Physics integrator stability / Open access

Rejected steps are always halved · case 01

Badly rejected steps retry too large and cause repeated rejections.

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

ROOT CAUSE

The reject branch multiplies h by 0.5 ignoring the error ratio.

VERIFIED REPAIR

Shrink by max(0.2, 0.9*(tol/err)^(1/2)).

Unsuccessful approach: A 0.5 floor prevents the controller from shrinking far enough.

Case contract

solve(y0, lam, s, t_end, dt0, tol): integrate y'=-lam*y+s to t_end with the embedded Euler/Heun pair. err=|y_heun-y_euler|/max(1,|y|) at the step start. Accept if err<=tol (propagate Heun); grow h by min(2, 0.9*(tol/err)^(1/2)) (2 when err=0); on reject shrink by max(0.2, 0.9*(tol/err)^(1/2)). Steps are clipped to t_end; at most 500 attempts. Return [y, accepted, rejected, last h] rounded.

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(y0, lam, s, t_end, dt0, tol):
    def f(y):
        return -lam * y + s
    t, y, h = 0.0, y0, dt0
    acc = rej = 0
    while t < t_end - 1e-12 and acc + rej < 500:
        h = min(h, t_end - t)
        k1 = f(y)
        k2 = f(y + h * k1)
        y_low = y + h * k1
        y_high = y + h / 2 * (k1 + k2)
        err = abs(y_high - y_low) / max(1.0, abs(y))
        if err <= tol:
            t += h
            y = y_high
            acc += 1
            grow = 2.0 if err == 0 else min(2.0, 0.9 * (tol / err) ** 0.5)
            h = h * grow
        else:
            rej += 1
            h = h * 0.5
    return [round(y, 6), acc, rej, round(h, 6)]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[[[-0.696, 4.83, -1.861, 0.5, 0.7, 0.0001], [-0.41309, 64, 3, 0.004672]], [[-0.952, 1.695, -1.777, 2.0, 0.3, 0.01], [-1.043824, 6, 1, 0.806875]], [[1.329, 2.639, 0.535, 1.5, 0.7, 0.01], [0.225936, 17, 2, 0.200122]], [[-0.678, 2.724, 0.55, 1.5, 0.05, 0.01], [0.185448, 16, 0, 0.164424]], [[-0.303, 0.644, 0.316, 1.5, 0.05, 0.01], [0.187358, 8, 0, 0.223575]], [[1.121, 4.954, -0.6, 0.5, 0.3, 0.01], [-0.014719, 14, 2, 0.068659]], [[-0.223, 4.209, -0.57, 1.5, 0.7, 0.01], [-0.136586, 8, 2, 0.375934]], [[-0.603, 0.0, 1.691, 2.0, 0.1, 0.0001], [2.779, 5, 0, 1.0]]], [[[0.248, 2.246, 0.565, 1.5, 0.7, 0.01], [0.249628, 2, 0, 1.166103]], [[2.431, 0.0, 0.71, 1.0, 0.1, 0.01], [3.141, 4, 0, 0.6]], [[-0.907, 0.0, 0.08, 2.0, 0.3, 0.0001], [-0.747, 3, 0, 2.2]], [[0.342, 2.406, 1.149, 1.0, 0.3, 0.001], [0.465139, 15, 2, 0.090253]], [[1.498, 1.599, -1.343, 1.5, 0.7, 0.0001], [-0.627468, 167, 3, 0.017123]], [[2.09, 3.725, -1.778, 1.0, 0.7, 0.0001], [-0.415384, 205, 4, 0.005852]], [[1.324, 1.927, -0.437, 2.0, 0.3, 0.001], [-0.193674, 55, 2, 0.105477]], [[-2.813, 4.676, -0.613, 1.0, 0.1, 0.01], [-0.158154, 23, 1, 0.132111]]], [[[-2.871, 0.0, 0.577, 0.5, 0.3, 0.001], [-2.5825, 2, 0, 0.4]], [[-0.304, 3.173, 1.878, 2.0, 0.1, 0.0001], [0.590274, 148, 2, 0.004498]], [[2.099, 1.458, -0.969, 2.0, 0.7, 0.01], [-0.512845, 21, 2, 0.154279]], [[-0.18, 0.0, 0.811, 2.0, 0.05, 0.0001], [1.442, 6, 0, 0.9]], [[1.402, 2.3, -1.252, 0.5, 0.1, 0.01], [0.073687, 10, 1, 0.066063]], [[1.749, 2.243, 1.405, 1.5, 0.05, 0.01], [0.667087, 15, 0, 0.241935]], [[2.731, 1.096, -1.616, 1.5, 0.1, 0.0001], [-0.661911, 164, 2, 0.005732]], [[-1.619, 0.0, 0.292, 1.0, 0.1, 0.0001], [-1.327, 4, 0, 0.6]]], [[[-2.345, 2.942, -1.888, 2.0, 0.7, 0.01], [-0.64795, 21, 2, 0.218574]], [[0.219, 2.157, 0.867, 1.0, 0.1, 0.01], [0.379141, 6, 0, 0.311548]], [[0.687, 3.619, 0.954, 0.5, 0.05, 0.01], [0.334636, 8, 0, 0.050261]], [[-1.101, 3.233, -1.581, 2.0, 0.1, 0.001], [-0.49013, 42, 2, 0.175182]], [[-2.44, 2.788, -0.813, 1.5, 0.7, 0.01], [-0.326363, 21, 2, 0.098371]], [[-2.521, 1.781, 1.018, 0.5, 0.3, 0.0001], [-0.697788, 83, 3, 0.000494]], [[-2.752, 2.034, 0.926, 1.5, 0.7, 0.001], [0.303269, 65, 3, 0.023728]], [[0.741, 0.0, 1.287, 1.5, 0.1, 0.001], [2.6715, 4, 0, 1.6]]], [[[-1.507, 4.389, 1.938, 2.0, 0.3, 0.01], [0.439365, 26, 2, 0.506375]], [[-0.306, 1.726, 0.514, 0.5, 0.3, 0.0001], [0.043044, 44, 3, 0.012094]], [[-1.261, 2.268, -0.675, 1.5, 0.3, 0.0001], [-0.329731, 128, 3, 0.007802]], [[1.532, 3.924, -1.443, 0.5, 0.7, 0.0001], [-0.100654, 134, 4, 0.001341]], [[2.878, 0.0, 1.169, 1.5, 0.7, 0.0001], [4.6315, 2, 0, 1.6]], [[-1.151, 1.35, -1.656, 0.5, 0.1, 0.001], [-1.188015, 5, 0, 0.046485]], [[1.917, 0.404, 1.543, 1.5, 0.1, 0.01], [2.779594, 6, 0, 0.239185]], [[2.057, 4.405, 0.797, 1.5, 0.1, 0.01], [0.185265, 23, 1, 0.310434]]]]
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.41309, 64, 7, 0.009813][-0.41309, 64, 3, 0.004672]Failed
case 1[-1.043398, 7, 1, 0.064187][-1.043824, 6, 1, 0.806875]Failed
case 2[0.225932, 17, 4, 0.264651][0.225936, 17, 2, 0.200122]Failed
case 3[0.185448, 16, 0, 0.164424][0.185448, 16, 0, 0.164424]Passed
case 4[0.187358, 8, 0, 0.223575][0.187358, 8, 0, 0.223575]Passed
case 5[-0.014772, 15, 4, 0.02169][-0.014719, 14, 2, 0.068659]Failed
case 6[-0.136567, 8, 3, 0.483913][-0.136586, 8, 2, 0.375934]Failed
case 7[2.779, 5, 0, 1.0][2.779, 5, 0, 1.0]Passed

SHA-256 / df196a32c0c1b264abe196ed187709195efa2d489f1030688acf6ffff38386f9

2 / The unsuccessful fix

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(y0, lam, s, t_end, dt0, tol):
    def f(y):
        return -lam * y + s
    t, y, h = 0.0, y0, dt0
    acc = rej = 0
    while t < t_end - 1e-12 and acc + rej < 500:
        h = min(h, t_end - t)
        k1 = f(y)
        k2 = f(y + h * k1)
        y_low = y + h * k1
        y_high = y + h / 2 * (k1 + k2)
        err = abs(y_high - y_low) / max(1.0, abs(y))
        if err <= tol:
            t += h
            y = y_high
            acc += 1
            grow = 2.0 if err == 0 else min(2.0, 0.9 * (tol / err) ** 0.5)
            h = h * grow
        else:
            rej += 1
            h = h * max(0.5, 0.9 * (tol / err) ** 0.5)
    return [round(y, 6), acc, rej, round(h, 6)]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[[[-0.696, 4.83, -1.861, 0.5, 0.7, 0.0001], [-0.41309, 64, 3, 0.004672]], [[-0.952, 1.695, -1.777, 2.0, 0.3, 0.01], [-1.043824, 6, 1, 0.806875]], [[1.329, 2.639, 0.535, 1.5, 0.7, 0.01], [0.225936, 17, 2, 0.200122]], [[-0.678, 2.724, 0.55, 1.5, 0.05, 0.01], [0.185448, 16, 0, 0.164424]], [[-0.303, 0.644, 0.316, 1.5, 0.05, 0.01], [0.187358, 8, 0, 0.223575]], [[1.121, 4.954, -0.6, 0.5, 0.3, 0.01], [-0.014719, 14, 2, 0.068659]], [[-0.223, 4.209, -0.57, 1.5, 0.7, 0.01], [-0.136586, 8, 2, 0.375934]], [[-0.603, 0.0, 1.691, 2.0, 0.1, 0.0001], [2.779, 5, 0, 1.0]]], [[[0.248, 2.246, 0.565, 1.5, 0.7, 0.01], [0.249628, 2, 0, 1.166103]], [[2.431, 0.0, 0.71, 1.0, 0.1, 0.01], [3.141, 4, 0, 0.6]], [[-0.907, 0.0, 0.08, 2.0, 0.3, 0.0001], [-0.747, 3, 0, 2.2]], [[0.342, 2.406, 1.149, 1.0, 0.3, 0.001], [0.465139, 15, 2, 0.090253]], [[1.498, 1.599, -1.343, 1.5, 0.7, 0.0001], [-0.627468, 167, 3, 0.017123]], [[2.09, 3.725, -1.778, 1.0, 0.7, 0.0001], [-0.415384, 205, 4, 0.005852]], [[1.324, 1.927, -0.437, 2.0, 0.3, 0.001], [-0.193674, 55, 2, 0.105477]], [[-2.813, 4.676, -0.613, 1.0, 0.1, 0.01], [-0.158154, 23, 1, 0.132111]]], [[[-2.871, 0.0, 0.577, 0.5, 0.3, 0.001], [-2.5825, 2, 0, 0.4]], [[-0.304, 3.173, 1.878, 2.0, 0.1, 0.0001], [0.590274, 148, 2, 0.004498]], [[2.099, 1.458, -0.969, 2.0, 0.7, 0.01], [-0.512845, 21, 2, 0.154279]], [[-0.18, 0.0, 0.811, 2.0, 0.05, 0.0001], [1.442, 6, 0, 0.9]], [[1.402, 2.3, -1.252, 0.5, 0.1, 0.01], [0.073687, 10, 1, 0.066063]], [[1.749, 2.243, 1.405, 1.5, 0.05, 0.01], [0.667087, 15, 0, 0.241935]], [[2.731, 1.096, -1.616, 1.5, 0.1, 0.0001], [-0.661911, 164, 2, 0.005732]], [[-1.619, 0.0, 0.292, 1.0, 0.1, 0.0001], [-1.327, 4, 0, 0.6]]], [[[-2.345, 2.942, -1.888, 2.0, 0.7, 0.01], [-0.64795, 21, 2, 0.218574]], [[0.219, 2.157, 0.867, 1.0, 0.1, 0.01], [0.379141, 6, 0, 0.311548]], [[0.687, 3.619, 0.954, 0.5, 0.05, 0.01], [0.334636, 8, 0, 0.050261]], [[-1.101, 3.233, -1.581, 2.0, 0.1, 0.001], [-0.49013, 42, 2, 0.175182]], [[-2.44, 2.788, -0.813, 1.5, 0.7, 0.01], [-0.326363, 21, 2, 0.098371]], [[-2.521, 1.781, 1.018, 0.5, 0.3, 0.0001], [-0.697788, 83, 3, 0.000494]], [[-2.752, 2.034, 0.926, 1.5, 0.7, 0.001], [0.303269, 65, 3, 0.023728]], [[0.741, 0.0, 1.287, 1.5, 0.1, 0.001], [2.6715, 4, 0, 1.6]]], [[[-1.507, 4.389, 1.938, 2.0, 0.3, 0.01], [0.439365, 26, 2, 0.506375]], [[-0.306, 1.726, 0.514, 0.5, 0.3, 0.0001], [0.043044, 44, 3, 0.012094]], [[-1.261, 2.268, -0.675, 1.5, 0.3, 0.0001], [-0.329731, 128, 3, 0.007802]], [[1.532, 3.924, -1.443, 0.5, 0.7, 0.0001], [-0.100654, 134, 4, 0.001341]], [[2.878, 0.0, 1.169, 1.5, 0.7, 0.0001], [4.6315, 2, 0, 1.6]], [[-1.151, 1.35, -1.656, 0.5, 0.1, 0.001], [-1.188015, 5, 0, 0.046485]], [[1.917, 0.404, 1.543, 1.5, 0.1, 0.01], [2.779594, 6, 0, 0.239185]], [[2.057, 4.405, 0.797, 1.5, 0.1, 0.01], [0.185265, 23, 1, 0.310434]]]]
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.41309, 64, 7, 0.004672][-0.41309, 64, 3, 0.004672]Failed
case 1[-1.043824, 6, 1, 0.806875][-1.043824, 6, 1, 0.806875]Passed
case 2[0.225936, 17, 4, 0.200122][0.225936, 17, 2, 0.200122]Failed
case 3[0.185448, 16, 0, 0.164424][0.185448, 16, 0, 0.164424]Passed
case 4[0.187358, 8, 0, 0.223575][0.187358, 8, 0, 0.223575]Passed
case 5[-0.014719, 14, 4, 0.068659][-0.014719, 14, 2, 0.068659]Failed
case 6[-0.136586, 8, 3, 0.375934][-0.136586, 8, 2, 0.375934]Failed
case 7[2.779, 5, 0, 1.0][2.779, 5, 0, 1.0]Passed

SHA-256 / 684d9108f52223523d1c8dbaf71b1583c22d862599e1adc08c4c6fba38cb47c5

3 / The verified repair

Exit 0
"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(y0, lam, s, t_end, dt0, tol):
    def f(y):
        return -lam * y + s
    t, y, h = 0.0, y0, dt0
    acc = rej = 0
    while t < t_end - 1e-12 and acc + rej < 500:
        h = min(h, t_end - t)
        k1 = f(y)
        k2 = f(y + h * k1)
        y_low = y + h * k1
        y_high = y + h / 2 * (k1 + k2)
        err = abs(y_high - y_low) / max(1.0, abs(y))
        if err <= tol:
            t += h
            y = y_high
            acc += 1
            grow = 2.0 if err == 0 else min(2.0, 0.9 * (tol / err) ** 0.5)
            h = h * grow
        else:
            rej += 1
            h = h * max(0.2, 0.9 * (tol / err) ** 0.5)
    return [round(y, 6), acc, rej, round(h, 6)]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[[[-0.696, 4.83, -1.861, 0.5, 0.7, 0.0001], [-0.41309, 64, 3, 0.004672]], [[-0.952, 1.695, -1.777, 2.0, 0.3, 0.01], [-1.043824, 6, 1, 0.806875]], [[1.329, 2.639, 0.535, 1.5, 0.7, 0.01], [0.225936, 17, 2, 0.200122]], [[-0.678, 2.724, 0.55, 1.5, 0.05, 0.01], [0.185448, 16, 0, 0.164424]], [[-0.303, 0.644, 0.316, 1.5, 0.05, 0.01], [0.187358, 8, 0, 0.223575]], [[1.121, 4.954, -0.6, 0.5, 0.3, 0.01], [-0.014719, 14, 2, 0.068659]], [[-0.223, 4.209, -0.57, 1.5, 0.7, 0.01], [-0.136586, 8, 2, 0.375934]], [[-0.603, 0.0, 1.691, 2.0, 0.1, 0.0001], [2.779, 5, 0, 1.0]]], [[[0.248, 2.246, 0.565, 1.5, 0.7, 0.01], [0.249628, 2, 0, 1.166103]], [[2.431, 0.0, 0.71, 1.0, 0.1, 0.01], [3.141, 4, 0, 0.6]], [[-0.907, 0.0, 0.08, 2.0, 0.3, 0.0001], [-0.747, 3, 0, 2.2]], [[0.342, 2.406, 1.149, 1.0, 0.3, 0.001], [0.465139, 15, 2, 0.090253]], [[1.498, 1.599, -1.343, 1.5, 0.7, 0.0001], [-0.627468, 167, 3, 0.017123]], [[2.09, 3.725, -1.778, 1.0, 0.7, 0.0001], [-0.415384, 205, 4, 0.005852]], [[1.324, 1.927, -0.437, 2.0, 0.3, 0.001], [-0.193674, 55, 2, 0.105477]], [[-2.813, 4.676, -0.613, 1.0, 0.1, 0.01], [-0.158154, 23, 1, 0.132111]]], [[[-2.871, 0.0, 0.577, 0.5, 0.3, 0.001], [-2.5825, 2, 0, 0.4]], [[-0.304, 3.173, 1.878, 2.0, 0.1, 0.0001], [0.590274, 148, 2, 0.004498]], [[2.099, 1.458, -0.969, 2.0, 0.7, 0.01], [-0.512845, 21, 2, 0.154279]], [[-0.18, 0.0, 0.811, 2.0, 0.05, 0.0001], [1.442, 6, 0, 0.9]], [[1.402, 2.3, -1.252, 0.5, 0.1, 0.01], [0.073687, 10, 1, 0.066063]], [[1.749, 2.243, 1.405, 1.5, 0.05, 0.01], [0.667087, 15, 0, 0.241935]], [[2.731, 1.096, -1.616, 1.5, 0.1, 0.0001], [-0.661911, 164, 2, 0.005732]], [[-1.619, 0.0, 0.292, 1.0, 0.1, 0.0001], [-1.327, 4, 0, 0.6]]], [[[-2.345, 2.942, -1.888, 2.0, 0.7, 0.01], [-0.64795, 21, 2, 0.218574]], [[0.219, 2.157, 0.867, 1.0, 0.1, 0.01], [0.379141, 6, 0, 0.311548]], [[0.687, 3.619, 0.954, 0.5, 0.05, 0.01], [0.334636, 8, 0, 0.050261]], [[-1.101, 3.233, -1.581, 2.0, 0.1, 0.001], [-0.49013, 42, 2, 0.175182]], [[-2.44, 2.788, -0.813, 1.5, 0.7, 0.01], [-0.326363, 21, 2, 0.098371]], [[-2.521, 1.781, 1.018, 0.5, 0.3, 0.0001], [-0.697788, 83, 3, 0.000494]], [[-2.752, 2.034, 0.926, 1.5, 0.7, 0.001], [0.303269, 65, 3, 0.023728]], [[0.741, 0.0, 1.287, 1.5, 0.1, 0.001], [2.6715, 4, 0, 1.6]]], [[[-1.507, 4.389, 1.938, 2.0, 0.3, 0.01], [0.439365, 26, 2, 0.506375]], [[-0.306, 1.726, 0.514, 0.5, 0.3, 0.0001], [0.043044, 44, 3, 0.012094]], [[-1.261, 2.268, -0.675, 1.5, 0.3, 0.0001], [-0.329731, 128, 3, 0.007802]], [[1.532, 3.924, -1.443, 0.5, 0.7, 0.0001], [-0.100654, 134, 4, 0.001341]], [[2.878, 0.0, 1.169, 1.5, 0.7, 0.0001], [4.6315, 2, 0, 1.6]], [[-1.151, 1.35, -1.656, 0.5, 0.1, 0.001], [-1.188015, 5, 0, 0.046485]], [[1.917, 0.404, 1.543, 1.5, 0.1, 0.01], [2.779594, 6, 0, 0.239185]], [[2.057, 4.405, 0.797, 1.5, 0.1, 0.01], [0.185265, 23, 1, 0.310434]]]]
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.41309, 64, 3, 0.004672][-0.41309, 64, 3, 0.004672]Passed
case 1[-1.043824, 6, 1, 0.806875][-1.043824, 6, 1, 0.806875]Passed
case 2[0.225936, 17, 2, 0.200122][0.225936, 17, 2, 0.200122]Passed
case 3[0.185448, 16, 0, 0.164424][0.185448, 16, 0, 0.164424]Passed
case 4[0.187358, 8, 0, 0.223575][0.187358, 8, 0, 0.223575]Passed
case 5[-0.014719, 14, 2, 0.068659][-0.014719, 14, 2, 0.068659]Passed
case 6[-0.136586, 8, 2, 0.375934][-0.136586, 8, 2, 0.375934]Passed
case 7[2.779, 5, 0, 1.0][2.779, 5, 0, 1.0]Passed

SHA-256 / 8f39f0b1513026767458b41d51442e965e42d602cd8c7ffec1ae832563e29bff

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.368961+00:00.

Case digest / 1c4bb2e2a5d2c325aa201e650fa6e98361dafe7a3262d30d4b98c81d1a795800