FA-87051 / Physics integrator stability / Open access
Adaptive steps overshoot the requested end time · case 01
The solution is reported for a time later than t_end.
ROOT CAUSE
The step is never clipped to t_end - t.
VERIFIED REPAIR
Clip every attempted step to the remaining interval.
Unsuccessful approach: Clipping to t_end instead of the remainder still allows overshoot late in the run.
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:
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.734, 1.789, 1.259, 1.5, 0.1, 0.001], [0.705988, 9, 0, 0.108235]], [[-0.952, 1.695, -1.777, 2.0, 0.3, 0.01], [-1.043824, 6, 1, 0.806875]], [[1.314, 3.637, 1.469, 1.0, 0.1, 0.001], [0.428088, 42, 2, 0.054341]], [[2.009, 1.907, -1.617, 0.5, 0.3, 0.01], [0.255071, 10, 1, 0.029016]], [[-2.661, 0.0, 0.567, 1.5, 0.05, 0.001], [-1.8105, 5, 0, 1.5]], [[1.665, 1.112, 1.969, 2.0, 0.05, 0.001], [1.758958, 11, 0, 0.396553]], [[1.329, 2.639, 0.535, 1.5, 0.7, 0.01], [0.225936, 17, 2, 0.200122]], [[-2.569, 4.482, -1.811, 1.5, 0.1, 0.001], [-0.406872, 68, 2, 0.019689]]], [[[-2.712, 3.277, 1.673, 1.0, 0.05, 0.01], [0.386621, 23, 1, 0.024826]], [[2.279, 0.391, 1.095, 2.0, 0.3, 0.001], [2.561598, 9, 1, 0.024918]], [[2.431, 0.0, 0.71, 1.0, 0.1, 0.01], [3.141, 4, 0, 0.6]], [[2.09, 3.725, -1.778, 1.0, 0.7, 0.0001], [-0.415384, 205, 4, 0.005852]], [[-2.161, 4.981, 0.821, 2.0, 0.05, 0.0001], [0.164701, 232, 2, 0.153215]], [[-0.62, 4.337, 0.838, 0.5, 0.3, 0.001], [0.100011, 32, 3, 0.010439]], [[2.544, 3.611, 0.037, 1.5, 0.05, 0.0001], [0.021528, 217, 2, 0.032019]], [[0.762, 3.516, -1.514, 2.0, 0.05, 0.01], [-0.428351, 21, 1, 0.517761]]], [[[0.886, 0.0, -0.594, 2.0, 0.1, 0.0001], [-0.302, 5, 0, 1.0]], [[1.412, 4.795, 1.541, 1.0, 0.3, 0.001], [0.330607, 50, 3, 0.043844]], [[-1.429, 4.357, 1.585, 2.0, 0.7, 0.001], [0.363343, 71, 3, 0.130298]], [[-2.866, 0.946, 0.95, 1.0, 0.3, 0.001], [-0.498791, 30, 2, 0.034169]], [[-0.217, 4.32, 1.674, 1.5, 0.1, 0.001], [0.38641, 42, 2, 0.111773]], [[2.311, 3.452, -0.798, 2.0, 0.1, 0.001], [-0.228411, 77, 2, 0.022342]], [[2.099, 1.458, -0.969, 2.0, 0.7, 0.01], [-0.512845, 21, 2, 0.154279]], [[1.267, 2.693, -0.039, 0.5, 0.7, 0.0001], [0.318914, 87, 3, 0.008112]]], [[[-2.363, 2.228, 1.475, 2.0, 0.3, 0.0001], [0.626886, 233, 3, 0.006708]], [[2.284, 0.645, 1.783, 1.0, 0.05, 0.0001], [2.512294, 20, 1, 0.061128]], [[-0.815, 0.978, -0.497, 0.5, 0.3, 0.001], [-0.696428, 7, 1, 0.050506]], [[-1.257, 0.0, -1.478, 0.5, 0.1, 0.01], [-1.996, 3, 0, 0.4]], [[0.219, 2.157, 0.867, 1.0, 0.1, 0.01], [0.379141, 6, 0, 0.311548]], [[-2.405, 0.0, -1.998, 1.5, 0.3, 0.0001], [-5.402, 3, 0, 1.2]], [[0.957, 4.675, -1.869, 1.0, 0.3, 0.0001], [-0.387109, 169, 3, 0.023272]], [[0.741, 0.0, 1.287, 1.5, 0.1, 0.001], [2.6715, 4, 0, 1.6]]], [[[-0.41, 3.365, -1.877, 2.0, 0.3, 0.0001], [-0.557606, 63, 3, 0.216167]], [[1.604, 3.262, 1.026, 1.0, 0.1, 0.0001], [0.363958, 141, 2, 0.004622]], [[-2.568, 3.406, -1.466, 1.0, 0.3, 0.001], [-0.501574, 54, 2, 0.042216]], [[1.532, 3.924, -1.443, 0.5, 0.7, 0.0001], [-0.100654, 134, 4, 0.001341]], [[-1.508, 0.973, -1.003, 1.5, 0.7, 0.001], [-1.141938, 17, 2, 0.103067]], [[-0.96, 2.462, -0.926, 1.5, 0.1, 0.001], [-0.390863, 35, 1, 0.112273]], [[2.555, 2.399, 1.507, 2.0, 0.3, 0.0001], [0.644093, 176, 3, 0.040274]], [[-1.261, 2.268, -0.675, 1.5, 0.3, 0.0001], [-0.329731, 128, 3, 0.007802]]]]
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.705144, 9, 0, 0.452604] | [0.705988, 9, 0, 0.108235] | Failed |
| case 1 | [-1.044234, 6, 1, 0.841589] | [-1.043824, 6, 1, 0.806875] | Failed |
| case 2 | [0.425311, 42, 2, 0.067739] | [0.428088, 42, 2, 0.054341] | Failed |
| case 3 | [0.164836, 10, 1, 0.062678] | [0.255071, 10, 1, 0.029016] | Failed |
| case 4 | [-1.78215, 5, 0, 1.6] | [-1.8105, 5, 0, 1.5] | Failed |
| case 5 | [1.760516, 11, 0, 0.396553] | [1.758958, 11, 0, 0.396553] | Failed |
| case 6 | [0.220382, 17, 2, 0.277955] | [0.225936, 17, 2, 0.200122] | Failed |
| case 7 | [-0.405776, 68, 2, 0.165688] | [-0.406872, 68, 2, 0.019689] | Failed |
SHA-256 / d8babf6794b657f78b47294667e5adc06fe2af71b16f67733ad5f4bb9f18d9cc
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)
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.734, 1.789, 1.259, 1.5, 0.1, 0.001], [0.705988, 9, 0, 0.108235]], [[-0.952, 1.695, -1.777, 2.0, 0.3, 0.01], [-1.043824, 6, 1, 0.806875]], [[1.314, 3.637, 1.469, 1.0, 0.1, 0.001], [0.428088, 42, 2, 0.054341]], [[2.009, 1.907, -1.617, 0.5, 0.3, 0.01], [0.255071, 10, 1, 0.029016]], [[-2.661, 0.0, 0.567, 1.5, 0.05, 0.001], [-1.8105, 5, 0, 1.5]], [[1.665, 1.112, 1.969, 2.0, 0.05, 0.001], [1.758958, 11, 0, 0.396553]], [[1.329, 2.639, 0.535, 1.5, 0.7, 0.01], [0.225936, 17, 2, 0.200122]], [[-2.569, 4.482, -1.811, 1.5, 0.1, 0.001], [-0.406872, 68, 2, 0.019689]]], [[[-2.712, 3.277, 1.673, 1.0, 0.05, 0.01], [0.386621, 23, 1, 0.024826]], [[2.279, 0.391, 1.095, 2.0, 0.3, 0.001], [2.561598, 9, 1, 0.024918]], [[2.431, 0.0, 0.71, 1.0, 0.1, 0.01], [3.141, 4, 0, 0.6]], [[2.09, 3.725, -1.778, 1.0, 0.7, 0.0001], [-0.415384, 205, 4, 0.005852]], [[-2.161, 4.981, 0.821, 2.0, 0.05, 0.0001], [0.164701, 232, 2, 0.153215]], [[-0.62, 4.337, 0.838, 0.5, 0.3, 0.001], [0.100011, 32, 3, 0.010439]], [[2.544, 3.611, 0.037, 1.5, 0.05, 0.0001], [0.021528, 217, 2, 0.032019]], [[0.762, 3.516, -1.514, 2.0, 0.05, 0.01], [-0.428351, 21, 1, 0.517761]]], [[[0.886, 0.0, -0.594, 2.0, 0.1, 0.0001], [-0.302, 5, 0, 1.0]], [[1.412, 4.795, 1.541, 1.0, 0.3, 0.001], [0.330607, 50, 3, 0.043844]], [[-1.429, 4.357, 1.585, 2.0, 0.7, 0.001], [0.363343, 71, 3, 0.130298]], [[-2.866, 0.946, 0.95, 1.0, 0.3, 0.001], [-0.498791, 30, 2, 0.034169]], [[-0.217, 4.32, 1.674, 1.5, 0.1, 0.001], [0.38641, 42, 2, 0.111773]], [[2.311, 3.452, -0.798, 2.0, 0.1, 0.001], [-0.228411, 77, 2, 0.022342]], [[2.099, 1.458, -0.969, 2.0, 0.7, 0.01], [-0.512845, 21, 2, 0.154279]], [[1.267, 2.693, -0.039, 0.5, 0.7, 0.0001], [0.318914, 87, 3, 0.008112]]], [[[-2.363, 2.228, 1.475, 2.0, 0.3, 0.0001], [0.626886, 233, 3, 0.006708]], [[2.284, 0.645, 1.783, 1.0, 0.05, 0.0001], [2.512294, 20, 1, 0.061128]], [[-0.815, 0.978, -0.497, 0.5, 0.3, 0.001], [-0.696428, 7, 1, 0.050506]], [[-1.257, 0.0, -1.478, 0.5, 0.1, 0.01], [-1.996, 3, 0, 0.4]], [[0.219, 2.157, 0.867, 1.0, 0.1, 0.01], [0.379141, 6, 0, 0.311548]], [[-2.405, 0.0, -1.998, 1.5, 0.3, 0.0001], [-5.402, 3, 0, 1.2]], [[0.957, 4.675, -1.869, 1.0, 0.3, 0.0001], [-0.387109, 169, 3, 0.023272]], [[0.741, 0.0, 1.287, 1.5, 0.1, 0.001], [2.6715, 4, 0, 1.6]]], [[[-0.41, 3.365, -1.877, 2.0, 0.3, 0.0001], [-0.557606, 63, 3, 0.216167]], [[1.604, 3.262, 1.026, 1.0, 0.1, 0.0001], [0.363958, 141, 2, 0.004622]], [[-2.568, 3.406, -1.466, 1.0, 0.3, 0.001], [-0.501574, 54, 2, 0.042216]], [[1.532, 3.924, -1.443, 0.5, 0.7, 0.0001], [-0.100654, 134, 4, 0.001341]], [[-1.508, 0.973, -1.003, 1.5, 0.7, 0.001], [-1.141938, 17, 2, 0.103067]], [[-0.96, 2.462, -0.926, 1.5, 0.1, 0.001], [-0.390863, 35, 1, 0.112273]], [[2.555, 2.399, 1.507, 2.0, 0.3, 0.0001], [0.644093, 176, 3, 0.040274]], [[-1.261, 2.268, -0.675, 1.5, 0.3, 0.0001], [-0.329731, 128, 3, 0.007802]]]]
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.705144, 9, 0, 0.452604] | [0.705988, 9, 0, 0.108235] | Failed |
| case 1 | [-1.044234, 6, 1, 0.841589] | [-1.043824, 6, 1, 0.806875] | Failed |
| case 2 | [0.425311, 42, 2, 0.067739] | [0.428088, 42, 2, 0.054341] | Failed |
| case 3 | [0.164836, 10, 1, 0.062678] | [0.255071, 10, 1, 0.029016] | Failed |
| case 4 | [-1.78215, 5, 0, 1.6] | [-1.8105, 5, 0, 1.5] | Failed |
| case 5 | [1.760516, 11, 0, 0.396553] | [1.758958, 11, 0, 0.396553] | Failed |
| case 6 | [0.220382, 17, 2, 0.277955] | [0.225936, 17, 2, 0.200122] | Failed |
| case 7 | [-0.405776, 68, 2, 0.165688] | [-0.406872, 68, 2, 0.019689] | Failed |
SHA-256 / 434daa75785d6546d822426c95214b35e0b9f77c7fd845617e67ea49c91b687d
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.734, 1.789, 1.259, 1.5, 0.1, 0.001], [0.705988, 9, 0, 0.108235]], [[-0.952, 1.695, -1.777, 2.0, 0.3, 0.01], [-1.043824, 6, 1, 0.806875]], [[1.314, 3.637, 1.469, 1.0, 0.1, 0.001], [0.428088, 42, 2, 0.054341]], [[2.009, 1.907, -1.617, 0.5, 0.3, 0.01], [0.255071, 10, 1, 0.029016]], [[-2.661, 0.0, 0.567, 1.5, 0.05, 0.001], [-1.8105, 5, 0, 1.5]], [[1.665, 1.112, 1.969, 2.0, 0.05, 0.001], [1.758958, 11, 0, 0.396553]], [[1.329, 2.639, 0.535, 1.5, 0.7, 0.01], [0.225936, 17, 2, 0.200122]], [[-2.569, 4.482, -1.811, 1.5, 0.1, 0.001], [-0.406872, 68, 2, 0.019689]]], [[[-2.712, 3.277, 1.673, 1.0, 0.05, 0.01], [0.386621, 23, 1, 0.024826]], [[2.279, 0.391, 1.095, 2.0, 0.3, 0.001], [2.561598, 9, 1, 0.024918]], [[2.431, 0.0, 0.71, 1.0, 0.1, 0.01], [3.141, 4, 0, 0.6]], [[2.09, 3.725, -1.778, 1.0, 0.7, 0.0001], [-0.415384, 205, 4, 0.005852]], [[-2.161, 4.981, 0.821, 2.0, 0.05, 0.0001], [0.164701, 232, 2, 0.153215]], [[-0.62, 4.337, 0.838, 0.5, 0.3, 0.001], [0.100011, 32, 3, 0.010439]], [[2.544, 3.611, 0.037, 1.5, 0.05, 0.0001], [0.021528, 217, 2, 0.032019]], [[0.762, 3.516, -1.514, 2.0, 0.05, 0.01], [-0.428351, 21, 1, 0.517761]]], [[[0.886, 0.0, -0.594, 2.0, 0.1, 0.0001], [-0.302, 5, 0, 1.0]], [[1.412, 4.795, 1.541, 1.0, 0.3, 0.001], [0.330607, 50, 3, 0.043844]], [[-1.429, 4.357, 1.585, 2.0, 0.7, 0.001], [0.363343, 71, 3, 0.130298]], [[-2.866, 0.946, 0.95, 1.0, 0.3, 0.001], [-0.498791, 30, 2, 0.034169]], [[-0.217, 4.32, 1.674, 1.5, 0.1, 0.001], [0.38641, 42, 2, 0.111773]], [[2.311, 3.452, -0.798, 2.0, 0.1, 0.001], [-0.228411, 77, 2, 0.022342]], [[2.099, 1.458, -0.969, 2.0, 0.7, 0.01], [-0.512845, 21, 2, 0.154279]], [[1.267, 2.693, -0.039, 0.5, 0.7, 0.0001], [0.318914, 87, 3, 0.008112]]], [[[-2.363, 2.228, 1.475, 2.0, 0.3, 0.0001], [0.626886, 233, 3, 0.006708]], [[2.284, 0.645, 1.783, 1.0, 0.05, 0.0001], [2.512294, 20, 1, 0.061128]], [[-0.815, 0.978, -0.497, 0.5, 0.3, 0.001], [-0.696428, 7, 1, 0.050506]], [[-1.257, 0.0, -1.478, 0.5, 0.1, 0.01], [-1.996, 3, 0, 0.4]], [[0.219, 2.157, 0.867, 1.0, 0.1, 0.01], [0.379141, 6, 0, 0.311548]], [[-2.405, 0.0, -1.998, 1.5, 0.3, 0.0001], [-5.402, 3, 0, 1.2]], [[0.957, 4.675, -1.869, 1.0, 0.3, 0.0001], [-0.387109, 169, 3, 0.023272]], [[0.741, 0.0, 1.287, 1.5, 0.1, 0.001], [2.6715, 4, 0, 1.6]]], [[[-0.41, 3.365, -1.877, 2.0, 0.3, 0.0001], [-0.557606, 63, 3, 0.216167]], [[1.604, 3.262, 1.026, 1.0, 0.1, 0.0001], [0.363958, 141, 2, 0.004622]], [[-2.568, 3.406, -1.466, 1.0, 0.3, 0.001], [-0.501574, 54, 2, 0.042216]], [[1.532, 3.924, -1.443, 0.5, 0.7, 0.0001], [-0.100654, 134, 4, 0.001341]], [[-1.508, 0.973, -1.003, 1.5, 0.7, 0.001], [-1.141938, 17, 2, 0.103067]], [[-0.96, 2.462, -0.926, 1.5, 0.1, 0.001], [-0.390863, 35, 1, 0.112273]], [[2.555, 2.399, 1.507, 2.0, 0.3, 0.0001], [0.644093, 176, 3, 0.040274]], [[-1.261, 2.268, -0.675, 1.5, 0.3, 0.0001], [-0.329731, 128, 3, 0.007802]]]]
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.705988, 9, 0, 0.108235] | [0.705988, 9, 0, 0.108235] | Passed |
| case 1 | [-1.043824, 6, 1, 0.806875] | [-1.043824, 6, 1, 0.806875] | Passed |
| case 2 | [0.428088, 42, 2, 0.054341] | [0.428088, 42, 2, 0.054341] | Passed |
| case 3 | [0.255071, 10, 1, 0.029016] | [0.255071, 10, 1, 0.029016] | Passed |
| case 4 | [-1.8105, 5, 0, 1.5] | [-1.8105, 5, 0, 1.5] | Passed |
| case 5 | [1.758958, 11, 0, 0.396553] | [1.758958, 11, 0, 0.396553] | Passed |
| case 6 | [0.225936, 17, 2, 0.200122] | [0.225936, 17, 2, 0.200122] | Passed |
| case 7 | [-0.406872, 68, 2, 0.019689] | [-0.406872, 68, 2, 0.019689] | Passed |
SHA-256 / e5f2cbad60ca594d08b3beae4fff68367e1ce6ff3d2a8f8120134e13c09882d8
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.287868+00:00.
Case digest / 8593d238edb21b5c1ae25bcd205b76ffe8726e1dd088e439b3debb04bbb5073d