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.
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 fixture | Actual | Expected | Outcome |
|---|---|---|---|
| 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 fixture | Actual | Expected | Outcome |
|---|---|---|---|
| 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 fixture | Actual | Expected | Outcome |
|---|---|---|---|
| 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