FA-86876 / Physics integrator stability / Open access
Verlet drift applies the full acceleration dt squared term · case 01
Swing amplitude is systematically wrong at larger time steps.
ROOT CAUSE
The position update adds a0*dt*dt instead of 0.5*a0*dt*dt.
VERIFIED REPAIR
Keep the Taylor factor one half on the acceleration term.
Unsuccessful approach: Dropping one dt factor (0.5*a0*dt) makes the term dimensionally a velocity.
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 + 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 = [[[[146.96, 2.386, 9.81, 2.012, 0.02, 14], [181.32788, 2.029348, 47.805782]], [[87.81, 0.31, 3.71, 1.235, 0.01, 11], [88.722943, -0.020319, 4.480049]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[-123.14, 2.838, 9.81, 2.8, 0.02, 10], [-86.931847, 3.501277, 74.05287]], [[47.0, -0.897, 1.62, 1.061, 0.05, 10], [14.725067, -1.294022, 0.998958]], [[-28.02, 2.032, 1.62, 1.549, 0.1, 6], [42.741056, 1.954585, 5.249758]], [[-46.16, 2.086, 3.71, 0.917, 0.1, 1], [-33.372114, 2.343182, 2.869399]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]]], [[[-33.78, 1.282, 9.81, 1.531, 0.02, 11], [-13.503901, 1.857824, 4.46032]], [[-94.77, -0.214, 1.62, 2.169, 0.01, 2], [-95.006698, -0.199117, 3.913697]], [[-83.79, 0.173, 3.71, 1.845, 0.02, 8], [-80.740326, 0.491946, 6.15544]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[98.27, -2.427, 1.62, 2.657, 0.01, 5], [91.273751, -2.457359, 25.71527]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[-97.92, -2.539, 9.81, 0.573, 0.1, 7], [22.897574, 6.430386, 7.231114]], [[112.84, -2.85, 1.62, 0.918, 0.05, 18], [-61.55966, -3.341714, 5.484281]]], [[[99.84, 2.522, 9.81, 2.3, 0.1, 15], [139.078626, -1.167819, 43.219065]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[-67.59, -1.125, 9.81, 2.341, 0.01, 8], [-72.028677, -0.810202, 17.678207]], [[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]], [[57.82, -0.433, 9.81, 1.682, 0.02, 15], [38.351771, -1.766761, 7.976004]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[-107.93, 0.087, 1.62, 2.144, 0.02, 12], [-105.543534, 0.260467, 4.559945]]], [[[-18.92, -0.987, 9.81, 1.541, 0.02, 1], [-20.027365, -0.944557, 1.973481]], [[-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]], [[-76.73, -1.475, 1.62, 0.956, 0.05, 16], [-113.853312, -0.165621, 2.187552]], [[-73.59, 1.271, 9.81, 0.814, 0.1, 5], [24.673646, 4.040094, 6.136624]], [[3.44, 0.497, 1.62, 1.232, 0.05, 2], [6.260347, 0.485875, 0.191061]], [[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]]], [[[-107.87, -1.067, 3.71, 0.509, 0.02, 19], [-102.945115, 1.531755, 2.61536]], [[-74.15, 2.008, 3.71, 2.881, 0.1, 22], [159.697406, 0.957205, 24.515454]], [[136.06, 2.649, 3.71, 2.91, 0.1, 14], [364.986535, 3.370148, 48.130759]], [[-34.09, 2.305, 3.71, 0.783, 0.02, 21], [27.079805, 2.429454, 2.12776]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[-71.0, 1.904, 3.71, 2.676, 0.05, 22], [67.389778, 1.946364, 19.675143]], [[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]]]]
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 | [181.085303, 2.025805, 47.778458] | [181.32788, 2.029348, 47.805782] | Failed |
| case 1 | [88.628333, -0.020312, 4.472485] | [88.722943, -0.020319, 4.480049] | Failed |
| case 2 | [0.0, 0.0, 0.0] | [0.0, 0.0, 0.0] | Passed |
| case 3 | [-86.552656, 3.501628, 73.881] | [-86.931847, 3.501277, 74.05287] | Failed |
| case 4 | [14.172494, -1.290162, 0.989207] | [14.725067, -1.294022, 0.998958] | Failed |
| case 5 | [42.658572, 1.953096, 5.240328] | [42.741056, 1.954585, 5.249758] | Failed |
| case 6 | [-32.536128, 2.340705, 2.83752] | [-33.372114, 2.343182, 2.869399] | Failed |
| case 7 | [89.297411, -0.245241, 19.499702] | [89.648705, -0.245248, 19.619998] | Failed |
SHA-256 / 98aaf6b8289c4cc15685aaf73609985a7b452e068506d7fc89f2de95d01db005
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
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 = [[[[146.96, 2.386, 9.81, 2.012, 0.02, 14], [181.32788, 2.029348, 47.805782]], [[87.81, 0.31, 3.71, 1.235, 0.01, 11], [88.722943, -0.020319, 4.480049]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[-123.14, 2.838, 9.81, 2.8, 0.02, 10], [-86.931847, 3.501277, 74.05287]], [[47.0, -0.897, 1.62, 1.061, 0.05, 10], [14.725067, -1.294022, 0.998958]], [[-28.02, 2.032, 1.62, 1.549, 0.1, 6], [42.741056, 1.954585, 5.249758]], [[-46.16, 2.086, 3.71, 0.917, 0.1, 1], [-33.372114, 2.343182, 2.869399]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]]], [[[-33.78, 1.282, 9.81, 1.531, 0.02, 11], [-13.503901, 1.857824, 4.46032]], [[-94.77, -0.214, 1.62, 2.169, 0.01, 2], [-95.006698, -0.199117, 3.913697]], [[-83.79, 0.173, 3.71, 1.845, 0.02, 8], [-80.740326, 0.491946, 6.15544]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[98.27, -2.427, 1.62, 2.657, 0.01, 5], [91.273751, -2.457359, 25.71527]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[-97.92, -2.539, 9.81, 0.573, 0.1, 7], [22.897574, 6.430386, 7.231114]], [[112.84, -2.85, 1.62, 0.918, 0.05, 18], [-61.55966, -3.341714, 5.484281]]], [[[99.84, 2.522, 9.81, 2.3, 0.1, 15], [139.078626, -1.167819, 43.219065]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[-67.59, -1.125, 9.81, 2.341, 0.01, 8], [-72.028677, -0.810202, 17.678207]], [[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]], [[57.82, -0.433, 9.81, 1.682, 0.02, 15], [38.351771, -1.766761, 7.976004]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[-107.93, 0.087, 1.62, 2.144, 0.02, 12], [-105.543534, 0.260467, 4.559945]]], [[[-18.92, -0.987, 9.81, 1.541, 0.02, 1], [-20.027365, -0.944557, 1.973481]], [[-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]], [[-76.73, -1.475, 1.62, 0.956, 0.05, 16], [-113.853312, -0.165621, 2.187552]], [[-73.59, 1.271, 9.81, 0.814, 0.1, 5], [24.673646, 4.040094, 6.136624]], [[3.44, 0.497, 1.62, 1.232, 0.05, 2], [6.260347, 0.485875, 0.191061]], [[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]]], [[[-107.87, -1.067, 3.71, 0.509, 0.02, 19], [-102.945115, 1.531755, 2.61536]], [[-74.15, 2.008, 3.71, 2.881, 0.1, 22], [159.697406, 0.957205, 24.515454]], [[136.06, 2.649, 3.71, 2.91, 0.1, 14], [364.986535, 3.370148, 48.130759]], [[-34.09, 2.305, 3.71, 0.783, 0.02, 21], [27.079805, 2.429454, 2.12776]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[-71.0, 1.904, 3.71, 2.676, 0.05, 22], [67.389778, 1.946364, 19.675143]], [[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]]]]
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 | [163.89867, 1.818575, 45.395228] | [181.32788, 2.029348, 47.805782] | Failed |
| case 1 | [79.411715, -0.018195, 3.740186] | [88.722943, -0.020319, 4.480049] | Failed |
| case 2 | [0.0, 0.0, 0.0] | [0.0, 0.0, 0.0] | Passed |
| case 3 | [-68.162751, 3.506974, 65.462258] | [-86.931847, 3.501277, 74.05287] | Failed |
| case 4 | [5.813768, -1.225457, 0.854114] | [14.725067, -1.294022, 0.998958] | Failed |
| case 5 | [41.722002, 1.942499, 5.163257] | [42.741056, 1.954585, 5.249758] | Failed |
| case 6 | [-25.848237, 2.320103, 2.603572] | [-33.372114, 2.343182, 2.869399] | Failed |
| case 7 | [82.974105, -0.244329, 17.339516] | [89.648705, -0.245248, 19.619998] | Failed |
SHA-256 / 827201d83ffcebce06640c10339b6d870786af42ae8278ec56d3183334d42804
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 = [[[[146.96, 2.386, 9.81, 2.012, 0.02, 14], [181.32788, 2.029348, 47.805782]], [[87.81, 0.31, 3.71, 1.235, 0.01, 11], [88.722943, -0.020319, 4.480049]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[-123.14, 2.838, 9.81, 2.8, 0.02, 10], [-86.931847, 3.501277, 74.05287]], [[47.0, -0.897, 1.62, 1.061, 0.05, 10], [14.725067, -1.294022, 0.998958]], [[-28.02, 2.032, 1.62, 1.549, 0.1, 6], [42.741056, 1.954585, 5.249758]], [[-46.16, 2.086, 3.71, 0.917, 0.1, 1], [-33.372114, 2.343182, 2.869399]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]]], [[[-33.78, 1.282, 9.81, 1.531, 0.02, 11], [-13.503901, 1.857824, 4.46032]], [[-94.77, -0.214, 1.62, 2.169, 0.01, 2], [-95.006698, -0.199117, 3.913697]], [[-83.79, 0.173, 3.71, 1.845, 0.02, 8], [-80.740326, 0.491946, 6.15544]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[98.27, -2.427, 1.62, 2.657, 0.01, 5], [91.273751, -2.457359, 25.71527]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[-97.92, -2.539, 9.81, 0.573, 0.1, 7], [22.897574, 6.430386, 7.231114]], [[112.84, -2.85, 1.62, 0.918, 0.05, 18], [-61.55966, -3.341714, 5.484281]]], [[[99.84, 2.522, 9.81, 2.3, 0.1, 15], [139.078626, -1.167819, 43.219065]], [[0.0, 0.0, 9.81, 1.0, 0.01, 5], [0.0, 0.0, 0.0]], [[-67.59, -1.125, 9.81, 2.341, 0.01, 8], [-72.028677, -0.810202, 17.678207]], [[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]], [[57.82, -0.433, 9.81, 1.682, 0.02, 15], [38.351771, -1.766761, 7.976004]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[-107.93, 0.087, 1.62, 2.144, 0.02, 12], [-105.543534, 0.260467, 4.559945]]], [[[-18.92, -0.987, 9.81, 1.541, 0.02, 1], [-20.027365, -0.944557, 1.973481]], [[-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]], [[-76.73, -1.475, 1.62, 0.956, 0.05, 16], [-113.853312, -0.165621, 2.187552]], [[-73.59, 1.271, 9.81, 0.814, 0.1, 5], [24.673646, 4.040094, 6.136624]], [[3.44, 0.497, 1.62, 1.232, 0.05, 2], [6.260347, 0.485875, 0.191061]], [[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]]], [[[-107.87, -1.067, 3.71, 0.509, 0.02, 19], [-102.945115, 1.531755, 2.61536]], [[-74.15, 2.008, 3.71, 2.881, 0.1, 22], [159.697406, 0.957205, 24.515454]], [[136.06, 2.649, 3.71, 2.91, 0.1, 14], [364.986535, 3.370148, 48.130759]], [[-34.09, 2.305, 3.71, 0.783, 0.02, 21], [27.079805, 2.429454, 2.12776]], [[90.0, 0.0, 9.81, 2.0, 0.05, 1], [89.648705, -0.245248, 19.619998]], [[-71.0, 1.904, 3.71, 2.676, 0.05, 22], [67.389778, 1.946364, 19.675143]], [[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]]]]
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 | [181.32788, 2.029348, 47.805782] | [181.32788, 2.029348, 47.805782] | Passed |
| case 1 | [88.722943, -0.020319, 4.480049] | [88.722943, -0.020319, 4.480049] | Passed |
| case 2 | [0.0, 0.0, 0.0] | [0.0, 0.0, 0.0] | Passed |
| case 3 | [-86.931847, 3.501277, 74.05287] | [-86.931847, 3.501277, 74.05287] | Passed |
| case 4 | [14.725067, -1.294022, 0.998958] | [14.725067, -1.294022, 0.998958] | Passed |
| case 5 | [42.741056, 1.954585, 5.249758] | [42.741056, 1.954585, 5.249758] | Passed |
| case 6 | [-33.372114, 2.343182, 2.869399] | [-33.372114, 2.343182, 2.869399] | Passed |
| case 7 | [89.648705, -0.245248, 19.619998] | [89.648705, -0.245248, 19.619998] | Passed |
SHA-256 / 7c1468aaa7afac643cf3044ef7e9f39331e6ac1bf78a055e51479526fcd2371d
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.487357+00:00.
Case digest / cc75b77527f891cc949140107fe48325f3d446876612adc0739491e44378d893