FA-86891 / Physics integrator stability / Open access
Pendulum force uses the small-angle linearization · case 01
Large releases oscillate with the harmonic period instead of the slower nonlinear period.
ROOT CAUSE
The acceleration uses -(g/L)*theta instead of -(g/L)*sin(theta).
VERIFIED REPAIR
Evaluate the full sine restoring term.
Unsuccessful approach: A cubic Taylor truncation still diverges from sine at large angles.
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) * 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 = [[[[149.67, 1.436, 1.62, 2.272, 0.05, 9], [185.401488, 1.368665, 12.179767]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[-75.32, -0.382, 3.71, 1.505, 0.02, 6], [-76.959694, -0.094613, 4.333835]], [[94.74, 2.643, 9.81, 1.283, 0.05, 8], [122.553091, -0.123795, 19.371253]], [[66.41, -2.95, 9.81, 2.372, 0.01, 5], [57.693591, -3.132551, 38.438603]], [[6.7, 0.98, 3.71, 1.742, 0.02, 10], [17.492223, 0.890259, 1.501395]], [[47.0, -0.897, 1.62, 1.061, 0.05, 10], [14.725067, -1.294022, 0.998958]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]]], [[[-83.79, 0.173, 3.71, 1.845, 0.02, 8], [-80.740326, 0.491946, 6.15544]], [[-58.54, -2.701, 1.62, 0.993, 0.02, 16], [-103.538582, -2.197553, 4.366181]], [[67.56, 1.326, 1.62, 2.503, 0.01, 15], [78.561436, 1.23324, 8.014877]], [[50.01, 1.967, 9.81, 2.774, 0.01, 17], [66.748485, 1.454635, 24.611383]], [[-96.75, 1.217, 1.62, 1.754, 0.1, 3], [-73.460993, 1.490891, 5.451773]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[76.08, 0.426, 1.62, 1.418, 0.05, 9], [80.568171, -0.079174, 1.927017]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]]], [[[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[34.97, -0.247, 9.81, 2.137, 0.02, 1], [34.656809, -0.299415, 3.92428]], [[-128.69, 0.495, 1.62, 0.634, 0.02, 17], [-112.075118, 1.232516, 1.718384]], [[-23.33, 0.186, 9.81, 1.417, 0.02, 9], [-18.974881, 0.643458, 1.171022]], [[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]], [[111.32, -2.123, 3.71, 2.98, 0.01, 22], [82.883153, -2.39183, 35.087773]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]]], [[[38.9, -2.033, 9.81, 2.054, 0.05, 2], [26.450962, -2.29074, 13.178713]], [[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]], [[-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]], [[148.89, 2.983, 9.81, 1.797, 0.02, 24], [227.600517, 3.299133, 47.089245]], [[93.17, 0.932, 1.62, 2.524, 0.01, 10], [98.326808, 0.868164, 7.081809]], [[-18.92, -0.987, 9.81, 1.541, 0.02, 1], [-20.027365, -0.944557, 1.973481]]], [[[-44.62, 1.36, 9.81, 1.251, 0.05, 21], [52.156799, -0.560005, 4.988612]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[131.06, -2.348, 3.71, 2.875, 0.02, 8], [108.752695, -2.525573, 40.456508]], [[54.65, -2.722, 3.71, 2.224, 0.02, 26], [-31.882444, -2.88299, 21.800199]], [[-77.72, -1.078, 3.71, 1.566, 0.02, 9], [-86.66729, -0.655603, 5.999139]], [[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]], [[-113.82, 1.15, 9.81, 2.142, 0.01, 4], [-110.991123, 1.319308, 32.533389]]]]
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 | [175.1431, 0.515537, 8.034035] | [185.401488, 1.368665, 12.179767] | Failed |
| case 1 | [0.0, 0.0, 0.0] | [0.0, 0.0, 0.0] | Passed |
| case 2 | [-76.598353, 0.011304, 4.289563] | [-76.959694, -0.094613, 4.333835] | Failed |
| case 3 | [91.481287, -2.896915, 19.818677] | [122.553091, -0.123795, 19.371253] | Failed |
| case 4 | [57.629812, -3.174023, 39.152499] | [57.693591, -3.132551, 38.438603] | Failed |
| case 5 | [17.489381, 0.889448, 1.499109] | [17.492223, 0.890259, 1.501395] | Failed |
| case 6 | [14.200666, -1.318086, 1.030408] | [14.725067, -1.294022, 0.998958] | Failed |
| case 7 | [89.448188, -0.384057, 19.726043] | [89.648705, -0.245248, 19.619998] | Failed |
SHA-256 / ab2584d6d9abcfb2274cf7a456f878c78c00e1a9607ff9b813a9fa30d86b54b6
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) * (t - t ** 3 / 6)
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 = [[[[149.67, 1.436, 1.62, 2.272, 0.05, 9], [185.401488, 1.368665, 12.179767]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[-75.32, -0.382, 3.71, 1.505, 0.02, 6], [-76.959694, -0.094613, 4.333835]], [[94.74, 2.643, 9.81, 1.283, 0.05, 8], [122.553091, -0.123795, 19.371253]], [[66.41, -2.95, 9.81, 2.372, 0.01, 5], [57.693591, -3.132551, 38.438603]], [[6.7, 0.98, 3.71, 1.742, 0.02, 10], [17.492223, 0.890259, 1.501395]], [[47.0, -0.897, 1.62, 1.061, 0.05, 10], [14.725067, -1.294022, 0.998958]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]]], [[[-83.79, 0.173, 3.71, 1.845, 0.02, 8], [-80.740326, 0.491946, 6.15544]], [[-58.54, -2.701, 1.62, 0.993, 0.02, 16], [-103.538582, -2.197553, 4.366181]], [[67.56, 1.326, 1.62, 2.503, 0.01, 15], [78.561436, 1.23324, 8.014877]], [[50.01, 1.967, 9.81, 2.774, 0.01, 17], [66.748485, 1.454635, 24.611383]], [[-96.75, 1.217, 1.62, 1.754, 0.1, 3], [-73.460993, 1.490891, 5.451773]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[76.08, 0.426, 1.62, 1.418, 0.05, 9], [80.568171, -0.079174, 1.927017]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]]], [[[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[34.97, -0.247, 9.81, 2.137, 0.02, 1], [34.656809, -0.299415, 3.92428]], [[-128.69, 0.495, 1.62, 0.634, 0.02, 17], [-112.075118, 1.232516, 1.718384]], [[-23.33, 0.186, 9.81, 1.417, 0.02, 9], [-18.974881, 0.643458, 1.171022]], [[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]], [[111.32, -2.123, 3.71, 2.98, 0.01, 22], [82.883153, -2.39183, 35.087773]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]]], [[[38.9, -2.033, 9.81, 2.054, 0.05, 2], [26.450962, -2.29074, 13.178713]], [[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]], [[-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]], [[148.89, 2.983, 9.81, 1.797, 0.02, 24], [227.600517, 3.299133, 47.089245]], [[93.17, 0.932, 1.62, 2.524, 0.01, 10], [98.326808, 0.868164, 7.081809]], [[-18.92, -0.987, 9.81, 1.541, 0.02, 1], [-20.027365, -0.944557, 1.973481]]], [[[-44.62, 1.36, 9.81, 1.251, 0.05, 21], [52.156799, -0.560005, 4.988612]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[131.06, -2.348, 3.71, 2.875, 0.02, 8], [108.752695, -2.525573, 40.456508]], [[54.65, -2.722, 3.71, 2.224, 0.02, 26], [-31.882444, -2.88299, 21.800199]], [[-77.72, -1.078, 3.71, 1.566, 0.02, 9], [-86.66729, -0.655603, 5.999139]], [[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]], [[-113.82, 1.15, 9.81, 2.142, 0.01, 4], [-110.991123, 1.319308, 32.533389]]]]
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 | [190.81413, 1.8884, 16.499871] | [185.401488, 1.368665, 12.179767] | Failed |
| case 1 | [0.0, 0.0, 0.0] | [0.0, 0.0, 0.0] | Passed |
| case 2 | [-76.993096, -0.104509, 4.339239] | [-76.959694, -0.094613, 4.333835] | Failed |
| case 3 | [130.306728, 0.782359, 21.231776] | [122.553091, -0.123795, 19.371253] | Failed |
| case 4 | [57.697691, -3.130005, 38.395157] | [57.693591, -3.132551, 38.438603] | Failed |
| case 5 | [17.49223, 0.890262, 1.501402] | [17.492223, 0.890259, 1.501395] | Failed |
| case 6 | [14.738868, -1.293454, 0.998236] | [14.725067, -1.294022, 0.998958] | Failed |
| case 7 | [89.675111, -0.226975, 19.611783] | [89.648705, -0.245248, 19.619998] | Failed |
SHA-256 / 402f64154e1a55648741c0bcae58b43d78034842e4b27bec9e001982688ae9d4
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 = [[[[149.67, 1.436, 1.62, 2.272, 0.05, 9], [185.401488, 1.368665, 12.179767]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[-75.32, -0.382, 3.71, 1.505, 0.02, 6], [-76.959694, -0.094613, 4.333835]], [[94.74, 2.643, 9.81, 1.283, 0.05, 8], [122.553091, -0.123795, 19.371253]], [[66.41, -2.95, 9.81, 2.372, 0.01, 5], [57.693591, -3.132551, 38.438603]], [[6.7, 0.98, 3.71, 1.742, 0.02, 10], [17.492223, 0.890259, 1.501395]], [[47.0, -0.897, 1.62, 1.061, 0.05, 10], [14.725067, -1.294022, 0.998958]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]]], [[[-83.79, 0.173, 3.71, 1.845, 0.02, 8], [-80.740326, 0.491946, 6.15544]], [[-58.54, -2.701, 1.62, 0.993, 0.02, 16], [-103.538582, -2.197553, 4.366181]], [[67.56, 1.326, 1.62, 2.503, 0.01, 15], [78.561436, 1.23324, 8.014877]], [[50.01, 1.967, 9.81, 2.774, 0.01, 17], [66.748485, 1.454635, 24.611383]], [[-96.75, 1.217, 1.62, 1.754, 0.1, 3], [-73.460993, 1.490891, 5.451773]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[76.08, 0.426, 1.62, 1.418, 0.05, 9], [80.568171, -0.079174, 1.927017]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]]], [[[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[34.97, -0.247, 9.81, 2.137, 0.02, 1], [34.656809, -0.299415, 3.92428]], [[-128.69, 0.495, 1.62, 0.634, 0.02, 17], [-112.075118, 1.232516, 1.718384]], [[-23.33, 0.186, 9.81, 1.417, 0.02, 9], [-18.974881, 0.643458, 1.171022]], [[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]], [[111.32, -2.123, 3.71, 2.98, 0.01, 22], [82.883153, -2.39183, 35.087773]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]]], [[[38.9, -2.033, 9.81, 2.054, 0.05, 2], [26.450962, -2.29074, 13.178713]], [[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]], [[-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]], [[148.89, 2.983, 9.81, 1.797, 0.02, 24], [227.600517, 3.299133, 47.089245]], [[93.17, 0.932, 1.62, 2.524, 0.01, 10], [98.326808, 0.868164, 7.081809]], [[-18.92, -0.987, 9.81, 1.541, 0.02, 1], [-20.027365, -0.944557, 1.973481]]], [[[-44.62, 1.36, 9.81, 1.251, 0.05, 21], [52.156799, -0.560005, 4.988612]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[131.06, -2.348, 3.71, 2.875, 0.02, 8], [108.752695, -2.525573, 40.456508]], [[54.65, -2.722, 3.71, 2.224, 0.02, 26], [-31.882444, -2.88299, 21.800199]], [[-77.72, -1.078, 3.71, 1.566, 0.02, 9], [-86.66729, -0.655603, 5.999139]], [[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]], [[-113.82, 1.15, 9.81, 2.142, 0.01, 4], [-110.991123, 1.319308, 32.533389]]]]
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 | [185.401488, 1.368665, 12.179767] | [185.401488, 1.368665, 12.179767] | Passed |
| case 1 | [0.0, 0.0, 0.0] | [0.0, 0.0, 0.0] | Passed |
| case 2 | [-76.959694, -0.094613, 4.333835] | [-76.959694, -0.094613, 4.333835] | Passed |
| case 3 | [122.553091, -0.123795, 19.371253] | [122.553091, -0.123795, 19.371253] | Passed |
| case 4 | [57.693591, -3.132551, 38.438603] | [57.693591, -3.132551, 38.438603] | Passed |
| case 5 | [17.492223, 0.890259, 1.501395] | [17.492223, 0.890259, 1.501395] | Passed |
| case 6 | [14.725067, -1.294022, 0.998958] | [14.725067, -1.294022, 0.998958] | Passed |
| case 7 | [89.648705, -0.245248, 19.619998] | [89.648705, -0.245248, 19.619998] | Passed |
SHA-256 / 86fc408e6369591057a22670abca0d93b2c642e03d31c501cc7d4400122f1508
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.779892+00:00.
Case digest / 4f23467744b1bc963b8f8930a341b4bd27d6aa57a136a57b6c534b0d4d5cd581