FAILURE MAP
← Case archive

FA-86886 / Physics integrator stability / Open access

Pendulum treats the degree input as radians · case 01

A 30 degree release swings as if released from 30 radians.

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

ROOT CAUSE

The initial angle is used directly without converting degrees to radians.

VERIFIED REPAIR

Convert with math.radians before integrating.

Unsuccessful approach: Multiplying by pi/360 halves the 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 = 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.84, -2.902, 9.81, 1.0, 0.01, 5], [-98.45263, -2.413579, 14.16467]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[47.0, -0.897, 1.62, 1.061, 0.05, 10], [14.725067, -1.294022, 0.998958]], [[87.81, 0.31, 3.71, 1.235, 0.01, 11], [88.722943, -0.020319, 4.480049]], [[-129.15, 2.597, 9.81, 1.034, 0.02, 2], [-122.853785, 2.903609, 20.153379]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[63.66, 2.606, 3.71, 0.985, 0.02, 8], [84.9478, 2.028054, 5.327806]], [[140.32, -0.763, 9.81, 2.845, 0.05, 10], [99.827729, -2.171297, 51.752981]]], [[[132.37, 1.164, 3.71, 1.897, 0.05, 8], [153.323241, 0.70366, 14.217487]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[-148.24, 0.711, 1.62, 1.415, 0.1, 3], [-134.302426, 0.923966, 4.748011]], [[112.84, -2.85, 1.62, 0.918, 0.05, 18], [-61.55966, -3.341714, 5.484281]], [[50.01, 1.967, 9.81, 2.774, 0.01, 17], [66.748485, 1.454635, 24.611383]], [[-83.79, 0.173, 3.71, 1.845, 0.02, 8], [-80.740326, 0.491946, 6.15544]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[132.44, 0.741, 3.71, 2.113, 0.1, 16], [120.242008, -1.072974, 14.357572]]], [[[-132.91, 1.906, 3.71, 1.161, 0.05, 10], [-57.95129, 3.371139, 9.680955]], [[-124.01, -2.0, 9.81, 0.956, 0.01, 22], [-138.432846, -0.351896, 16.451635]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[-87.9, -2.746, 9.81, 0.669, 0.05, 22], [97.511749, 1.623222, 8.010482]], [[-147.17, 0.123, 9.81, 2.31, 0.1, 15], [54.162842, 3.475648, 41.623788]], [[-23.64, 0.163, 9.81, 2.99, 0.05, 7], [-16.088501, 0.565375, 2.577636]], [[-65.1, -0.228, 1.62, 2.64, 0.05, 2], [-66.246499, -0.172077, 2.657281]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]]], [[[-106.5, -1.792, 9.81, 2.402, 0.1, 18], [21.607042, 3.609021, 39.230431]], [[-73.59, 1.271, 9.81, 0.814, 0.1, 5], [24.673646, 4.040094, 6.136624]], [[7.34, 1.721, 9.81, 1.693, 0.01, 8], [15.045893, 1.630818, 4.380856]], [[37.92, 1.544, 1.62, 2.753, 0.02, 25], [78.875105, 1.297176, 9.975805]], [[129.71, 0.223, 1.62, 2.683, 0.01, 21], [131.813176, 0.127103, 7.302408]], [[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]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]]], [[[-16.99, -0.173, 3.71, 2.908, 0.05, 16], [-17.529938, 0.150997, 0.597434]], [[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]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[94.96, 0.484, 9.81, 1.629, 0.1, 17], [-95.192476, 0.430719, 17.672903]], [[116.79, -2.258, 1.62, 2.493, 0.1, 6], [32.851266, -2.601404, 21.675418]], [[-87.63, 2.437, 9.81, 2.308, 0.01, 7], [-77.262703, 2.731595, 37.522955]], [[-54.71, -2.248, 1.62, 1.318, 0.02, 30], [-120.050645, -1.550048, 5.291224]]]]
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[-5212.908837, -2.807238, 23.485257][-98.45263, -2.413579, 14.16467]Failed
case 1[0.0, 0.0, 0.0][0.0, 0.0, 0.0]Passed
case 2[2664.219414, -1.165293, 3.877547][14.725067, -1.294022, 0.998958]Failed
case 3[5033.244457, 0.355016, 0.127928][88.722943, -0.020319, 4.480049]Failed
case 4[-7393.934398, 2.487216, 23.295608][-122.853785, 2.903609, 20.153379]Failed
case 5[5156.3061, -0.219552, 28.411336][89.648705, -0.245248, 19.619998]Failed
case 6[3669.088127, 2.094466, 4.478083][84.9478, 2.028054, 5.327806]Failed
case 7[7994.835201, -2.409456, 44.103292][99.827729, -2.171297, 51.752981]Failed

SHA-256 / 8cce3ab98993af2df7d5186de7cb4ad9bb315c4dd0ff4fa9210f7ff4e05e7f48

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 = theta_deg * math.pi / 360
    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.84, -2.902, 9.81, 1.0, 0.01, 5], [-98.45263, -2.413579, 14.16467]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[47.0, -0.897, 1.62, 1.061, 0.05, 10], [14.725067, -1.294022, 0.998958]], [[87.81, 0.31, 3.71, 1.235, 0.01, 11], [88.722943, -0.020319, 4.480049]], [[-129.15, 2.597, 9.81, 1.034, 0.02, 2], [-122.853785, 2.903609, 20.153379]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[63.66, 2.606, 3.71, 0.985, 0.02, 8], [84.9478, 2.028054, 5.327806]], [[140.32, -0.763, 9.81, 2.845, 0.05, 10], [99.827729, -2.171297, 51.752981]]], [[[132.37, 1.164, 3.71, 1.897, 0.05, 8], [153.323241, 0.70366, 14.217487]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[-148.24, 0.711, 1.62, 1.415, 0.1, 3], [-134.302426, 0.923966, 4.748011]], [[112.84, -2.85, 1.62, 0.918, 0.05, 18], [-61.55966, -3.341714, 5.484281]], [[50.01, 1.967, 9.81, 2.774, 0.01, 17], [66.748485, 1.454635, 24.611383]], [[-83.79, 0.173, 3.71, 1.845, 0.02, 8], [-80.740326, 0.491946, 6.15544]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[132.44, 0.741, 3.71, 2.113, 0.1, 16], [120.242008, -1.072974, 14.357572]]], [[[-132.91, 1.906, 3.71, 1.161, 0.05, 10], [-57.95129, 3.371139, 9.680955]], [[-124.01, -2.0, 9.81, 0.956, 0.01, 22], [-138.432846, -0.351896, 16.451635]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[-87.9, -2.746, 9.81, 0.669, 0.05, 22], [97.511749, 1.623222, 8.010482]], [[-147.17, 0.123, 9.81, 2.31, 0.1, 15], [54.162842, 3.475648, 41.623788]], [[-23.64, 0.163, 9.81, 2.99, 0.05, 7], [-16.088501, 0.565375, 2.577636]], [[-65.1, -0.228, 1.62, 2.64, 0.05, 2], [-66.246499, -0.172077, 2.657281]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]]], [[[-106.5, -1.792, 9.81, 2.402, 0.1, 18], [21.607042, 3.609021, 39.230431]], [[-73.59, 1.271, 9.81, 0.814, 0.1, 5], [24.673646, 4.040094, 6.136624]], [[7.34, 1.721, 9.81, 1.693, 0.01, 8], [15.045893, 1.630818, 4.380856]], [[37.92, 1.544, 1.62, 2.753, 0.02, 25], [78.875105, 1.297176, 9.975805]], [[129.71, 0.223, 1.62, 2.683, 0.01, 21], [131.813176, 0.127103, 7.302408]], [[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]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]]], [[[-16.99, -0.173, 3.71, 2.908, 0.05, 16], [-17.529938, 0.150997, 0.597434]], [[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]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[94.96, 0.484, 9.81, 1.629, 0.1, 17], [-95.192476, 0.430719, 17.672903]], [[116.79, -2.258, 1.62, 2.493, 0.1, 6], [32.851266, -2.601404, 21.675418]], [[-87.63, 2.437, 9.81, 2.308, 0.01, 7], [-77.262703, 2.731595, 37.522955]], [[-54.71, -2.248, 1.62, 1.318, 0.02, 30], [-120.050645, -1.550048, 5.291224]]]]
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[-53.211794, -2.52988, 7.135343][-98.45263, -2.413579, 14.16467]Failed
case 1[0.0, 0.0, 0.0][0.0, 0.0, 0.0]Passed
case 2[-4.909826, -1.022943, 0.595291][14.725067, -1.294022, 0.998958]Failed
case 3[45.129824, 0.077824, 1.353961][88.722943, -0.020319, 4.480049]Failed
case 4[-58.23565, 2.930105, 9.393336][-122.853785, 2.903609, 20.153379]Failed
case 5[44.751597, -0.173041, 5.746434][89.648705, -0.245248, 19.619998]Failed
case 6[53.974473, 2.196017, 3.844511][84.9478, 2.028054, 5.327806]Failed
case 7[27.339813, -2.088209, 20.76501][99.827729, -2.171297, 51.752981]Failed

SHA-256 / 359ccec6952eb26e9ec0a3ddac6d01a74c8ee758da350b874112c1672646548f

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.84, -2.902, 9.81, 1.0, 0.01, 5], [-98.45263, -2.413579, 14.16467]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[47.0, -0.897, 1.62, 1.061, 0.05, 10], [14.725067, -1.294022, 0.998958]], [[87.81, 0.31, 3.71, 1.235, 0.01, 11], [88.722943, -0.020319, 4.480049]], [[-129.15, 2.597, 9.81, 1.034, 0.02, 2], [-122.853785, 2.903609, 20.153379]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[63.66, 2.606, 3.71, 0.985, 0.02, 8], [84.9478, 2.028054, 5.327806]], [[140.32, -0.763, 9.81, 2.845, 0.05, 10], [99.827729, -2.171297, 51.752981]]], [[[132.37, 1.164, 3.71, 1.897, 0.05, 8], [153.323241, 0.70366, 14.217487]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[-148.24, 0.711, 1.62, 1.415, 0.1, 3], [-134.302426, 0.923966, 4.748011]], [[112.84, -2.85, 1.62, 0.918, 0.05, 18], [-61.55966, -3.341714, 5.484281]], [[50.01, 1.967, 9.81, 2.774, 0.01, 17], [66.748485, 1.454635, 24.611383]], [[-83.79, 0.173, 3.71, 1.845, 0.02, 8], [-80.740326, 0.491946, 6.15544]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[132.44, 0.741, 3.71, 2.113, 0.1, 16], [120.242008, -1.072974, 14.357572]]], [[[-132.91, 1.906, 3.71, 1.161, 0.05, 10], [-57.95129, 3.371139, 9.680955]], [[-124.01, -2.0, 9.81, 0.956, 0.01, 22], [-138.432846, -0.351896, 16.451635]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[-87.9, -2.746, 9.81, 0.669, 0.05, 22], [97.511749, 1.623222, 8.010482]], [[-147.17, 0.123, 9.81, 2.31, 0.1, 15], [54.162842, 3.475648, 41.623788]], [[-23.64, 0.163, 9.81, 2.99, 0.05, 7], [-16.088501, 0.565375, 2.577636]], [[-65.1, -0.228, 1.62, 2.64, 0.05, 2], [-66.246499, -0.172077, 2.657281]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]]], [[[-106.5, -1.792, 9.81, 2.402, 0.1, 18], [21.607042, 3.609021, 39.230431]], [[-73.59, 1.271, 9.81, 0.814, 0.1, 5], [24.673646, 4.040094, 6.136624]], [[7.34, 1.721, 9.81, 1.693, 0.01, 8], [15.045893, 1.630818, 4.380856]], [[37.92, 1.544, 1.62, 2.753, 0.02, 25], [78.875105, 1.297176, 9.975805]], [[129.71, 0.223, 1.62, 2.683, 0.01, 21], [131.813176, 0.127103, 7.302408]], [[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]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]]], [[[-16.99, -0.173, 3.71, 2.908, 0.05, 16], [-17.529938, 0.150997, 0.597434]], [[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]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[94.96, 0.484, 9.81, 1.629, 0.1, 17], [-95.192476, 0.430719, 17.672903]], [[116.79, -2.258, 1.62, 2.493, 0.1, 6], [32.851266, -2.601404, 21.675418]], [[-87.63, 2.437, 9.81, 2.308, 0.01, 7], [-77.262703, 2.731595, 37.522955]], [[-54.71, -2.248, 1.62, 1.318, 0.02, 30], [-120.050645, -1.550048, 5.291224]]]]
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[-98.45263, -2.413579, 14.16467][-98.45263, -2.413579, 14.16467]Passed
case 1[0.0, 0.0, 0.0][0.0, 0.0, 0.0]Passed
case 2[14.725067, -1.294022, 0.998958][14.725067, -1.294022, 0.998958]Passed
case 3[88.722943, -0.020319, 4.480049][88.722943, -0.020319, 4.480049]Passed
case 4[-122.853785, 2.903609, 20.153379][-122.853785, 2.903609, 20.153379]Passed
case 5[89.648705, -0.245248, 19.619998][89.648705, -0.245248, 19.619998]Passed
case 6[84.9478, 2.028054, 5.327806][84.9478, 2.028054, 5.327806]Passed
case 7[99.827729, -2.171297, 51.752981][99.827729, -2.171297, 51.752981]Passed

SHA-256 / ed5f2c0c454a5d21780163940089151f1e3373cfb05f5d458d1cd9c93018cab1

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

Case digest / ca0b924779e8824d98084dcbeb280f5daa9ad76ed852f68b3905ac340b37cede