FAILURE MAP
← Case archive

FA-87126 / Physics integrator stability / Open access

Verlet inertia term ignores the change in step length · case 01

Whenever the frame time changes the implied velocity jumps.

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

ROOT CAUSE

The displacement x-prev is reused without scaling by h/h_prev.

VERIFIED REPAIR

Scale the previous displacement by h_n/h_{n-1}.

Unsuccessful approach: Scaling by h_{n-1}/h_n inverts the correction.

Case contract

solve(x0, v0, w2, g, dts): position Verlet with variable steps, acceleration -w2*x+g. First step is Taylor x1=x0+v0*h0+0.5*a(x0)*h0^2; then x_{n+1}=x_n+(x_n-x_{n-1})*(h_n/h_{n-1})+a(x_n)*h_n*(h_n+h_{n-1})/2. Velocity estimate is (x_n-x_{n-1})/h_last. Empty dts returns the initial state. Return [x, v] 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(x0, v0, w2, g, dts):
    def acc(p):
        return -w2 * p + g
    if not dts:
        return [round(x0, 6), round(v0, 6)]
    prev = x0
    x = x0 + v0 * dts[0] + 0.5 * acc(x0) * dts[0] * dts[0]
    for i in range(1, len(dts)):
        h, hp = dts[i], dts[i - 1]
        nxt = x + (x - prev) + acc(x) * h * (h + hp) / 2
        prev, x = x, nxt
    v = (x - prev) / dts[-1]
    return [round(x, 6), round(v, 6)]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[[[1.122, 1.423, 17.941, -9.81, [0.03, 0.05, 0.02, 0.03, 0.03, 0.02]], [0.892542, -3.585623]], [[-1.571, -1.847, 12.561, 0.0, [0.05, 0.05]], [-1.654908, -0.324492]], [[0.69, -1.106, 19.376, 2.0, [0.01, 0.03, 0.02, 0.01, 0.01]], [0.567186, -1.883264]], [[1.282, -0.856, 0.733, -9.81, [0.05, 0.05, 0.05, 0.03]], [0.95467, -2.61524]], [[1.699, 1.89, 11.648, 2.0, [0.01, 0.01, 0.01]], [1.747613, 1.439167]], [[1.207, 1.792, 8.666, 0.0, [0.02, 0.05, 0.02, 0.02, 0.03]], [1.350164, 0.395333]], [[1.835, -1.641, 6.597, 2.0, [0.02, 0.02]], [1.761368, -1.939568]], [[0.223, -0.287, 16.575, 2.0, [0.05, 0.03, 0.05]], [0.173161, -0.43558]]], [[[-0.913, -1.493, 2.034, 0.0, [0.02, 0.02, 0.02, 0.03, 0.03]], [-1.077985, -1.282343]], [[1.693, 1.184, 10.982, -9.81, [0.02, 0.03, 0.05, 0.05]], [1.550068, -2.368135]], [[1.735, 0.846, 4.974, 2.0, [0.03, 0.03, 0.05, 0.03, 0.05, 0.01]], [1.768339, -0.484358]], [[0.454, 0.048, 11.925, 0.0, [0.01, 0.01, 0.01]], [0.453003, -0.087359]], [[0.644, -0.151, 13.871, -9.81, [0.02, 0.02, 0.01, 0.01, 0.05, 0.02, 0.03]], [0.387894, -2.717601]], [[-0.121, 1.284, 14.702, 2.0, [0.01, 0.01, 0.02, 0.03, 0.03, 0.03, 0.01]], [0.086716, 1.603261]], [[-0.075, 1.691, 14.847, 2.0, [0.05, 0.02, 0.03]], [0.105977, 1.863265]], [[1.972, 1.228, 18.377, 2.0, [0.01, 0.03]], [1.993612, 0.36813]]], [[[0.68, 1.896, 12.013, -9.81, [0.03, 0.03, 0.03]], [0.775896, 0.500859]], [[0.012, 0.753, 6.017, 0.0, [0.01, 0.03, 0.02]], [0.056914, 0.743966]], [[-0.26, 1.352, 14.082, 0.0, [0.02, 0.02, 0.05, 0.02, 0.03, 0.05, 0.03, 0.02]], [0.121919, 1.619287]], [[-1.478, -0.428, 14.024, 2.0, [0.01, 0.02, 0.01]], [-1.476913, 0.36867]], [[0.106, -1.937, 2.608, 0.0, [0.03, 0.01, 0.03, 0.05, 0.01, 0.05, 0.05, 0.02]], [-0.374085, -1.858207]], [[-0.045, -0.271, 10.763, 2.0, [0.02, 0.03]], [-0.055405, -0.182724]], [[0.104, 2.0, 8.382, 2.0, [0.05, 0.02, 0.01, 0.01, 0.03, 0.02, 0.05]], [0.485754, 1.959947]], [[0.637, -0.169, 4.032, 0.0, [0.01, 0.03]], [0.62819, -0.233063]]], [[[-1.698, 0.325, 6.546, -9.81, [0.02, 0.02]], [-1.683974, 0.363268]], [[1.678, -1.604, 6.016, 2.0, [0.02, 0.03, 0.05, 0.02, 0.05, 0.05, 0.02]], [1.087528, -3.119149]], [[-0.434, -1.688, 15.899, 0.0, [0.03, 0.05, 0.01, 0.02]], [-0.573341, -0.885589]], [[0.423, 1.658, 5.012, -9.81, [0.01, 0.05, 0.02, 0.03, 0.03, 0.01, 0.03]], [0.523014, -0.377134]], [[0.774, -1.188, 7.384, 2.0, [0.03, 0.05, 0.05, 0.02]], [0.55902, -1.610601]], [[-0.757, 0.123, 15.508, 0.0, [0.01, 0.03]], [-0.742705, 0.415925]], [[-0.101, -1.02, 19.226, 2.0, [0.05, 0.01]], [-0.154839, -0.776626]], [[-0.582, 0.602, 4.289, 2.0, [0.02, 0.03, 0.01]], [-0.537873, 0.844843]]], [[[0.328, -1.645, 18.837, -9.81, [0.02, 0.03]], [0.226274, -2.187599]], [[0.777, 1.48, 11.801, 0.0, [0.05, 0.01, 0.01, 0.02, 0.02]], [0.881554, 0.495811]], [[0.705, -0.579, 5.962, 0.0, [0.03, 0.05, 0.03, 0.03, 0.02, 0.03, 0.05, 0.03, 0.02, 0.03]], [0.333218, -1.592526]], [[-1.434, -0.348, 10.741, -9.81, [0.03, 0.03, 0.03, 0.01, 0.05, 0.03, 0.03, 0.03, 0.01, 0.01]], [-1.336072, 1.031162]], [[1.94, 0.689, 18.476, 0.0, [0.05, 0.01, 0.03, 0.01, 0.05, 0.02, 0.03, 0.02, 0.02]], [1.133627, -6.57541]], [[1.097, 0.642, 16.162, 2.0, [0.01, 0.03]], [1.110042, 0.246936]], [[0.391, 1.387, 17.389, -9.81, [0.05, 0.03, 0.01]], [0.447014, -0.078323]], [[-0.957, 1.594, 5.217, 0.0, [0.05, 0.03]], [-0.814041, 1.900589]]]]
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.781189, -5.6062][0.892542, -3.585623]Failed
case 1[-1.654908, -0.324492][-1.654908, -0.324492]Passed
case 2[0.584845, -2.62431][0.567186, -1.883264]Failed
case 3[0.910819, -4.076939][0.95467, -2.61524]Failed
case 4[1.747613, 1.439167][1.747613, 1.439167]Passed
case 5[1.260638, -0.17639][1.350164, 0.395333]Failed
case 6[1.761368, -1.939568][1.761368, -1.939568]Passed
case 7[0.16793, -0.40844][0.173161, -0.43558]Failed

SHA-256 / a7148a8c372842a01a288f28d7c42adc2242e64aef91caabeb0dcbe873018379

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, w2, g, dts):
    def acc(p):
        return -w2 * p + g
    if not dts:
        return [round(x0, 6), round(v0, 6)]
    prev = x0
    x = x0 + v0 * dts[0] + 0.5 * acc(x0) * dts[0] * dts[0]
    for i in range(1, len(dts)):
        h, hp = dts[i], dts[i - 1]
        nxt = x + (x - prev) * (hp / h) + acc(x) * h * (h + hp) / 2
        prev, x = x, nxt
    v = (x - prev) / dts[-1]
    return [round(x, 6), round(v, 6)]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[[[1.122, 1.423, 17.941, -9.81, [0.03, 0.05, 0.02, 0.03, 0.03, 0.02]], [0.892542, -3.585623]], [[-1.571, -1.847, 12.561, 0.0, [0.05, 0.05]], [-1.654908, -0.324492]], [[0.69, -1.106, 19.376, 2.0, [0.01, 0.03, 0.02, 0.01, 0.01]], [0.567186, -1.883264]], [[1.282, -0.856, 0.733, -9.81, [0.05, 0.05, 0.05, 0.03]], [0.95467, -2.61524]], [[1.699, 1.89, 11.648, 2.0, [0.01, 0.01, 0.01]], [1.747613, 1.439167]], [[1.207, 1.792, 8.666, 0.0, [0.02, 0.05, 0.02, 0.02, 0.03]], [1.350164, 0.395333]], [[1.835, -1.641, 6.597, 2.0, [0.02, 0.02]], [1.761368, -1.939568]], [[0.223, -0.287, 16.575, 2.0, [0.05, 0.03, 0.05]], [0.173161, -0.43558]]], [[[-0.913, -1.493, 2.034, 0.0, [0.02, 0.02, 0.02, 0.03, 0.03]], [-1.077985, -1.282343]], [[1.693, 1.184, 10.982, -9.81, [0.02, 0.03, 0.05, 0.05]], [1.550068, -2.368135]], [[1.735, 0.846, 4.974, 2.0, [0.03, 0.03, 0.05, 0.03, 0.05, 0.01]], [1.768339, -0.484358]], [[0.454, 0.048, 11.925, 0.0, [0.01, 0.01, 0.01]], [0.453003, -0.087359]], [[0.644, -0.151, 13.871, -9.81, [0.02, 0.02, 0.01, 0.01, 0.05, 0.02, 0.03]], [0.387894, -2.717601]], [[-0.121, 1.284, 14.702, 2.0, [0.01, 0.01, 0.02, 0.03, 0.03, 0.03, 0.01]], [0.086716, 1.603261]], [[-0.075, 1.691, 14.847, 2.0, [0.05, 0.02, 0.03]], [0.105977, 1.863265]], [[1.972, 1.228, 18.377, 2.0, [0.01, 0.03]], [1.993612, 0.36813]]], [[[0.68, 1.896, 12.013, -9.81, [0.03, 0.03, 0.03]], [0.775896, 0.500859]], [[0.012, 0.753, 6.017, 0.0, [0.01, 0.03, 0.02]], [0.056914, 0.743966]], [[-0.26, 1.352, 14.082, 0.0, [0.02, 0.02, 0.05, 0.02, 0.03, 0.05, 0.03, 0.02]], [0.121919, 1.619287]], [[-1.478, -0.428, 14.024, 2.0, [0.01, 0.02, 0.01]], [-1.476913, 0.36867]], [[0.106, -1.937, 2.608, 0.0, [0.03, 0.01, 0.03, 0.05, 0.01, 0.05, 0.05, 0.02]], [-0.374085, -1.858207]], [[-0.045, -0.271, 10.763, 2.0, [0.02, 0.03]], [-0.055405, -0.182724]], [[0.104, 2.0, 8.382, 2.0, [0.05, 0.02, 0.01, 0.01, 0.03, 0.02, 0.05]], [0.485754, 1.959947]], [[0.637, -0.169, 4.032, 0.0, [0.01, 0.03]], [0.62819, -0.233063]]], [[[-1.698, 0.325, 6.546, -9.81, [0.02, 0.02]], [-1.683974, 0.363268]], [[1.678, -1.604, 6.016, 2.0, [0.02, 0.03, 0.05, 0.02, 0.05, 0.05, 0.02]], [1.087528, -3.119149]], [[-0.434, -1.688, 15.899, 0.0, [0.03, 0.05, 0.01, 0.02]], [-0.573341, -0.885589]], [[0.423, 1.658, 5.012, -9.81, [0.01, 0.05, 0.02, 0.03, 0.03, 0.01, 0.03]], [0.523014, -0.377134]], [[0.774, -1.188, 7.384, 2.0, [0.03, 0.05, 0.05, 0.02]], [0.55902, -1.610601]], [[-0.757, 0.123, 15.508, 0.0, [0.01, 0.03]], [-0.742705, 0.415925]], [[-0.101, -1.02, 19.226, 2.0, [0.05, 0.01]], [-0.154839, -0.776626]], [[-0.582, 0.602, 4.289, 2.0, [0.02, 0.03, 0.01]], [-0.537873, 0.844843]]], [[[0.328, -1.645, 18.837, -9.81, [0.02, 0.03]], [0.226274, -2.187599]], [[0.777, 1.48, 11.801, 0.0, [0.05, 0.01, 0.01, 0.02, 0.02]], [0.881554, 0.495811]], [[0.705, -0.579, 5.962, 0.0, [0.03, 0.05, 0.03, 0.03, 0.02, 0.03, 0.05, 0.03, 0.02, 0.03]], [0.333218, -1.592526]], [[-1.434, -0.348, 10.741, -9.81, [0.03, 0.03, 0.03, 0.01, 0.05, 0.03, 0.03, 0.03, 0.01, 0.01]], [-1.336072, 1.031162]], [[1.94, 0.689, 18.476, 0.0, [0.05, 0.01, 0.03, 0.01, 0.05, 0.02, 0.03, 0.02, 0.02]], [1.133627, -6.57541]], [[1.097, 0.642, 16.162, 2.0, [0.01, 0.03]], [1.110042, 0.246936]], [[0.391, 1.387, 17.389, -9.81, [0.05, 0.03, 0.01]], [0.447014, -0.078323]], [[-0.957, 1.594, 5.217, 0.0, [0.05, 0.03]], [-0.814041, 1.900589]]]]
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.536241, -10.299188][0.892542, -3.585623]Failed
case 1[-1.654908, -0.324492][-1.654908, -0.324492]Passed
case 2[0.557116, -4.517333][0.567186, -1.883264]Failed
case 3[0.837734, -6.513105][0.95467, -2.61524]Failed
case 4[1.747613, 1.439167][1.747613, 1.439167]Passed
case 5[1.165247, -0.813252][1.350164, 0.395333]Failed
case 6[1.761368, -1.939568][1.761368, -1.939568]Passed
case 7[0.157997, -0.387497][0.173161, -0.43558]Failed

SHA-256 / 00dfaae004ff29b9dbfc9cbd7480a7b227d2ea9d85c4c8361f34ff20b8598e7f

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, w2, g, dts):
    def acc(p):
        return -w2 * p + g
    if not dts:
        return [round(x0, 6), round(v0, 6)]
    prev = x0
    x = x0 + v0 * dts[0] + 0.5 * acc(x0) * dts[0] * dts[0]
    for i in range(1, len(dts)):
        h, hp = dts[i], dts[i - 1]
        nxt = x + (x - prev) * (h / hp) + acc(x) * h * (h + hp) / 2
        prev, x = x, nxt
    v = (x - prev) / dts[-1]
    return [round(x, 6), round(v, 6)]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[[[1.122, 1.423, 17.941, -9.81, [0.03, 0.05, 0.02, 0.03, 0.03, 0.02]], [0.892542, -3.585623]], [[-1.571, -1.847, 12.561, 0.0, [0.05, 0.05]], [-1.654908, -0.324492]], [[0.69, -1.106, 19.376, 2.0, [0.01, 0.03, 0.02, 0.01, 0.01]], [0.567186, -1.883264]], [[1.282, -0.856, 0.733, -9.81, [0.05, 0.05, 0.05, 0.03]], [0.95467, -2.61524]], [[1.699, 1.89, 11.648, 2.0, [0.01, 0.01, 0.01]], [1.747613, 1.439167]], [[1.207, 1.792, 8.666, 0.0, [0.02, 0.05, 0.02, 0.02, 0.03]], [1.350164, 0.395333]], [[1.835, -1.641, 6.597, 2.0, [0.02, 0.02]], [1.761368, -1.939568]], [[0.223, -0.287, 16.575, 2.0, [0.05, 0.03, 0.05]], [0.173161, -0.43558]]], [[[-0.913, -1.493, 2.034, 0.0, [0.02, 0.02, 0.02, 0.03, 0.03]], [-1.077985, -1.282343]], [[1.693, 1.184, 10.982, -9.81, [0.02, 0.03, 0.05, 0.05]], [1.550068, -2.368135]], [[1.735, 0.846, 4.974, 2.0, [0.03, 0.03, 0.05, 0.03, 0.05, 0.01]], [1.768339, -0.484358]], [[0.454, 0.048, 11.925, 0.0, [0.01, 0.01, 0.01]], [0.453003, -0.087359]], [[0.644, -0.151, 13.871, -9.81, [0.02, 0.02, 0.01, 0.01, 0.05, 0.02, 0.03]], [0.387894, -2.717601]], [[-0.121, 1.284, 14.702, 2.0, [0.01, 0.01, 0.02, 0.03, 0.03, 0.03, 0.01]], [0.086716, 1.603261]], [[-0.075, 1.691, 14.847, 2.0, [0.05, 0.02, 0.03]], [0.105977, 1.863265]], [[1.972, 1.228, 18.377, 2.0, [0.01, 0.03]], [1.993612, 0.36813]]], [[[0.68, 1.896, 12.013, -9.81, [0.03, 0.03, 0.03]], [0.775896, 0.500859]], [[0.012, 0.753, 6.017, 0.0, [0.01, 0.03, 0.02]], [0.056914, 0.743966]], [[-0.26, 1.352, 14.082, 0.0, [0.02, 0.02, 0.05, 0.02, 0.03, 0.05, 0.03, 0.02]], [0.121919, 1.619287]], [[-1.478, -0.428, 14.024, 2.0, [0.01, 0.02, 0.01]], [-1.476913, 0.36867]], [[0.106, -1.937, 2.608, 0.0, [0.03, 0.01, 0.03, 0.05, 0.01, 0.05, 0.05, 0.02]], [-0.374085, -1.858207]], [[-0.045, -0.271, 10.763, 2.0, [0.02, 0.03]], [-0.055405, -0.182724]], [[0.104, 2.0, 8.382, 2.0, [0.05, 0.02, 0.01, 0.01, 0.03, 0.02, 0.05]], [0.485754, 1.959947]], [[0.637, -0.169, 4.032, 0.0, [0.01, 0.03]], [0.62819, -0.233063]]], [[[-1.698, 0.325, 6.546, -9.81, [0.02, 0.02]], [-1.683974, 0.363268]], [[1.678, -1.604, 6.016, 2.0, [0.02, 0.03, 0.05, 0.02, 0.05, 0.05, 0.02]], [1.087528, -3.119149]], [[-0.434, -1.688, 15.899, 0.0, [0.03, 0.05, 0.01, 0.02]], [-0.573341, -0.885589]], [[0.423, 1.658, 5.012, -9.81, [0.01, 0.05, 0.02, 0.03, 0.03, 0.01, 0.03]], [0.523014, -0.377134]], [[0.774, -1.188, 7.384, 2.0, [0.03, 0.05, 0.05, 0.02]], [0.55902, -1.610601]], [[-0.757, 0.123, 15.508, 0.0, [0.01, 0.03]], [-0.742705, 0.415925]], [[-0.101, -1.02, 19.226, 2.0, [0.05, 0.01]], [-0.154839, -0.776626]], [[-0.582, 0.602, 4.289, 2.0, [0.02, 0.03, 0.01]], [-0.537873, 0.844843]]], [[[0.328, -1.645, 18.837, -9.81, [0.02, 0.03]], [0.226274, -2.187599]], [[0.777, 1.48, 11.801, 0.0, [0.05, 0.01, 0.01, 0.02, 0.02]], [0.881554, 0.495811]], [[0.705, -0.579, 5.962, 0.0, [0.03, 0.05, 0.03, 0.03, 0.02, 0.03, 0.05, 0.03, 0.02, 0.03]], [0.333218, -1.592526]], [[-1.434, -0.348, 10.741, -9.81, [0.03, 0.03, 0.03, 0.01, 0.05, 0.03, 0.03, 0.03, 0.01, 0.01]], [-1.336072, 1.031162]], [[1.94, 0.689, 18.476, 0.0, [0.05, 0.01, 0.03, 0.01, 0.05, 0.02, 0.03, 0.02, 0.02]], [1.133627, -6.57541]], [[1.097, 0.642, 16.162, 2.0, [0.01, 0.03]], [1.110042, 0.246936]], [[0.391, 1.387, 17.389, -9.81, [0.05, 0.03, 0.01]], [0.447014, -0.078323]], [[-0.957, 1.594, 5.217, 0.0, [0.05, 0.03]], [-0.814041, 1.900589]]]]
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.892542, -3.585623][0.892542, -3.585623]Passed
case 1[-1.654908, -0.324492][-1.654908, -0.324492]Passed
case 2[0.567186, -1.883264][0.567186, -1.883264]Passed
case 3[0.95467, -2.61524][0.95467, -2.61524]Passed
case 4[1.747613, 1.439167][1.747613, 1.439167]Passed
case 5[1.350164, 0.395333][1.350164, 0.395333]Passed
case 6[1.761368, -1.939568][1.761368, -1.939568]Passed
case 7[0.173161, -0.43558][0.173161, -0.43558]Passed

SHA-256 / b9e524e177d94b4499a4608f0d0abec637371aeb2ae9b62c592d9d468b23b974

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

Case digest / 3d5a398ec7850225344fa844cf17f6613dd114e4db2e99b695d61b1e0ed73d12