FAILURE MAP
← Case archive

FA-86896 / Physics integrator stability / Open access

Pendulum potential energy uses cos theta instead of 1 - cos theta · case 01

The reported energy is offset and has the wrong sign convention at rest.

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

ROOT CAUSE

Potential energy is computed as g*L*cos(theta).

VERIFIED REPAIR

Use g*L*(1-cos theta) measured from the lowest point.

Unsuccessful approach: Converting the already-radian angle again with radians() evaluates the wrong angle.

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 + 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 * 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 = [[[[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[-123.14, 2.838, 9.81, 2.8, 0.02, 10], [-86.931847, 3.501277, 74.05287]], [[87.81, 0.31, 3.71, 1.235, 0.01, 11], [88.722943, -0.020319, 4.480049]], [[47.0, -0.897, 1.62, 1.061, 0.05, 10], [14.725067, -1.294022, 0.998958]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[-46.16, 2.086, 3.71, 0.917, 0.1, 1], [-33.372114, 2.343182, 2.869399]], [[-14.0, 0.52, 9.81, 1.66, 0.1, 4], [2.334284, 0.778834, 0.849263]], [[-48.19, 1.061, 1.62, 1.175, 0.05, 8], [-19.879265, 1.371061, 1.411083]]], [[[-96.75, 1.217, 1.62, 1.754, 0.1, 3], [-73.460993, 1.490891, 5.451773]], [[-76.38, 1.024, 9.81, 2.214, 0.01, 12], [-67.585639, 1.52995, 19.17464]], [[-83.79, 0.173, 3.71, 1.845, 0.02, 8], [-80.740326, 0.491946, 6.15544]], [[-23.11, -1.127, 3.71, 2.161, 0.05, 12], [-51.918037, -0.480511, 3.611448]], [[-0.87, -2.001, 3.71, 0.717, 0.01, 9], [-11.099352, -1.952345, 1.029521]], [[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]], [[-33.78, 1.282, 9.81, 1.531, 0.02, 11], [-13.503901, 1.857824, 4.46032]]], [[[58.85, 2.786, 1.62, 1.006, 0.1, 15], [231.321212, 2.02495, 4.723108]], [[30.18, -0.932, 1.62, 2.192, 0.01, 1], [29.644939, -0.935685, 2.568152]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[54.84, 2.14, 9.81, 2.419, 0.05, 17], [78.883524, -1.215601, 23.47846]], [[-124.01, -2.0, 9.81, 0.956, 0.01, 22], [-138.432846, -0.351896, 16.451635]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[-128.69, 0.495, 1.62, 0.634, 0.02, 17], [-112.075118, 1.232516, 1.718384]], [[-110.76, -2.814, 3.71, 2.007, 0.02, 2], [-117.130885, -2.74648, 26.03361]]], [[[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[-113.01, 0.456, 3.71, 2.862, 0.02, 8], [-107.945309, 0.65005, 15.620154]], [[43.67, 2.105, 3.71, 1.353, 0.1, 4], [81.504443, 1.136382, 5.460058]], [[-127.14, 1.913, 3.71, 0.583, 0.05, 8], [-56.637765, 4.279127, 4.08531]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[-82.24, 1.27, 3.71, 0.87, 0.02, 1], [-80.736269, 1.354341, 3.402275]], [[110.18, -0.771, 9.81, 2.517, 0.1, 14], [-88.732128, -1.858959, 35.091939]], [[-106.5, -1.792, 9.81, 2.402, 0.1, 18], [21.607042, 3.609021, 39.230431]]], [[[131.06, -2.348, 3.71, 2.875, 0.02, 8], [108.752695, -2.525573, 40.456508]], [[60.87, -1.327, 9.81, 1.243, 0.02, 23], [-5.782756, -3.126505, 7.613507]], [[136.06, 2.649, 3.71, 2.91, 0.1, 14], [364.986535, 3.370148, 48.130759]], [[-62.89, 1.692, 9.81, 1.301, 0.1, 23], [-74.638718, -0.090938, 9.388882]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[89.14, -1.757, 9.81, 1.734, 0.05, 20], [-85.551502, -1.947875, 21.395293]], [[93.33, 1.174, 1.62, 1.89, 0.05, 7], [113.927, 0.884673, 5.701427]], [[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[89.648705, -0.245248, 0.240587][89.648705, -0.245248, 19.619998]Failed
case 1[-86.931847, 3.501277, 49.525252][-86.931847, 3.501277, 74.05287]Failed
case 2[88.722943, -0.020319, 0.102431][88.722943, -0.020319, 4.480049]Failed
case 3[14.725067, -1.294022, 2.604874][14.725067, -1.294022, 0.998958]Failed
case 4[0.0, 0.0, 9.81][0.0, 0.0, 0.0]Failed
case 5[-33.372114, 2.343182, 5.149573][-33.372114, 2.343182, 2.869399]Failed
case 6[2.334284, 0.778834, 17.106837][2.334284, 0.778834, 0.849263]Failed
case 7[-19.879265, 1.371061, 3.087729][-19.879265, 1.371061, 1.411083]Failed

SHA-256 / 6f606dbbf043cf1c495621cfbb47a9fdfe72b35eff22362a7460730beaa176b4

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 * 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(math.radians(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 = [[[[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[-123.14, 2.838, 9.81, 2.8, 0.02, 10], [-86.931847, 3.501277, 74.05287]], [[87.81, 0.31, 3.71, 1.235, 0.01, 11], [88.722943, -0.020319, 4.480049]], [[47.0, -0.897, 1.62, 1.061, 0.05, 10], [14.725067, -1.294022, 0.998958]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[-46.16, 2.086, 3.71, 0.917, 0.1, 1], [-33.372114, 2.343182, 2.869399]], [[-14.0, 0.52, 9.81, 1.66, 0.1, 4], [2.334284, 0.778834, 0.849263]], [[-48.19, 1.061, 1.62, 1.175, 0.05, 8], [-19.879265, 1.371061, 1.411083]]], [[[-96.75, 1.217, 1.62, 1.754, 0.1, 3], [-73.460993, 1.490891, 5.451773]], [[-76.38, 1.024, 9.81, 2.214, 0.01, 12], [-67.585639, 1.52995, 19.17464]], [[-83.79, 0.173, 3.71, 1.845, 0.02, 8], [-80.740326, 0.491946, 6.15544]], [[-23.11, -1.127, 3.71, 2.161, 0.05, 12], [-51.918037, -0.480511, 3.611448]], [[-0.87, -2.001, 3.71, 0.717, 0.01, 9], [-11.099352, -1.952345, 1.029521]], [[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]], [[-33.78, 1.282, 9.81, 1.531, 0.02, 11], [-13.503901, 1.857824, 4.46032]]], [[[58.85, 2.786, 1.62, 1.006, 0.1, 15], [231.321212, 2.02495, 4.723108]], [[30.18, -0.932, 1.62, 2.192, 0.01, 1], [29.644939, -0.935685, 2.568152]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[54.84, 2.14, 9.81, 2.419, 0.05, 17], [78.883524, -1.215601, 23.47846]], [[-124.01, -2.0, 9.81, 0.956, 0.01, 22], [-138.432846, -0.351896, 16.451635]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[-128.69, 0.495, 1.62, 0.634, 0.02, 17], [-112.075118, 1.232516, 1.718384]], [[-110.76, -2.814, 3.71, 2.007, 0.02, 2], [-117.130885, -2.74648, 26.03361]]], [[[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[-113.01, 0.456, 3.71, 2.862, 0.02, 8], [-107.945309, 0.65005, 15.620154]], [[43.67, 2.105, 3.71, 1.353, 0.1, 4], [81.504443, 1.136382, 5.460058]], [[-127.14, 1.913, 3.71, 0.583, 0.05, 8], [-56.637765, 4.279127, 4.08531]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[-82.24, 1.27, 3.71, 0.87, 0.02, 1], [-80.736269, 1.354341, 3.402275]], [[110.18, -0.771, 9.81, 2.517, 0.1, 14], [-88.732128, -1.858959, 35.091939]], [[-106.5, -1.792, 9.81, 2.402, 0.1, 18], [21.607042, 3.609021, 39.230431]]], [[[131.06, -2.348, 3.71, 2.875, 0.02, 8], [108.752695, -2.525573, 40.456508]], [[60.87, -1.327, 9.81, 1.243, 0.02, 23], [-5.782756, -3.126505, 7.613507]], [[136.06, 2.649, 3.71, 2.91, 0.1, 14], [364.986535, 3.370148, 48.130759]], [[-62.89, 1.692, 9.81, 1.301, 0.1, 23], [-74.638718, -0.090938, 9.388882]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[89.14, -1.757, 9.81, 1.734, 0.05, 20], [-85.551502, -1.947875, 21.395293]], [[93.33, 1.174, 1.62, 1.89, 0.05, 7], [113.927, 0.884673, 5.701427]], [[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[89.648705, -0.245248, 0.127608][89.648705, -0.245248, 19.619998]Failed
case 1[-86.931847, 3.501277, 48.064691][-86.931847, 3.501277, 74.05287]Failed
case 2[88.722943, -0.020319, 0.001988][88.722943, -0.020319, 4.480049]Failed
case 3[14.725067, -1.294022, 0.942523][14.725067, -1.294022, 0.998958]Failed
case 4[0.0, 0.0, 0.0][0.0, 0.0, 0.0]Passed
case 5[-33.372114, 2.343182, 2.308627][-33.372114, 2.343182, 2.869399]Failed
case 6[2.334284, 0.778834, 0.835754][2.334284, 0.778834, 0.849263]Failed
case 7[-19.879265, 1.371061, 1.297691][-19.879265, 1.371061, 1.411083]Failed

SHA-256 / 064a0c0090bd1ca38582fa07a1c4ac3c7a08b5108126dfb9078584cf691ae765

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 = [[[[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[-123.14, 2.838, 9.81, 2.8, 0.02, 10], [-86.931847, 3.501277, 74.05287]], [[87.81, 0.31, 3.71, 1.235, 0.01, 11], [88.722943, -0.020319, 4.480049]], [[47.0, -0.897, 1.62, 1.061, 0.05, 10], [14.725067, -1.294022, 0.998958]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[-46.16, 2.086, 3.71, 0.917, 0.1, 1], [-33.372114, 2.343182, 2.869399]], [[-14.0, 0.52, 9.81, 1.66, 0.1, 4], [2.334284, 0.778834, 0.849263]], [[-48.19, 1.061, 1.62, 1.175, 0.05, 8], [-19.879265, 1.371061, 1.411083]]], [[[-96.75, 1.217, 1.62, 1.754, 0.1, 3], [-73.460993, 1.490891, 5.451773]], [[-76.38, 1.024, 9.81, 2.214, 0.01, 12], [-67.585639, 1.52995, 19.17464]], [[-83.79, 0.173, 3.71, 1.845, 0.02, 8], [-80.740326, 0.491946, 6.15544]], [[-23.11, -1.127, 3.71, 2.161, 0.05, 12], [-51.918037, -0.480511, 3.611448]], [[-0.87, -2.001, 3.71, 0.717, 0.01, 9], [-11.099352, -1.952345, 1.029521]], [[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]], [[-33.78, 1.282, 9.81, 1.531, 0.02, 11], [-13.503901, 1.857824, 4.46032]]], [[[58.85, 2.786, 1.62, 1.006, 0.1, 15], [231.321212, 2.02495, 4.723108]], [[30.18, -0.932, 1.62, 2.192, 0.01, 1], [29.644939, -0.935685, 2.568152]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[54.84, 2.14, 9.81, 2.419, 0.05, 17], [78.883524, -1.215601, 23.47846]], [[-124.01, -2.0, 9.81, 0.956, 0.01, 22], [-138.432846, -0.351896, 16.451635]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[-128.69, 0.495, 1.62, 0.634, 0.02, 17], [-112.075118, 1.232516, 1.718384]], [[-110.76, -2.814, 3.71, 2.007, 0.02, 2], [-117.130885, -2.74648, 26.03361]]], [[[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[-113.01, 0.456, 3.71, 2.862, 0.02, 8], [-107.945309, 0.65005, 15.620154]], [[43.67, 2.105, 3.71, 1.353, 0.1, 4], [81.504443, 1.136382, 5.460058]], [[-127.14, 1.913, 3.71, 0.583, 0.05, 8], [-56.637765, 4.279127, 4.08531]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[-82.24, 1.27, 3.71, 0.87, 0.02, 1], [-80.736269, 1.354341, 3.402275]], [[110.18, -0.771, 9.81, 2.517, 0.1, 14], [-88.732128, -1.858959, 35.091939]], [[-106.5, -1.792, 9.81, 2.402, 0.1, 18], [21.607042, 3.609021, 39.230431]]], [[[131.06, -2.348, 3.71, 2.875, 0.02, 8], [108.752695, -2.525573, 40.456508]], [[60.87, -1.327, 9.81, 1.243, 0.02, 23], [-5.782756, -3.126505, 7.613507]], [[136.06, 2.649, 3.71, 2.91, 0.1, 14], [364.986535, 3.370148, 48.130759]], [[-62.89, 1.692, 9.81, 1.301, 0.1, 23], [-74.638718, -0.090938, 9.388882]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[89.14, -1.757, 9.81, 1.734, 0.05, 20], [-85.551502, -1.947875, 21.395293]], [[93.33, 1.174, 1.62, 1.89, 0.05, 7], [113.927, 0.884673, 5.701427]], [[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[89.648705, -0.245248, 19.619998][89.648705, -0.245248, 19.619998]Passed
case 1[-86.931847, 3.501277, 74.05287][-86.931847, 3.501277, 74.05287]Passed
case 2[88.722943, -0.020319, 4.480049][88.722943, -0.020319, 4.480049]Passed
case 3[14.725067, -1.294022, 0.998958][14.725067, -1.294022, 0.998958]Passed
case 4[0.0, 0.0, 0.0][0.0, 0.0, 0.0]Passed
case 5[-33.372114, 2.343182, 2.869399][-33.372114, 2.343182, 2.869399]Passed
case 6[2.334284, 0.778834, 0.849263][2.334284, 0.778834, 0.849263]Passed
case 7[-19.879265, 1.371061, 1.411083][-19.879265, 1.371061, 1.411083]Passed

SHA-256 / f29daf925ed7697c88ac7857ceeeec37545663d9bb54293a58fba3a12f3431e8

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

Case digest / 39482794e824dac09140190dd97d333e0fc538df77755eaa6d2c7892c7fdea8a