FA-86931 / Physics integrator stability / Open access
Damping rate multiplies by mass instead of dividing · case 01
Heavy bodies appear strongly damped and light ones barely damped.
ROOT CAUSE
The normalized damping is computed as c*m.
VERIFIED REPAIR
Use c/m as the linear coefficient.
Unsuccessful approach: Using c/(2m) confuses the rate with the damping ratio numerator.
Case contract
solve(k, m, c, dt): classify explicit Euler on x'=v, v'=(-k*x-c*v)/m. Eigenvalues are the roots of s^2+(c/m)s+k/m; amplification is max |1+dt*lambda|. Return ["marginal" if |amp-1|<=1e-9, else "stable" if amp<1, else "unstable", amp 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(k, m, c, dt):
a = c * m
b = k / m
disc = a * a - 4 * b
if disc >= 0:
r = math.sqrt(disc)
lams = [complex((-a + r) / 2, 0), complex((-a - r) / 2, 0)]
else:
r = math.sqrt(-disc)
lams = [complex(-a / 2, r / 2), complex(-a / 2, -r / 2)]
amp = max(abs(1 + dt * lam) for lam in lams)
if abs(amp - 1) <= 1e-9:
label = 'marginal'
elif amp < 1:
label = 'stable'
else:
label = 'unstable'
return [label, round(amp, 6)]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[[[0.0, 1.0, 2.0008, 1.0], ['unstable', 1.0008]], [[42.221, 2.418, 7.819, 0.01], ['stable', 0.984586]], [[16.494, 1.905, 4.155, 0.05], ['stable', 0.955296]], [[28.666, 0.459, 5.82, 0.1], ['stable', 0.597125]], [[42.548, 3.763, 4.806, 0.1], ['stable', 0.992649]], [[0.0, 3.907, 1.097, 1.0], ['marginal', 1.0]], [[0.0, 1.0, 1.0, 0.5], ['marginal', 1.0]], [[0.0, 0.421, 0.691, 0.05], ['marginal', 1.0]]], [[[0.0, 1.0, 2.0008, 1.0], ['unstable', 1.0008]], [[38.018, 1.276, 0.385, 0.2], ['unstable', 1.459946]], [[59.677, 2.374, 3.266, 1.0], ['unstable', 4.976144]], [[6.203, 0.678, 1.745, 0.05], ['stable', 0.945614]], [[0.0, 0.907, 6.001, 0.01], ['marginal', 1.0]], [[0.0, 1.0, 1.0, 0.5], ['marginal', 1.0]], [[0.0, 1.043, 3.236, 0.05], ['marginal', 1.0]], [[18.985, 2.12, 2.861, 1.0], ['unstable', 2.933541]]], [[[48.986, 0.701, 3.497, 0.1], ['unstable', 1.095419]], [[49.989, 2.073, 1.922, 0.8], ['unstable', 3.961243]], [[0.0, 1.763, 0.013, 0.8], ['marginal', 1.0]], [[17.741, 0.849, 5.676, 0.01], ['stable', 0.967075]], [[20.646, 3.925, 4.888, 0.2], ['stable', 0.980477]], [[0.0, 1.0, 2.0008, 1.0], ['unstable', 1.0008]], [[42.196, 3.443, 4.277, 0.8], ['unstable', 2.801748]], [[0.0, 1.0, 1.0, 0.5], ['marginal', 1.0]]], [[[41.131, 0.314, 6.187, 0.4], ['unstable', 3.751925]], [[23.134, 3.325, 2.902, 0.05], ['stable', 0.98679]], [[3.87, 2.12, 0.308, 0.05], ['stable', 0.998649]], [[0.0, 1.0, 2.0008, 1.0], ['unstable', 1.0008]], [[0.0, 3.102, 2.755, 0.05], ['marginal', 1.0]], [[9.079, 3.699, 5.728, 0.05], ['stable', 0.963696]], [[0.0, 1.0, 1.0, 0.5], ['marginal', 1.0]], [[57.413, 2.505, 6.221, 0.2], ['unstable', 1.191674]]], [[[0.0, 1.0, 1.0, 0.5], ['marginal', 1.0]], [[0.0, 1.0, 2.0008, 1.0], ['unstable', 1.0008]], [[0.0, 1.889, 7.242, 0.8], ['unstable', 2.06702]], [[31.247, 2.981, 5.247, 0.01], ['stable', 0.991689]], [[29.999, 0.715, 0.909, 0.01], ['stable', 0.995732]], [[18.339, 0.83, 4.998, 0.4], ['unstable', 1.458271]], [[20.299, 1.465, 6.715, 0.05], ['stable', 0.897474]], [[1.0, 1.0, 3.0, 0.1], ['stable', 0.961803]]]]
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 | ['unstable', 1.0008] | ['unstable', 1.0008] | Passed |
| case 1 | ['stable', 0.990263] | ['stable', 0.984586] | Failed |
| case 2 | ['stable', 0.934449] | ['stable', 0.955296] | Failed |
| case 3 | ['unstable', 1.165072] | ['stable', 0.597125] | Failed |
| case 4 | ['stable', 0.935154] | ['stable', 0.992649] | Failed |
| case 5 | ['unstable', 3.285979] | ['marginal', 1.0] | Failed |
| case 6 | ['marginal', 1.0] | ['marginal', 1.0] | Passed |
| case 7 | ['marginal', 1.0] | ['marginal', 1.0] | Passed |
SHA-256 / a2238a2cd5a3d0fdfcc05da9d2721c20e080f2a4cfa116f60e9e0dff4083d0e6
2 / The unsuccessful fix
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(k, m, c, dt):
a = c / (2 * m)
b = k / m
disc = a * a - 4 * b
if disc >= 0:
r = math.sqrt(disc)
lams = [complex((-a + r) / 2, 0), complex((-a - r) / 2, 0)]
else:
r = math.sqrt(-disc)
lams = [complex(-a / 2, r / 2), complex(-a / 2, -r / 2)]
amp = max(abs(1 + dt * lam) for lam in lams)
if abs(amp - 1) <= 1e-9:
label = 'marginal'
elif amp < 1:
label = 'stable'
else:
label = 'unstable'
return [label, round(amp, 6)]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[[[0.0, 1.0, 2.0008, 1.0], ['unstable', 1.0008]], [[42.221, 2.418, 7.819, 0.01], ['stable', 0.984586]], [[16.494, 1.905, 4.155, 0.05], ['stable', 0.955296]], [[28.666, 0.459, 5.82, 0.1], ['stable', 0.597125]], [[42.548, 3.763, 4.806, 0.1], ['stable', 0.992649]], [[0.0, 3.907, 1.097, 1.0], ['marginal', 1.0]], [[0.0, 1.0, 1.0, 0.5], ['marginal', 1.0]], [[0.0, 0.421, 0.691, 0.05], ['marginal', 1.0]]], [[[0.0, 1.0, 2.0008, 1.0], ['unstable', 1.0008]], [[38.018, 1.276, 0.385, 0.2], ['unstable', 1.459946]], [[59.677, 2.374, 3.266, 1.0], ['unstable', 4.976144]], [[6.203, 0.678, 1.745, 0.05], ['stable', 0.945614]], [[0.0, 0.907, 6.001, 0.01], ['marginal', 1.0]], [[0.0, 1.0, 1.0, 0.5], ['marginal', 1.0]], [[0.0, 1.043, 3.236, 0.05], ['marginal', 1.0]], [[18.985, 2.12, 2.861, 1.0], ['unstable', 2.933541]]], [[[48.986, 0.701, 3.497, 0.1], ['unstable', 1.095419]], [[49.989, 2.073, 1.922, 0.8], ['unstable', 3.961243]], [[0.0, 1.763, 0.013, 0.8], ['marginal', 1.0]], [[17.741, 0.849, 5.676, 0.01], ['stable', 0.967075]], [[20.646, 3.925, 4.888, 0.2], ['stable', 0.980477]], [[0.0, 1.0, 2.0008, 1.0], ['unstable', 1.0008]], [[42.196, 3.443, 4.277, 0.8], ['unstable', 2.801748]], [[0.0, 1.0, 1.0, 0.5], ['marginal', 1.0]]], [[[41.131, 0.314, 6.187, 0.4], ['unstable', 3.751925]], [[23.134, 3.325, 2.902, 0.05], ['stable', 0.98679]], [[3.87, 2.12, 0.308, 0.05], ['stable', 0.998649]], [[0.0, 1.0, 2.0008, 1.0], ['unstable', 1.0008]], [[0.0, 3.102, 2.755, 0.05], ['marginal', 1.0]], [[9.079, 3.699, 5.728, 0.05], ['stable', 0.963696]], [[0.0, 1.0, 1.0, 0.5], ['marginal', 1.0]], [[57.413, 2.505, 6.221, 0.2], ['unstable', 1.191674]]], [[[0.0, 1.0, 1.0, 0.5], ['marginal', 1.0]], [[0.0, 1.0, 2.0008, 1.0], ['unstable', 1.0008]], [[0.0, 1.889, 7.242, 0.8], ['unstable', 2.06702]], [[31.247, 2.981, 5.247, 0.01], ['stable', 0.991689]], [[29.999, 0.715, 0.909, 0.01], ['stable', 0.995732]], [[18.339, 0.83, 4.998, 0.4], ['unstable', 1.458271]], [[20.299, 1.465, 6.715, 0.05], ['stable', 0.897474]], [[1.0, 1.0, 3.0, 0.1], ['stable', 0.961803]]]]
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 | ['marginal', 1.0] | ['unstable', 1.0008] | Failed |
| case 1 | ['stable', 0.992763] | ['stable', 0.984586] | Failed |
| case 2 | ['stable', 0.983422] | ['stable', 0.955296] | Failed |
| case 3 | ['stable', 0.995261] | ['stable', 0.597125] | Failed |
| case 4 | ['unstable', 1.02431] | ['stable', 0.992649] | Failed |
| case 5 | ['marginal', 1.0] | ['marginal', 1.0] | Passed |
| case 6 | ['marginal', 1.0] | ['marginal', 1.0] | Passed |
| case 7 | ['marginal', 1.0] | ['marginal', 1.0] | Passed |
SHA-256 / ff1773c0422c791eed15e9c0f8a29330c04259305e88d1a4fc5917f6146ef17f
3 / The verified repair
Exit 0"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(k, m, c, dt):
a = c / m
b = k / m
disc = a * a - 4 * b
if disc >= 0:
r = math.sqrt(disc)
lams = [complex((-a + r) / 2, 0), complex((-a - r) / 2, 0)]
else:
r = math.sqrt(-disc)
lams = [complex(-a / 2, r / 2), complex(-a / 2, -r / 2)]
amp = max(abs(1 + dt * lam) for lam in lams)
if abs(amp - 1) <= 1e-9:
label = 'marginal'
elif amp < 1:
label = 'stable'
else:
label = 'unstable'
return [label, round(amp, 6)]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[[[0.0, 1.0, 2.0008, 1.0], ['unstable', 1.0008]], [[42.221, 2.418, 7.819, 0.01], ['stable', 0.984586]], [[16.494, 1.905, 4.155, 0.05], ['stable', 0.955296]], [[28.666, 0.459, 5.82, 0.1], ['stable', 0.597125]], [[42.548, 3.763, 4.806, 0.1], ['stable', 0.992649]], [[0.0, 3.907, 1.097, 1.0], ['marginal', 1.0]], [[0.0, 1.0, 1.0, 0.5], ['marginal', 1.0]], [[0.0, 0.421, 0.691, 0.05], ['marginal', 1.0]]], [[[0.0, 1.0, 2.0008, 1.0], ['unstable', 1.0008]], [[38.018, 1.276, 0.385, 0.2], ['unstable', 1.459946]], [[59.677, 2.374, 3.266, 1.0], ['unstable', 4.976144]], [[6.203, 0.678, 1.745, 0.05], ['stable', 0.945614]], [[0.0, 0.907, 6.001, 0.01], ['marginal', 1.0]], [[0.0, 1.0, 1.0, 0.5], ['marginal', 1.0]], [[0.0, 1.043, 3.236, 0.05], ['marginal', 1.0]], [[18.985, 2.12, 2.861, 1.0], ['unstable', 2.933541]]], [[[48.986, 0.701, 3.497, 0.1], ['unstable', 1.095419]], [[49.989, 2.073, 1.922, 0.8], ['unstable', 3.961243]], [[0.0, 1.763, 0.013, 0.8], ['marginal', 1.0]], [[17.741, 0.849, 5.676, 0.01], ['stable', 0.967075]], [[20.646, 3.925, 4.888, 0.2], ['stable', 0.980477]], [[0.0, 1.0, 2.0008, 1.0], ['unstable', 1.0008]], [[42.196, 3.443, 4.277, 0.8], ['unstable', 2.801748]], [[0.0, 1.0, 1.0, 0.5], ['marginal', 1.0]]], [[[41.131, 0.314, 6.187, 0.4], ['unstable', 3.751925]], [[23.134, 3.325, 2.902, 0.05], ['stable', 0.98679]], [[3.87, 2.12, 0.308, 0.05], ['stable', 0.998649]], [[0.0, 1.0, 2.0008, 1.0], ['unstable', 1.0008]], [[0.0, 3.102, 2.755, 0.05], ['marginal', 1.0]], [[9.079, 3.699, 5.728, 0.05], ['stable', 0.963696]], [[0.0, 1.0, 1.0, 0.5], ['marginal', 1.0]], [[57.413, 2.505, 6.221, 0.2], ['unstable', 1.191674]]], [[[0.0, 1.0, 1.0, 0.5], ['marginal', 1.0]], [[0.0, 1.0, 2.0008, 1.0], ['unstable', 1.0008]], [[0.0, 1.889, 7.242, 0.8], ['unstable', 2.06702]], [[31.247, 2.981, 5.247, 0.01], ['stable', 0.991689]], [[29.999, 0.715, 0.909, 0.01], ['stable', 0.995732]], [[18.339, 0.83, 4.998, 0.4], ['unstable', 1.458271]], [[20.299, 1.465, 6.715, 0.05], ['stable', 0.897474]], [[1.0, 1.0, 3.0, 0.1], ['stable', 0.961803]]]]
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 | ['unstable', 1.0008] | ['unstable', 1.0008] | Passed |
| case 1 | ['stable', 0.984586] | ['stable', 0.984586] | Passed |
| case 2 | ['stable', 0.955296] | ['stable', 0.955296] | Passed |
| case 3 | ['stable', 0.597125] | ['stable', 0.597125] | Passed |
| case 4 | ['stable', 0.992649] | ['stable', 0.992649] | Passed |
| case 5 | ['marginal', 1.0] | ['marginal', 1.0] | Passed |
| case 6 | ['marginal', 1.0] | ['marginal', 1.0] | Passed |
| case 7 | ['marginal', 1.0] | ['marginal', 1.0] | Passed |
SHA-256 / 43a5f6724b1b78616a06def5eb4d5ed34f5939cf27a3ba251b768aef54c06888
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:54.077404+00:00.
Case digest / e929f502ad2ea29de7709d4dfa66bf058645271b11c058822a2835e969bab18f