FA-86881 / Physics integrator stability / Open access
Verlet never carries the new acceleration into the next step · case 01
After the first step the integrator keeps using the initial-angle acceleration.
ROOT CAUSE
The a0 = a1 hand-off at the end of the loop is missing.
VERIFIED REPAIR
Carry the freshly evaluated acceleration into the next step.
Unsuccessful approach: Recomputing a0 from an extrapolated previous angle is not the acceleration at the current 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
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 = [[[[45.28, -1.957, 9.81, 0.998, 0.1, 14], [7.145368, 3.058241, 4.733766]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[-88.3, 0.349, 3.71, 1.329, 0.02, 13], [-77.714345, 1.069639, 4.891833]], [[116.98, -2.664, 3.71, 2.568, 0.1, 10], [-63.542799, -3.110777, 37.190477]], [[149.67, 1.436, 1.62, 2.272, 0.05, 9], [185.401488, 1.368665, 12.179767]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[-46.19, -1.108, 9.81, 1.086, 0.1, 13], [51.096139, -0.291005, 4.012934]], [[-46.16, 2.086, 3.71, 0.917, 0.1, 1], [-33.372114, 2.343182, 2.869399]]], [[[50.01, 1.967, 9.81, 2.774, 0.01, 17], [66.748485, 1.454635, 24.611383]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[-71.06, 1.343, 9.81, 1.769, 0.05, 2], [-61.883545, 1.851333, 14.538443]], [[39.61, 2.112, 9.81, 1.387, 0.02, 3], [46.385577, 1.822399, 7.415255]], [[55.82, -1.801, 1.62, 2.039, 0.02, 1], [53.748674, -1.81398, 8.190126]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[-73.6, -2.997, 9.81, 1.448, 0.01, 6], [-83.223165, -2.599131, 19.610793]], [[-12.71, 2.5, 1.62, 2.44, 0.05, 9], [51.203117, 2.405701, 18.704048]]], [[[100.85, -0.237, 1.62, 0.605, 0.1, 1], [98.738703, -0.500821, 1.174909]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[71.19, 0.807, 9.81, 1.157, 0.1, 14], [-71.100761, 0.82223, 8.1263]], [[86.31, -0.863, 1.62, 2.852, 0.1, 6], [51.020408, -1.177026, 7.348228]], [[-110.76, -2.814, 3.71, 2.007, 0.02, 2], [-117.130885, -2.74648, 26.03361]], [[-87.9, -2.746, 9.81, 0.669, 0.05, 22], [97.511749, 1.623222, 8.010482]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[-133.47, 2.786, 9.81, 0.714, 0.01, 8], [-118.744784, 3.66775, 13.801778]]], [[[148.74, 0.509, 3.71, 1.117, 0.05, 7], [153.321028, -0.038026, 7.847849]], [[-82.24, 1.27, 3.71, 0.87, 0.02, 1], [-80.736269, 1.354341, 3.402275]], [[88.81, -0.345, 3.71, 1.561, 0.01, 14], [84.709379, -0.677201, 5.816049]], [[-54.29, 1.6, 9.81, 1.365, 0.05, 24], [60.323988, -1.135897, 7.963031]], [[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]], [[93.61, 1.047, 3.71, 1.718, 0.05, 12], [107.880879, -0.204741, 8.392642]], [[12.62, 0.495, 9.81, 1.593, 0.01, 11], [15.238893, 0.330964, 0.688467]]], [[[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[101.22, 0.152, 3.71, 1.491, 0.02, 6], [101.25883, -0.140698, 6.633614]], [[64.12, 1.072, 9.81, 2.267, 0.1, 5], [65.863237, -0.95491, 15.488411]], [[-113.82, 1.15, 9.81, 2.142, 0.01, 4], [-110.991123, 1.319308, 32.533389]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[-44.62, 1.36, 9.81, 1.251, 0.05, 21], [52.156799, -0.560005, 4.988612]], [[66.17, -1.096, 3.71, 2.374, 0.1, 1], [59.480851, -1.23479, 8.631365]], [[89.14, -1.757, 9.81, 1.734, 0.05, 20], [-85.551502, -1.947875, 21.395293]]]]
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 | [-246.405975, -5.404692, 28.255999] | [7.145368, 3.058241, 4.733766] | Failed |
| case 1 | [0.0, 0.0, 0.0] | [0.0, 0.0, 0.0] | Passed |
| case 2 | [-77.705781, 1.071748, 4.895102] | [-77.714345, 1.069639, 4.891833] | Failed |
| case 3 | [-68.028743, -3.461247, 45.465284] | [-63.542799, -3.110777, 37.190477] | Failed |
| case 4 | [185.002272, 1.32632, 11.88755] | [185.401488, 1.368665, 12.179767] | Failed |
| case 5 | [89.648705, -0.245248, 19.619998] | [89.648705, -0.245248, 19.619998] | Passed |
| case 6 | [113.1036, 2.720061, 19.19712] | [51.096139, -0.291005, 4.012934] | Failed |
| case 7 | [-33.372114, 2.343182, 2.869399] | [-33.372114, 2.343182, 2.869399] | Passed |
SHA-256 / 88ef12b167e01031733bb7bcbc0343c612920c83e99127c2ee65233f3b19a4e2
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 = acc(th - w * dt)
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 = [[[[45.28, -1.957, 9.81, 0.998, 0.1, 14], [7.145368, 3.058241, 4.733766]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[-88.3, 0.349, 3.71, 1.329, 0.02, 13], [-77.714345, 1.069639, 4.891833]], [[116.98, -2.664, 3.71, 2.568, 0.1, 10], [-63.542799, -3.110777, 37.190477]], [[149.67, 1.436, 1.62, 2.272, 0.05, 9], [185.401488, 1.368665, 12.179767]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[-46.19, -1.108, 9.81, 1.086, 0.1, 13], [51.096139, -0.291005, 4.012934]], [[-46.16, 2.086, 3.71, 0.917, 0.1, 1], [-33.372114, 2.343182, 2.869399]]], [[[50.01, 1.967, 9.81, 2.774, 0.01, 17], [66.748485, 1.454635, 24.611383]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[-71.06, 1.343, 9.81, 1.769, 0.05, 2], [-61.883545, 1.851333, 14.538443]], [[39.61, 2.112, 9.81, 1.387, 0.02, 3], [46.385577, 1.822399, 7.415255]], [[55.82, -1.801, 1.62, 2.039, 0.02, 1], [53.748674, -1.81398, 8.190126]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[-73.6, -2.997, 9.81, 1.448, 0.01, 6], [-83.223165, -2.599131, 19.610793]], [[-12.71, 2.5, 1.62, 2.44, 0.05, 9], [51.203117, 2.405701, 18.704048]]], [[[100.85, -0.237, 1.62, 0.605, 0.1, 1], [98.738703, -0.500821, 1.174909]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[71.19, 0.807, 9.81, 1.157, 0.1, 14], [-71.100761, 0.82223, 8.1263]], [[86.31, -0.863, 1.62, 2.852, 0.1, 6], [51.020408, -1.177026, 7.348228]], [[-110.76, -2.814, 3.71, 2.007, 0.02, 2], [-117.130885, -2.74648, 26.03361]], [[-87.9, -2.746, 9.81, 0.669, 0.05, 22], [97.511749, 1.623222, 8.010482]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[-133.47, 2.786, 9.81, 0.714, 0.01, 8], [-118.744784, 3.66775, 13.801778]]], [[[148.74, 0.509, 3.71, 1.117, 0.05, 7], [153.321028, -0.038026, 7.847849]], [[-82.24, 1.27, 3.71, 0.87, 0.02, 1], [-80.736269, 1.354341, 3.402275]], [[88.81, -0.345, 3.71, 1.561, 0.01, 14], [84.709379, -0.677201, 5.816049]], [[-54.29, 1.6, 9.81, 1.365, 0.05, 24], [60.323988, -1.135897, 7.963031]], [[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]], [[93.61, 1.047, 3.71, 1.718, 0.05, 12], [107.880879, -0.204741, 8.392642]], [[12.62, 0.495, 9.81, 1.593, 0.01, 11], [15.238893, 0.330964, 0.688467]]], [[[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[101.22, 0.152, 3.71, 1.491, 0.02, 6], [101.25883, -0.140698, 6.633614]], [[64.12, 1.072, 9.81, 2.267, 0.1, 5], [65.863237, -0.95491, 15.488411]], [[-113.82, 1.15, 9.81, 2.142, 0.01, 4], [-110.991123, 1.319308, 32.533389]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[-44.62, 1.36, 9.81, 1.251, 0.05, 21], [52.156799, -0.560005, 4.988612]], [[66.17, -1.096, 3.71, 2.374, 0.1, 1], [59.480851, -1.23479, 8.631365]], [[89.14, -1.757, 9.81, 1.734, 0.05, 20], [-85.551502, -1.947875, 21.395293]]]]
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.587999, 3.994733, 7.947577] | [7.145368, 3.058241, 4.733766] | Failed |
| case 1 | [0.0, 0.0, 0.0] | [0.0, 0.0, 0.0] | Passed |
| case 2 | [-77.711835, 1.070164, 4.892614] | [-77.714345, 1.069639, 4.891833] | Failed |
| case 3 | [-65.541177, -3.220973, 39.791117] | [-63.542799, -3.110777, 37.190477] | Failed |
| case 4 | [185.28061, 1.359047, 12.112771] | [185.401488, 1.368665, 12.179767] | Failed |
| case 5 | [89.648705, -0.245248, 19.619998] | [89.648705, -0.245248, 19.619998] | Passed |
| case 6 | [64.639095, -0.157647, 6.105154] | [51.096139, -0.291005, 4.012934] | Failed |
| case 7 | [-33.372114, 2.343182, 2.869399] | [-33.372114, 2.343182, 2.869399] | Passed |
SHA-256 / df29db479b780d7e671e889b547142e625f2a0b79ba53aac7b2b6e5aae58b36e
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 = [[[[45.28, -1.957, 9.81, 0.998, 0.1, 14], [7.145368, 3.058241, 4.733766]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[-88.3, 0.349, 3.71, 1.329, 0.02, 13], [-77.714345, 1.069639, 4.891833]], [[116.98, -2.664, 3.71, 2.568, 0.1, 10], [-63.542799, -3.110777, 37.190477]], [[149.67, 1.436, 1.62, 2.272, 0.05, 9], [185.401488, 1.368665, 12.179767]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[-46.19, -1.108, 9.81, 1.086, 0.1, 13], [51.096139, -0.291005, 4.012934]], [[-46.16, 2.086, 3.71, 0.917, 0.1, 1], [-33.372114, 2.343182, 2.869399]]], [[[50.01, 1.967, 9.81, 2.774, 0.01, 17], [66.748485, 1.454635, 24.611383]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[-71.06, 1.343, 9.81, 1.769, 0.05, 2], [-61.883545, 1.851333, 14.538443]], [[39.61, 2.112, 9.81, 1.387, 0.02, 3], [46.385577, 1.822399, 7.415255]], [[55.82, -1.801, 1.62, 2.039, 0.02, 1], [53.748674, -1.81398, 8.190126]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[-73.6, -2.997, 9.81, 1.448, 0.01, 6], [-83.223165, -2.599131, 19.610793]], [[-12.71, 2.5, 1.62, 2.44, 0.05, 9], [51.203117, 2.405701, 18.704048]]], [[[100.85, -0.237, 1.62, 0.605, 0.1, 1], [98.738703, -0.500821, 1.174909]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[71.19, 0.807, 9.81, 1.157, 0.1, 14], [-71.100761, 0.82223, 8.1263]], [[86.31, -0.863, 1.62, 2.852, 0.1, 6], [51.020408, -1.177026, 7.348228]], [[-110.76, -2.814, 3.71, 2.007, 0.02, 2], [-117.130885, -2.74648, 26.03361]], [[-87.9, -2.746, 9.81, 0.669, 0.05, 22], [97.511749, 1.623222, 8.010482]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[-133.47, 2.786, 9.81, 0.714, 0.01, 8], [-118.744784, 3.66775, 13.801778]]], [[[148.74, 0.509, 3.71, 1.117, 0.05, 7], [153.321028, -0.038026, 7.847849]], [[-82.24, 1.27, 3.71, 0.87, 0.02, 1], [-80.736269, 1.354341, 3.402275]], [[88.81, -0.345, 3.71, 1.561, 0.01, 14], [84.709379, -0.677201, 5.816049]], [[-54.29, 1.6, 9.81, 1.365, 0.05, 24], [60.323988, -1.135897, 7.963031]], [[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]], [[93.61, 1.047, 3.71, 1.718, 0.05, 12], [107.880879, -0.204741, 8.392642]], [[12.62, 0.495, 9.81, 1.593, 0.01, 11], [15.238893, 0.330964, 0.688467]]], [[[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[101.22, 0.152, 3.71, 1.491, 0.02, 6], [101.25883, -0.140698, 6.633614]], [[64.12, 1.072, 9.81, 2.267, 0.1, 5], [65.863237, -0.95491, 15.488411]], [[-113.82, 1.15, 9.81, 2.142, 0.01, 4], [-110.991123, 1.319308, 32.533389]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[-44.62, 1.36, 9.81, 1.251, 0.05, 21], [52.156799, -0.560005, 4.988612]], [[66.17, -1.096, 3.71, 2.374, 0.1, 1], [59.480851, -1.23479, 8.631365]], [[89.14, -1.757, 9.81, 1.734, 0.05, 20], [-85.551502, -1.947875, 21.395293]]]]
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 | [7.145368, 3.058241, 4.733766] | [7.145368, 3.058241, 4.733766] | Passed |
| case 1 | [0.0, 0.0, 0.0] | [0.0, 0.0, 0.0] | Passed |
| case 2 | [-77.714345, 1.069639, 4.891833] | [-77.714345, 1.069639, 4.891833] | Passed |
| case 3 | [-63.542799, -3.110777, 37.190477] | [-63.542799, -3.110777, 37.190477] | Passed |
| case 4 | [185.401488, 1.368665, 12.179767] | [185.401488, 1.368665, 12.179767] | Passed |
| case 5 | [89.648705, -0.245248, 19.619998] | [89.648705, -0.245248, 19.619998] | Passed |
| case 6 | [51.096139, -0.291005, 4.012934] | [51.096139, -0.291005, 4.012934] | Passed |
| case 7 | [-33.372114, 2.343182, 2.869399] | [-33.372114, 2.343182, 2.869399] | Passed |
SHA-256 / dc40cd5a138affddc52d10b32d308c1419f17bb8ff4f005b60832eef8e0ac3c6
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.698313+00:00.
Case digest / 86b5658862b26fa335f9f664b22514f9cf776883fcfae9f89cecf1f601516152