FAILURE MAP
← Case archive

FA-86901 / Physics integrator stability / Open access

Pendulum kinetic energy uses one power of the rod length · case 01

Energy diagnostics disagree with the integrated state for any rod length other than one.

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

ROOT CAUSE

Kinetic energy is 0.5*L*omega^2 instead of 0.5*L^2*omega^2.

VERIFIED REPAIR

Tangential speed is L*omega, so square the length.

Unsuccessful approach: Dropping the one half keeps L^2 but doubles the kinetic energy.

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 * 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 = [[[[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[-90.84, -2.902, 9.81, 1.0, 0.01, 5], [-98.45263, -2.413579, 14.16467]], [[140.32, -0.763, 9.81, 2.845, 0.05, 10], [99.827729, -2.171297, 51.752981]], [[-46.16, 2.086, 3.71, 0.917, 0.1, 1], [-33.372114, 2.343182, 2.869399]], [[102.57, 2.504, 3.71, 1.926, 0.02, 13], [136.50743, 2.076955, 20.33009]], [[146.96, 2.386, 9.81, 2.012, 0.02, 14], [181.32788, 2.029348, 47.805782]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[59.39, -2.805, 1.62, 2.933, 0.1, 10], [-102.829148, -2.65809, 36.196698]]], [[[86.7, 0.204, 3.71, 2.169, 0.01, 16], [87.317304, -0.06935, 7.681666]], [[-83.79, 0.173, 3.71, 1.845, 0.02, 8], [-80.740326, 0.491946, 6.15544]], [[17.69, -0.321, 9.81, 1.677, 0.01, 2], [17.301897, -0.356178, 0.922799]], [[-64.79, -0.621, 3.71, 0.595, 0.1, 10], [31.033368, 2.385827, 1.323544]], [[-62.84, 1.046, 9.81, 1.991, 0.1, 7], [21.788999, 2.386036, 12.679491]], [[40.42, -1.773, 3.71, 2.446, 0.02, 3], [34.227319, -1.828163, 11.569611]], [[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]]], [[[77.83, -0.227, 3.71, 2.715, 0.1, 16], [-20.773668, -1.423055, 8.118515]], [[56.22, -2.435, 3.71, 2.416, 0.1, 20], [-172.255925, -1.090215, 21.313846]], [[15.42, -0.469, 3.71, 0.999, 0.05, 2], [12.462645, -0.558661, 0.243071]], [[2.07, -0.507, 3.71, 1.386, 0.05, 21], [-17.959323, 0.011038, 0.25066]], [[-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]], [[8.44, 1.131, 9.81, 2.769, 0.01, 10], [14.734384, 1.059672, 5.198135]], [[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]], [[-106.5, -1.792, 9.81, 2.402, 0.1, 18], [21.607042, 3.609021, 39.230431]], [[-8.71, -0.809, 1.62, 1.564, 0.05, 3], [-15.53832, -0.776336, 0.82973]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[43.97, -2.412, 9.81, 2.949, 0.02, 7], [23.490378, -2.670315, 33.403404]], [[-83.33, 1.121, 9.81, 0.637, 0.01, 16], [-62.037587, 3.481303, 5.77773]], [[93.61, 1.047, 3.71, 1.718, 0.05, 12], [107.880879, -0.204741, 8.392642]], [[-113.01, 0.456, 3.71, 2.862, 0.02, 8], [-107.945309, 0.65005, 15.620154]]], [[[94.96, 0.484, 9.81, 1.629, 0.1, 17], [-95.192476, 0.430719, 17.672903]], [[6.11, 2.524, 3.71, 1.024, 0.05, 17], [83.139578, 0.208416, 3.368014]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[-111.15, -0.214, 3.71, 1.183, 0.02, 14], [-108.016567, 0.607424, 6.004571]], [[127.01, 1.447, 9.81, 1.012, 0.02, 22], [123.863552, -1.720919, 16.976148]], [[-74.15, 2.008, 3.71, 2.881, 0.1, 22], [159.697406, 0.957205, 24.515454]], [[89.14, -1.757, 9.81, 1.734, 0.05, 20], [-85.551502, -1.947875, 21.395293]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]]]]
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.0, 0.0, 0.0][0.0, 0.0, 0.0]Passed
case 1[-98.45263, -2.413579, 14.16467][-98.45263, -2.413579, 14.16467]Passed
case 2[99.827729, -2.171297, 39.379634][99.827729, -2.171297, 51.752981]Failed
case 3[-33.372114, 2.343182, 3.078343][-33.372114, 2.343182, 2.869399]Failed
case 4[136.50743, 2.076955, 16.483363][136.50743, 2.076955, 20.33009]Failed
case 5[181.32788, 2.029348, 43.613103][181.32788, 2.029348, 47.805782]Failed
case 6[89.648705, -0.245248, 19.559852][89.648705, -0.245248, 19.619998]Failed
case 7[-102.829148, -2.65809, 16.167969][-102.829148, -2.65809, 36.196698]Failed

SHA-256 / 7eac1b7336b6d6f4f5ecd815d05f35e6f99e76192ee757b7c80e52fcae1b960b

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 = 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 = [[[[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[-90.84, -2.902, 9.81, 1.0, 0.01, 5], [-98.45263, -2.413579, 14.16467]], [[140.32, -0.763, 9.81, 2.845, 0.05, 10], [99.827729, -2.171297, 51.752981]], [[-46.16, 2.086, 3.71, 0.917, 0.1, 1], [-33.372114, 2.343182, 2.869399]], [[102.57, 2.504, 3.71, 1.926, 0.02, 13], [136.50743, 2.076955, 20.33009]], [[146.96, 2.386, 9.81, 2.012, 0.02, 14], [181.32788, 2.029348, 47.805782]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[59.39, -2.805, 1.62, 2.933, 0.1, 10], [-102.829148, -2.65809, 36.196698]]], [[[86.7, 0.204, 3.71, 2.169, 0.01, 16], [87.317304, -0.06935, 7.681666]], [[-83.79, 0.173, 3.71, 1.845, 0.02, 8], [-80.740326, 0.491946, 6.15544]], [[17.69, -0.321, 9.81, 1.677, 0.01, 2], [17.301897, -0.356178, 0.922799]], [[-64.79, -0.621, 3.71, 0.595, 0.1, 10], [31.033368, 2.385827, 1.323544]], [[-62.84, 1.046, 9.81, 1.991, 0.1, 7], [21.788999, 2.386036, 12.679491]], [[40.42, -1.773, 3.71, 2.446, 0.02, 3], [34.227319, -1.828163, 11.569611]], [[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]]], [[[77.83, -0.227, 3.71, 2.715, 0.1, 16], [-20.773668, -1.423055, 8.118515]], [[56.22, -2.435, 3.71, 2.416, 0.1, 20], [-172.255925, -1.090215, 21.313846]], [[15.42, -0.469, 3.71, 0.999, 0.05, 2], [12.462645, -0.558661, 0.243071]], [[2.07, -0.507, 3.71, 1.386, 0.05, 21], [-17.959323, 0.011038, 0.25066]], [[-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]], [[8.44, 1.131, 9.81, 2.769, 0.01, 10], [14.734384, 1.059672, 5.198135]], [[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]], [[-106.5, -1.792, 9.81, 2.402, 0.1, 18], [21.607042, 3.609021, 39.230431]], [[-8.71, -0.809, 1.62, 1.564, 0.05, 3], [-15.53832, -0.776336, 0.82973]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[43.97, -2.412, 9.81, 2.949, 0.02, 7], [23.490378, -2.670315, 33.403404]], [[-83.33, 1.121, 9.81, 0.637, 0.01, 16], [-62.037587, 3.481303, 5.77773]], [[93.61, 1.047, 3.71, 1.718, 0.05, 12], [107.880879, -0.204741, 8.392642]], [[-113.01, 0.456, 3.71, 2.862, 0.02, 8], [-107.945309, 0.65005, 15.620154]]], [[[94.96, 0.484, 9.81, 1.629, 0.1, 17], [-95.192476, 0.430719, 17.672903]], [[6.11, 2.524, 3.71, 1.024, 0.05, 17], [83.139578, 0.208416, 3.368014]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[-111.15, -0.214, 3.71, 1.183, 0.02, 14], [-108.016567, 0.607424, 6.004571]], [[127.01, 1.447, 9.81, 1.012, 0.02, 22], [123.863552, -1.720919, 16.976148]], [[-74.15, 2.008, 3.71, 2.881, 0.1, 22], [159.697406, 0.957205, 24.515454]], [[89.14, -1.757, 9.81, 1.734, 0.05, 20], [-85.551502, -1.947875, 21.395293]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]]]]
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.0, 0.0, 0.0][0.0, 0.0, 0.0]Passed
case 1[-98.45263, -2.413579, 17.077352][-98.45263, -2.413579, 14.16467]Failed
case 2[99.827729, -2.171297, 70.832749][99.827729, -2.171297, 51.752981]Failed
case 3[-33.372114, 2.343182, 5.17785][-33.372114, 2.343182, 2.869399]Failed
case 4[136.50743, 2.076955, 28.330948][136.50743, 2.076955, 20.33009]Failed
case 5[181.32788, 2.029348, 56.141426][181.32788, 2.029348, 47.805782]Failed
case 6[89.648705, -0.245248, 19.740291][89.648705, -0.245248, 19.619998]Failed
case 7[-102.829148, -2.65809, 66.586899][-102.829148, -2.65809, 36.196698]Failed

SHA-256 / bde0f5ae8f50e76672b4db28f72824aa9b3e42177de894c572cbf937436147d1

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 = [[[[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[-90.84, -2.902, 9.81, 1.0, 0.01, 5], [-98.45263, -2.413579, 14.16467]], [[140.32, -0.763, 9.81, 2.845, 0.05, 10], [99.827729, -2.171297, 51.752981]], [[-46.16, 2.086, 3.71, 0.917, 0.1, 1], [-33.372114, 2.343182, 2.869399]], [[102.57, 2.504, 3.71, 1.926, 0.02, 13], [136.50743, 2.076955, 20.33009]], [[146.96, 2.386, 9.81, 2.012, 0.02, 14], [181.32788, 2.029348, 47.805782]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[59.39, -2.805, 1.62, 2.933, 0.1, 10], [-102.829148, -2.65809, 36.196698]]], [[[86.7, 0.204, 3.71, 2.169, 0.01, 16], [87.317304, -0.06935, 7.681666]], [[-83.79, 0.173, 3.71, 1.845, 0.02, 8], [-80.740326, 0.491946, 6.15544]], [[17.69, -0.321, 9.81, 1.677, 0.01, 2], [17.301897, -0.356178, 0.922799]], [[-64.79, -0.621, 3.71, 0.595, 0.1, 10], [31.033368, 2.385827, 1.323544]], [[-62.84, 1.046, 9.81, 1.991, 0.1, 7], [21.788999, 2.386036, 12.679491]], [[40.42, -1.773, 3.71, 2.446, 0.02, 3], [34.227319, -1.828163, 11.569611]], [[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]]], [[[77.83, -0.227, 3.71, 2.715, 0.1, 16], [-20.773668, -1.423055, 8.118515]], [[56.22, -2.435, 3.71, 2.416, 0.1, 20], [-172.255925, -1.090215, 21.313846]], [[15.42, -0.469, 3.71, 0.999, 0.05, 2], [12.462645, -0.558661, 0.243071]], [[2.07, -0.507, 3.71, 1.386, 0.05, 21], [-17.959323, 0.011038, 0.25066]], [[-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]], [[8.44, 1.131, 9.81, 2.769, 0.01, 10], [14.734384, 1.059672, 5.198135]], [[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]], [[-106.5, -1.792, 9.81, 2.402, 0.1, 18], [21.607042, 3.609021, 39.230431]], [[-8.71, -0.809, 1.62, 1.564, 0.05, 3], [-15.53832, -0.776336, 0.82973]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[43.97, -2.412, 9.81, 2.949, 0.02, 7], [23.490378, -2.670315, 33.403404]], [[-83.33, 1.121, 9.81, 0.637, 0.01, 16], [-62.037587, 3.481303, 5.77773]], [[93.61, 1.047, 3.71, 1.718, 0.05, 12], [107.880879, -0.204741, 8.392642]], [[-113.01, 0.456, 3.71, 2.862, 0.02, 8], [-107.945309, 0.65005, 15.620154]]], [[[94.96, 0.484, 9.81, 1.629, 0.1, 17], [-95.192476, 0.430719, 17.672903]], [[6.11, 2.524, 3.71, 1.024, 0.05, 17], [83.139578, 0.208416, 3.368014]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[-111.15, -0.214, 3.71, 1.183, 0.02, 14], [-108.016567, 0.607424, 6.004571]], [[127.01, 1.447, 9.81, 1.012, 0.02, 22], [123.863552, -1.720919, 16.976148]], [[-74.15, 2.008, 3.71, 2.881, 0.1, 22], [159.697406, 0.957205, 24.515454]], [[89.14, -1.757, 9.81, 1.734, 0.05, 20], [-85.551502, -1.947875, 21.395293]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]]]]
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.0, 0.0, 0.0][0.0, 0.0, 0.0]Passed
case 1[-98.45263, -2.413579, 14.16467][-98.45263, -2.413579, 14.16467]Passed
case 2[99.827729, -2.171297, 51.752981][99.827729, -2.171297, 51.752981]Passed
case 3[-33.372114, 2.343182, 2.869399][-33.372114, 2.343182, 2.869399]Passed
case 4[136.50743, 2.076955, 20.33009][136.50743, 2.076955, 20.33009]Passed
case 5[181.32788, 2.029348, 47.805782][181.32788, 2.029348, 47.805782]Passed
case 6[89.648705, -0.245248, 19.619998][89.648705, -0.245248, 19.619998]Passed
case 7[-102.829148, -2.65809, 36.196698][-102.829148, -2.65809, 36.196698]Passed

SHA-256 / bb27e0647e573baf708967d976e88800e678b5e3b319b0d7bc78712929e1e3f6

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

Case digest / c801aec3d8e7662921d8b2b87e5ea962bfba190b76cbf103be6b80375194ccd9