FAILURE MAP
← Case archive

FA-86876 / Physics integrator stability / Open access

Verlet drift applies the full acceleration dt squared term · case 01

Swing amplitude is systematically wrong at larger time steps.

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

ROOT CAUSE

The position update adds a0*dt*dt instead of 0.5*a0*dt*dt.

VERIFIED REPAIR

Keep the Taylor factor one half on the acceleration term.

Unsuccessful approach: Dropping one dt factor (0.5*a0*dt) makes the term dimensionally a velocity.

Case contract

solve(theta_deg, omega, g, L, dt, steps): planar pendulum theta''=-(g/L)*sin(theta), initial angle in degrees, omega in rad/s, integrated with velocity Verlet carrying the acceleration between steps. Return [angle in degrees, omega, energy per unit mass 0.5*L^2*omega^2 + g*L*(1-cos theta)], 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(theta_deg, omega, g, L, dt, steps):
    th = math.radians(theta_deg)
    w = omega
    def acc(t):
        return -(g / L) * math.sin(t)
    a0 = acc(th)
    for _ in range(steps):
        th = th + w * dt + a0 * dt * dt
        a1 = acc(th)
        w = w + 0.5 * (a0 + a1) * dt
        a0 = a1
    energy = 0.5 * L * L * w * w + g * L * (1 - math.cos(th))
    return [round(math.degrees(th), 6), round(w, 6), round(energy, 6)]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[[[146.96, 2.386, 9.81, 2.012, 0.02, 14], [181.32788, 2.029348, 47.805782]], [[87.81, 0.31, 3.71, 1.235, 0.01, 11], [88.722943, -0.020319, 4.480049]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[-123.14, 2.838, 9.81, 2.8, 0.02, 10], [-86.931847, 3.501277, 74.05287]], [[47.0, -0.897, 1.62, 1.061, 0.05, 10], [14.725067, -1.294022, 0.998958]], [[-28.02, 2.032, 1.62, 1.549, 0.1, 6], [42.741056, 1.954585, 5.249758]], [[-46.16, 2.086, 3.71, 0.917, 0.1, 1], [-33.372114, 2.343182, 2.869399]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]]], [[[-33.78, 1.282, 9.81, 1.531, 0.02, 11], [-13.503901, 1.857824, 4.46032]], [[-94.77, -0.214, 1.62, 2.169, 0.01, 2], [-95.006698, -0.199117, 3.913697]], [[-83.79, 0.173, 3.71, 1.845, 0.02, 8], [-80.740326, 0.491946, 6.15544]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[98.27, -2.427, 1.62, 2.657, 0.01, 5], [91.273751, -2.457359, 25.71527]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[-97.92, -2.539, 9.81, 0.573, 0.1, 7], [22.897574, 6.430386, 7.231114]], [[112.84, -2.85, 1.62, 0.918, 0.05, 18], [-61.55966, -3.341714, 5.484281]]], [[[99.84, 2.522, 9.81, 2.3, 0.1, 15], [139.078626, -1.167819, 43.219065]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[-67.59, -1.125, 9.81, 2.341, 0.01, 8], [-72.028677, -0.810202, 17.678207]], [[58.85, 2.786, 1.62, 1.006, 0.1, 15], [231.321212, 2.02495, 4.723108]], [[-124.01, -2.0, 9.81, 0.956, 0.01, 22], [-138.432846, -0.351896, 16.451635]], [[57.82, -0.433, 9.81, 1.682, 0.02, 15], [38.351771, -1.766761, 7.976004]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[-107.93, 0.087, 1.62, 2.144, 0.02, 12], [-105.543534, 0.260467, 4.559945]]], [[[-18.92, -0.987, 9.81, 1.541, 0.02, 1], [-20.027365, -0.944557, 1.973481]], [[-113.01, 0.456, 3.71, 2.862, 0.02, 8], [-107.945309, 0.65005, 15.620154]], [[-83.33, 1.121, 9.81, 0.637, 0.01, 16], [-62.037587, 3.481303, 5.77773]], [[-76.73, -1.475, 1.62, 0.956, 0.05, 16], [-113.853312, -0.165621, 2.187552]], [[-73.59, 1.271, 9.81, 0.814, 0.1, 5], [24.673646, 4.040094, 6.136624]], [[3.44, 0.497, 1.62, 1.232, 0.05, 2], [6.260347, 0.485875, 0.191061]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]]], [[[-107.87, -1.067, 3.71, 0.509, 0.02, 19], [-102.945115, 1.531755, 2.61536]], [[-74.15, 2.008, 3.71, 2.881, 0.1, 22], [159.697406, 0.957205, 24.515454]], [[136.06, 2.649, 3.71, 2.91, 0.1, 14], [364.986535, 3.370148, 48.130759]], [[-34.09, 2.305, 3.71, 0.783, 0.02, 21], [27.079805, 2.429454, 2.12776]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[-71.0, 1.904, 3.71, 2.676, 0.05, 22], [67.389778, 1.946364, 19.675143]], [[89.14, -1.757, 9.81, 1.734, 0.05, 20], [-85.551502, -1.947875, 21.395293]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.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[181.085303, 2.025805, 47.778458][181.32788, 2.029348, 47.805782]Failed
case 1[88.628333, -0.020312, 4.472485][88.722943, -0.020319, 4.480049]Failed
case 2[0.0, 0.0, 0.0][0.0, 0.0, 0.0]Passed
case 3[-86.552656, 3.501628, 73.881][-86.931847, 3.501277, 74.05287]Failed
case 4[14.172494, -1.290162, 0.989207][14.725067, -1.294022, 0.998958]Failed
case 5[42.658572, 1.953096, 5.240328][42.741056, 1.954585, 5.249758]Failed
case 6[-32.536128, 2.340705, 2.83752][-33.372114, 2.343182, 2.869399]Failed
case 7[89.297411, -0.245241, 19.499702][89.648705, -0.245248, 19.619998]Failed

SHA-256 / 98aaf6b8289c4cc15685aaf73609985a7b452e068506d7fc89f2de95d01db005

2 / The unsuccessful fix

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(theta_deg, omega, g, L, dt, steps):
    th = math.radians(theta_deg)
    w = omega
    def acc(t):
        return -(g / L) * math.sin(t)
    a0 = acc(th)
    for _ in range(steps):
        th = th + w * dt + 0.5 * a0 * dt
        a1 = acc(th)
        w = w + 0.5 * (a0 + a1) * dt
        a0 = a1
    energy = 0.5 * L * L * w * w + g * L * (1 - math.cos(th))
    return [round(math.degrees(th), 6), round(w, 6), round(energy, 6)]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[[[146.96, 2.386, 9.81, 2.012, 0.02, 14], [181.32788, 2.029348, 47.805782]], [[87.81, 0.31, 3.71, 1.235, 0.01, 11], [88.722943, -0.020319, 4.480049]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[-123.14, 2.838, 9.81, 2.8, 0.02, 10], [-86.931847, 3.501277, 74.05287]], [[47.0, -0.897, 1.62, 1.061, 0.05, 10], [14.725067, -1.294022, 0.998958]], [[-28.02, 2.032, 1.62, 1.549, 0.1, 6], [42.741056, 1.954585, 5.249758]], [[-46.16, 2.086, 3.71, 0.917, 0.1, 1], [-33.372114, 2.343182, 2.869399]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]]], [[[-33.78, 1.282, 9.81, 1.531, 0.02, 11], [-13.503901, 1.857824, 4.46032]], [[-94.77, -0.214, 1.62, 2.169, 0.01, 2], [-95.006698, -0.199117, 3.913697]], [[-83.79, 0.173, 3.71, 1.845, 0.02, 8], [-80.740326, 0.491946, 6.15544]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[98.27, -2.427, 1.62, 2.657, 0.01, 5], [91.273751, -2.457359, 25.71527]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[-97.92, -2.539, 9.81, 0.573, 0.1, 7], [22.897574, 6.430386, 7.231114]], [[112.84, -2.85, 1.62, 0.918, 0.05, 18], [-61.55966, -3.341714, 5.484281]]], [[[99.84, 2.522, 9.81, 2.3, 0.1, 15], [139.078626, -1.167819, 43.219065]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[-67.59, -1.125, 9.81, 2.341, 0.01, 8], [-72.028677, -0.810202, 17.678207]], [[58.85, 2.786, 1.62, 1.006, 0.1, 15], [231.321212, 2.02495, 4.723108]], [[-124.01, -2.0, 9.81, 0.956, 0.01, 22], [-138.432846, -0.351896, 16.451635]], [[57.82, -0.433, 9.81, 1.682, 0.02, 15], [38.351771, -1.766761, 7.976004]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[-107.93, 0.087, 1.62, 2.144, 0.02, 12], [-105.543534, 0.260467, 4.559945]]], [[[-18.92, -0.987, 9.81, 1.541, 0.02, 1], [-20.027365, -0.944557, 1.973481]], [[-113.01, 0.456, 3.71, 2.862, 0.02, 8], [-107.945309, 0.65005, 15.620154]], [[-83.33, 1.121, 9.81, 0.637, 0.01, 16], [-62.037587, 3.481303, 5.77773]], [[-76.73, -1.475, 1.62, 0.956, 0.05, 16], [-113.853312, -0.165621, 2.187552]], [[-73.59, 1.271, 9.81, 0.814, 0.1, 5], [24.673646, 4.040094, 6.136624]], [[3.44, 0.497, 1.62, 1.232, 0.05, 2], [6.260347, 0.485875, 0.191061]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]]], [[[-107.87, -1.067, 3.71, 0.509, 0.02, 19], [-102.945115, 1.531755, 2.61536]], [[-74.15, 2.008, 3.71, 2.881, 0.1, 22], [159.697406, 0.957205, 24.515454]], [[136.06, 2.649, 3.71, 2.91, 0.1, 14], [364.986535, 3.370148, 48.130759]], [[-34.09, 2.305, 3.71, 0.783, 0.02, 21], [27.079805, 2.429454, 2.12776]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[-71.0, 1.904, 3.71, 2.676, 0.05, 22], [67.389778, 1.946364, 19.675143]], [[89.14, -1.757, 9.81, 1.734, 0.05, 20], [-85.551502, -1.947875, 21.395293]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.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[163.89867, 1.818575, 45.395228][181.32788, 2.029348, 47.805782]Failed
case 1[79.411715, -0.018195, 3.740186][88.722943, -0.020319, 4.480049]Failed
case 2[0.0, 0.0, 0.0][0.0, 0.0, 0.0]Passed
case 3[-68.162751, 3.506974, 65.462258][-86.931847, 3.501277, 74.05287]Failed
case 4[5.813768, -1.225457, 0.854114][14.725067, -1.294022, 0.998958]Failed
case 5[41.722002, 1.942499, 5.163257][42.741056, 1.954585, 5.249758]Failed
case 6[-25.848237, 2.320103, 2.603572][-33.372114, 2.343182, 2.869399]Failed
case 7[82.974105, -0.244329, 17.339516][89.648705, -0.245248, 19.619998]Failed

SHA-256 / 827201d83ffcebce06640c10339b6d870786af42ae8278ec56d3183334d42804

3 / The verified repair

Exit 0
"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(theta_deg, omega, g, L, dt, steps):
    th = math.radians(theta_deg)
    w = omega
    def acc(t):
        return -(g / L) * math.sin(t)
    a0 = acc(th)
    for _ in range(steps):
        th = th + w * dt + 0.5 * a0 * dt * dt
        a1 = acc(th)
        w = w + 0.5 * (a0 + a1) * dt
        a0 = a1
    energy = 0.5 * L * L * w * w + g * L * (1 - math.cos(th))
    return [round(math.degrees(th), 6), round(w, 6), round(energy, 6)]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[[[146.96, 2.386, 9.81, 2.012, 0.02, 14], [181.32788, 2.029348, 47.805782]], [[87.81, 0.31, 3.71, 1.235, 0.01, 11], [88.722943, -0.020319, 4.480049]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[-123.14, 2.838, 9.81, 2.8, 0.02, 10], [-86.931847, 3.501277, 74.05287]], [[47.0, -0.897, 1.62, 1.061, 0.05, 10], [14.725067, -1.294022, 0.998958]], [[-28.02, 2.032, 1.62, 1.549, 0.1, 6], [42.741056, 1.954585, 5.249758]], [[-46.16, 2.086, 3.71, 0.917, 0.1, 1], [-33.372114, 2.343182, 2.869399]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]]], [[[-33.78, 1.282, 9.81, 1.531, 0.02, 11], [-13.503901, 1.857824, 4.46032]], [[-94.77, -0.214, 1.62, 2.169, 0.01, 2], [-95.006698, -0.199117, 3.913697]], [[-83.79, 0.173, 3.71, 1.845, 0.02, 8], [-80.740326, 0.491946, 6.15544]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[98.27, -2.427, 1.62, 2.657, 0.01, 5], [91.273751, -2.457359, 25.71527]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[-97.92, -2.539, 9.81, 0.573, 0.1, 7], [22.897574, 6.430386, 7.231114]], [[112.84, -2.85, 1.62, 0.918, 0.05, 18], [-61.55966, -3.341714, 5.484281]]], [[[99.84, 2.522, 9.81, 2.3, 0.1, 15], [139.078626, -1.167819, 43.219065]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[-67.59, -1.125, 9.81, 2.341, 0.01, 8], [-72.028677, -0.810202, 17.678207]], [[58.85, 2.786, 1.62, 1.006, 0.1, 15], [231.321212, 2.02495, 4.723108]], [[-124.01, -2.0, 9.81, 0.956, 0.01, 22], [-138.432846, -0.351896, 16.451635]], [[57.82, -0.433, 9.81, 1.682, 0.02, 15], [38.351771, -1.766761, 7.976004]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[-107.93, 0.087, 1.62, 2.144, 0.02, 12], [-105.543534, 0.260467, 4.559945]]], [[[-18.92, -0.987, 9.81, 1.541, 0.02, 1], [-20.027365, -0.944557, 1.973481]], [[-113.01, 0.456, 3.71, 2.862, 0.02, 8], [-107.945309, 0.65005, 15.620154]], [[-83.33, 1.121, 9.81, 0.637, 0.01, 16], [-62.037587, 3.481303, 5.77773]], [[-76.73, -1.475, 1.62, 0.956, 0.05, 16], [-113.853312, -0.165621, 2.187552]], [[-73.59, 1.271, 9.81, 0.814, 0.1, 5], [24.673646, 4.040094, 6.136624]], [[3.44, 0.497, 1.62, 1.232, 0.05, 2], [6.260347, 0.485875, 0.191061]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]]], [[[-107.87, -1.067, 3.71, 0.509, 0.02, 19], [-102.945115, 1.531755, 2.61536]], [[-74.15, 2.008, 3.71, 2.881, 0.1, 22], [159.697406, 0.957205, 24.515454]], [[136.06, 2.649, 3.71, 2.91, 0.1, 14], [364.986535, 3.370148, 48.130759]], [[-34.09, 2.305, 3.71, 0.783, 0.02, 21], [27.079805, 2.429454, 2.12776]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[-71.0, 1.904, 3.71, 2.676, 0.05, 22], [67.389778, 1.946364, 19.675143]], [[89.14, -1.757, 9.81, 1.734, 0.05, 20], [-85.551502, -1.947875, 21.395293]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.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[181.32788, 2.029348, 47.805782][181.32788, 2.029348, 47.805782]Passed
case 1[88.722943, -0.020319, 4.480049][88.722943, -0.020319, 4.480049]Passed
case 2[0.0, 0.0, 0.0][0.0, 0.0, 0.0]Passed
case 3[-86.931847, 3.501277, 74.05287][-86.931847, 3.501277, 74.05287]Passed
case 4[14.725067, -1.294022, 0.998958][14.725067, -1.294022, 0.998958]Passed
case 5[42.741056, 1.954585, 5.249758][42.741056, 1.954585, 5.249758]Passed
case 6[-33.372114, 2.343182, 2.869399][-33.372114, 2.343182, 2.869399]Passed
case 7[89.648705, -0.245248, 19.619998][89.648705, -0.245248, 19.619998]Passed

SHA-256 / 7c1468aaa7afac643cf3044ef7e9f39331e6ac1bf78a055e51479526fcd2371d

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

Case digest / cc75b77527f891cc949140107fe48325f3d446876612adc0739491e44378d893