FA-86911 / Physics integrator stability / Open access
Stability test checks only the first characteristic root · case 01
Overdamped systems whose fast root leaves the unit disc are reported stable.
ROOT CAUSE
Only lams[0] is used for the amplification factor.
VERIFIED REPAIR
Take the maximum amplification over both roots.
Unsuccessful approach: Taking the minimum reports the least dangerous mode.
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 = abs(1 + dt * lams[0])
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.179, 7.263, 0.4], ['unstable', 1.464122]], [[0.0, 3.279, 2.912, 0.2], ['marginal', 1.0]], [[0.0, 2.0, 1.0, 0.1], ['marginal', 1.0]], [[0.0, 1.112, 6.445, 0.8], ['unstable', 3.636691]], [[2.156, 1.67, 6.019, 0.8], ['unstable', 1.560685]], [[0.0, 1.0, 1.0, 0.5], ['marginal', 1.0]], [[0.0, 1.812, 5.536, 0.4], ['marginal', 1.0]], [[49.596, 2.586, 6.61, 0.2], ['unstable', 1.120684]]], [[[0.0, 0.677, 1.41, 1.0], ['unstable', 1.082718]], [[0.0, 1.0, 1.0, 0.5], ['marginal', 1.0]], [[0.0, 0.317, 0.575, 0.05], ['marginal', 1.0]], [[0.0, 2.0, 1.0, 0.1], ['marginal', 1.0]], [[54.495, 0.307, 4.164, 0.1], ['unstable', 1.191104]], [[0.178, 0.403, 1.4, 0.05], ['stable', 0.993391]], [[12.654, 0.648, 7.929, 0.8], ['unstable', 7.279383]], [[7.713, 0.795, 5.016, 0.4], ['stable', 0.462028]]], [[[0.0, 2.0, 1.0, 0.1], ['marginal', 1.0]], [[0.0, 1.0, 1.0, 0.5], ['marginal', 1.0]], [[0.0, 0.322, 4.742, 1.0], ['unstable', 13.726708]], [[1.0, 1.0, 3.0, 0.1], ['stable', 0.961803]], [[0.0, 0.737, 2.778, 0.05], ['marginal', 1.0]], [[0.0, 0.897, 5.697, 0.4], ['unstable', 1.540468]], [[0.0, 1.0, 2.0008, 1.0], ['unstable', 1.0008]], [[23.145, 2.844, 4.936, 0.1], ['stable', 0.952798]]], [[[0.0, 1.715, 7.493, 0.01], ['marginal', 1.0]], [[0.0, 2.0, 1.0, 0.1], ['marginal', 1.0]], [[0.0, 1.0, 1.0, 0.5], ['marginal', 1.0]], [[0.0, 1.558, 4.258, 0.8], ['unstable', 1.186393]], [[0.0, 0.821, 6.576, 1.0], ['unstable', 7.009744]], [[1.0, 1.0, 3.0, 0.1], ['stable', 0.961803]], [[0.0, 1.0, 2.0008, 1.0], ['unstable', 1.0008]], [[0.0, 1.1, 4.516, 0.1], ['marginal', 1.0]]], [[[12.171, 2.869, 0.163, 0.8], ['unstable', 1.915616]], [[0.0, 1.0, 1.0, 0.5], ['marginal', 1.0]], [[0.0, 0.76, 4.988, 1.0], ['unstable', 5.563158]], [[1.0, 1.0, 3.0, 0.1], ['stable', 0.961803]], [[0.0, 2.0, 1.0, 0.1], ['marginal', 1.0]], [[12.144, 0.347, 6.718, 0.8], ['unstable', 12.873758]], [[0.0, 1.0, 2.0008, 1.0], ['unstable', 1.0008]], [[0.0, 3.191, 7.686, 0.01], ['marginal', 1.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 | ['marginal', 1.0] | ['unstable', 1.464122] | Failed |
| case 1 | ['marginal', 1.0] | ['marginal', 1.0] | Passed |
| case 2 | ['marginal', 1.0] | ['marginal', 1.0] | Passed |
| case 3 | ['marginal', 1.0] | ['unstable', 3.636691] | Failed |
| case 4 | ['stable', 0.677332] | ['unstable', 1.560685] | Failed |
| case 5 | ['marginal', 1.0] | ['marginal', 1.0] | Passed |
| case 6 | ['marginal', 1.0] | ['marginal', 1.0] | Passed |
| case 7 | ['unstable', 1.120684] | ['unstable', 1.120684] | Passed |
SHA-256 / a24ccd03e1ad16513a2d8d1dc9e822549f85ed85709ace2b0a5c77a5ba11683a
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 / 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 = min(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.179, 7.263, 0.4], ['unstable', 1.464122]], [[0.0, 3.279, 2.912, 0.2], ['marginal', 1.0]], [[0.0, 2.0, 1.0, 0.1], ['marginal', 1.0]], [[0.0, 1.112, 6.445, 0.8], ['unstable', 3.636691]], [[2.156, 1.67, 6.019, 0.8], ['unstable', 1.560685]], [[0.0, 1.0, 1.0, 0.5], ['marginal', 1.0]], [[0.0, 1.812, 5.536, 0.4], ['marginal', 1.0]], [[49.596, 2.586, 6.61, 0.2], ['unstable', 1.120684]]], [[[0.0, 0.677, 1.41, 1.0], ['unstable', 1.082718]], [[0.0, 1.0, 1.0, 0.5], ['marginal', 1.0]], [[0.0, 0.317, 0.575, 0.05], ['marginal', 1.0]], [[0.0, 2.0, 1.0, 0.1], ['marginal', 1.0]], [[54.495, 0.307, 4.164, 0.1], ['unstable', 1.191104]], [[0.178, 0.403, 1.4, 0.05], ['stable', 0.993391]], [[12.654, 0.648, 7.929, 0.8], ['unstable', 7.279383]], [[7.713, 0.795, 5.016, 0.4], ['stable', 0.462028]]], [[[0.0, 2.0, 1.0, 0.1], ['marginal', 1.0]], [[0.0, 1.0, 1.0, 0.5], ['marginal', 1.0]], [[0.0, 0.322, 4.742, 1.0], ['unstable', 13.726708]], [[1.0, 1.0, 3.0, 0.1], ['stable', 0.961803]], [[0.0, 0.737, 2.778, 0.05], ['marginal', 1.0]], [[0.0, 0.897, 5.697, 0.4], ['unstable', 1.540468]], [[0.0, 1.0, 2.0008, 1.0], ['unstable', 1.0008]], [[23.145, 2.844, 4.936, 0.1], ['stable', 0.952798]]], [[[0.0, 1.715, 7.493, 0.01], ['marginal', 1.0]], [[0.0, 2.0, 1.0, 0.1], ['marginal', 1.0]], [[0.0, 1.0, 1.0, 0.5], ['marginal', 1.0]], [[0.0, 1.558, 4.258, 0.8], ['unstable', 1.186393]], [[0.0, 0.821, 6.576, 1.0], ['unstable', 7.009744]], [[1.0, 1.0, 3.0, 0.1], ['stable', 0.961803]], [[0.0, 1.0, 2.0008, 1.0], ['unstable', 1.0008]], [[0.0, 1.1, 4.516, 0.1], ['marginal', 1.0]]], [[[12.171, 2.869, 0.163, 0.8], ['unstable', 1.915616]], [[0.0, 1.0, 1.0, 0.5], ['marginal', 1.0]], [[0.0, 0.76, 4.988, 1.0], ['unstable', 5.563158]], [[1.0, 1.0, 3.0, 0.1], ['stable', 0.961803]], [[0.0, 2.0, 1.0, 0.1], ['marginal', 1.0]], [[12.144, 0.347, 6.718, 0.8], ['unstable', 12.873758]], [[0.0, 1.0, 2.0008, 1.0], ['unstable', 1.0008]], [[0.0, 3.191, 7.686, 0.01], ['marginal', 1.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 | ['marginal', 1.0] | ['unstable', 1.464122] | Failed |
| case 1 | ['stable', 0.822385] | ['marginal', 1.0] | Failed |
| case 2 | ['stable', 0.95] | ['marginal', 1.0] | Failed |
| case 3 | ['marginal', 1.0] | ['unstable', 3.636691] | Failed |
| case 4 | ['stable', 0.677332] | ['unstable', 1.560685] | Failed |
| case 5 | ['stable', 0.5] | ['marginal', 1.0] | Failed |
| case 6 | ['stable', 0.222075] | ['marginal', 1.0] | Failed |
| case 7 | ['unstable', 1.120684] | ['unstable', 1.120684] | Passed |
SHA-256 / d16acf06736616a0efe6c213edc9ff8257ec0bb6d38e9f96dbef34f7802e6ad7
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.179, 7.263, 0.4], ['unstable', 1.464122]], [[0.0, 3.279, 2.912, 0.2], ['marginal', 1.0]], [[0.0, 2.0, 1.0, 0.1], ['marginal', 1.0]], [[0.0, 1.112, 6.445, 0.8], ['unstable', 3.636691]], [[2.156, 1.67, 6.019, 0.8], ['unstable', 1.560685]], [[0.0, 1.0, 1.0, 0.5], ['marginal', 1.0]], [[0.0, 1.812, 5.536, 0.4], ['marginal', 1.0]], [[49.596, 2.586, 6.61, 0.2], ['unstable', 1.120684]]], [[[0.0, 0.677, 1.41, 1.0], ['unstable', 1.082718]], [[0.0, 1.0, 1.0, 0.5], ['marginal', 1.0]], [[0.0, 0.317, 0.575, 0.05], ['marginal', 1.0]], [[0.0, 2.0, 1.0, 0.1], ['marginal', 1.0]], [[54.495, 0.307, 4.164, 0.1], ['unstable', 1.191104]], [[0.178, 0.403, 1.4, 0.05], ['stable', 0.993391]], [[12.654, 0.648, 7.929, 0.8], ['unstable', 7.279383]], [[7.713, 0.795, 5.016, 0.4], ['stable', 0.462028]]], [[[0.0, 2.0, 1.0, 0.1], ['marginal', 1.0]], [[0.0, 1.0, 1.0, 0.5], ['marginal', 1.0]], [[0.0, 0.322, 4.742, 1.0], ['unstable', 13.726708]], [[1.0, 1.0, 3.0, 0.1], ['stable', 0.961803]], [[0.0, 0.737, 2.778, 0.05], ['marginal', 1.0]], [[0.0, 0.897, 5.697, 0.4], ['unstable', 1.540468]], [[0.0, 1.0, 2.0008, 1.0], ['unstable', 1.0008]], [[23.145, 2.844, 4.936, 0.1], ['stable', 0.952798]]], [[[0.0, 1.715, 7.493, 0.01], ['marginal', 1.0]], [[0.0, 2.0, 1.0, 0.1], ['marginal', 1.0]], [[0.0, 1.0, 1.0, 0.5], ['marginal', 1.0]], [[0.0, 1.558, 4.258, 0.8], ['unstable', 1.186393]], [[0.0, 0.821, 6.576, 1.0], ['unstable', 7.009744]], [[1.0, 1.0, 3.0, 0.1], ['stable', 0.961803]], [[0.0, 1.0, 2.0008, 1.0], ['unstable', 1.0008]], [[0.0, 1.1, 4.516, 0.1], ['marginal', 1.0]]], [[[12.171, 2.869, 0.163, 0.8], ['unstable', 1.915616]], [[0.0, 1.0, 1.0, 0.5], ['marginal', 1.0]], [[0.0, 0.76, 4.988, 1.0], ['unstable', 5.563158]], [[1.0, 1.0, 3.0, 0.1], ['stable', 0.961803]], [[0.0, 2.0, 1.0, 0.1], ['marginal', 1.0]], [[12.144, 0.347, 6.718, 0.8], ['unstable', 12.873758]], [[0.0, 1.0, 2.0008, 1.0], ['unstable', 1.0008]], [[0.0, 3.191, 7.686, 0.01], ['marginal', 1.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 | ['unstable', 1.464122] | ['unstable', 1.464122] | Passed |
| case 1 | ['marginal', 1.0] | ['marginal', 1.0] | Passed |
| case 2 | ['marginal', 1.0] | ['marginal', 1.0] | Passed |
| case 3 | ['unstable', 3.636691] | ['unstable', 3.636691] | Passed |
| case 4 | ['unstable', 1.560685] | ['unstable', 1.560685] | Passed |
| case 5 | ['marginal', 1.0] | ['marginal', 1.0] | Passed |
| case 6 | ['marginal', 1.0] | ['marginal', 1.0] | Passed |
| case 7 | ['unstable', 1.120684] | ['unstable', 1.120684] | Passed |
SHA-256 / a0eff2d5b31d243fbe59dbb01d26c9e855cf552c72ea004041a0823975c1d0c7
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.993493+00:00.
Case digest / 0ff6d7809e1d8c90665840e3e2559335d9cc13cc7899a9e091f791935a60f163