FAILURE MAP
← Case archive

FA-86926 / Physics integrator stability / Open access

Unit amplification is labeled stable before the marginal test · case 01

A free damped particle (zero stiffness) is reported stable although its drift mode never decays.

Verified by executionVariant 1 · 8 checks per implementationDownload source bundle ↓JSON ↗

ROOT CAUSE

The stable branch (amp <= 1) is tested before the marginal tolerance band.

VERIFIED REPAIR

Test the marginal band first, then strict stability.

Unsuccessful approach: Widening the marginal tolerance to 1e-3 mislabels slightly unstable systems.

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 amp <= 1:
        label = 'stable'
    elif abs(amp - 1) <= 1e-9:
        label = 'marginal'
    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]], [[1.0, 1.0, 3.0, 0.1], ['stable', 0.961803]], [[0.0, 2.0, 1.0, 0.1], ['marginal', 1.0]], [[48.787, 2.932, 7.285, 0.05], ['stable', 0.957792]], [[0.0, 3.074, 0.67, 0.1], ['marginal', 1.0]], [[0.0, 1.0, 1.0, 0.5], ['marginal', 1.0]], [[33.169, 0.238, 7.307, 0.1], ['unstable', 1.516322]], [[15.317, 1.272, 2.461, 0.1], ['stable', 0.962778]]], [[[0.0, 2.791, 6.117, 0.4], ['marginal', 1.0]], [[34.91, 3.761, 2.904, 1.0], ['unstable', 3.083824]], [[0.0, 1.0, 1.0, 0.5], ['marginal', 1.0]], [[5.819, 1.841, 1.025, 0.4], ['unstable', 1.132705]], [[0.0, 1.0, 2.0008, 1.0], ['unstable', 1.0008]], [[47.409, 1.216, 0.706, 0.01], ['stable', 0.999046]], [[0.0, 1.79, 3.179, 0.01], ['marginal', 1.0]], [[1.0, 1.0, 3.0, 0.1], ['stable', 0.961803]]], [[[8.315, 3.625, 0.483, 0.01], ['stable', 0.999448]], [[1.0, 1.0, 3.0, 0.1], ['stable', 0.961803]], [[0.0, 2.181, 7.092, 0.01], ['marginal', 1.0]], [[0.0, 1.763, 0.013, 0.8], ['marginal', 1.0]], [[0.0, 1.0, 2.0008, 1.0], ['unstable', 1.0008]], [[0.0, 3.996, 2.36, 1.0], ['marginal', 1.0]], [[40.419, 1.422, 6.433, 0.1], ['stable', 0.912058]], [[6.621, 1.513, 5.261, 0.8], ['unstable', 1.00942]]], [[[21.305, 1.348, 3.845, 0.2], ['unstable', 1.030398]], [[25.334, 1.767, 5.476, 0.8], ['unstable', 2.774282]], [[0.0, 0.88, 2.713, 0.01], ['marginal', 1.0]], [[57.915, 0.684, 2.142, 0.1], ['unstable', 1.238367]], [[0.0, 1.692, 2.361, 0.05], ['marginal', 1.0]], [[1.0, 1.0, 3.0, 0.1], ['stable', 0.961803]], [[0.0, 1.0, 2.0008, 1.0], ['unstable', 1.0008]], [[0.0, 2.091, 5.492, 0.4], ['marginal', 1.0]]], [[[3.053, 1.003, 2.137, 0.1], ['stable', 0.90409]], [[0.0, 0.824, 2.657, 0.1], ['marginal', 1.0]], [[33.574, 1.956, 6.795, 0.1], ['stable', 0.907884]], [[59.414, 2.945, 0.495, 0.2], ['unstable', 1.331678]], [[0.0, 3.877, 2.569, 0.1], ['marginal', 1.0]], [[0.0, 1.0, 2.0008, 1.0], ['unstable', 1.0008]], [[1.0, 1.0, 3.0, 0.1], ['stable', 0.961803]], [[0.0, 2.0, 1.0, 0.1], ['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 fixtureActualExpectedOutcome
case 0['unstable', 1.0008]['unstable', 1.0008]Passed
case 1['stable', 0.961803]['stable', 0.961803]Passed
case 2['stable', 1.0]['marginal', 1.0]Failed
case 3['stable', 0.957792]['stable', 0.957792]Passed
case 4['stable', 1.0]['marginal', 1.0]Failed
case 5['stable', 1.0]['marginal', 1.0]Failed
case 6['unstable', 1.516322]['unstable', 1.516322]Passed
case 7['stable', 0.962778]['stable', 0.962778]Passed

SHA-256 / 56ecce94d9dc314fb620298f5829fd658140c0cef1ec325ac09dd092ecb69859

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 = max(abs(1 + dt * lam) for lam in lams)
    if abs(amp - 1) <= 1e-3:
        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]], [[1.0, 1.0, 3.0, 0.1], ['stable', 0.961803]], [[0.0, 2.0, 1.0, 0.1], ['marginal', 1.0]], [[48.787, 2.932, 7.285, 0.05], ['stable', 0.957792]], [[0.0, 3.074, 0.67, 0.1], ['marginal', 1.0]], [[0.0, 1.0, 1.0, 0.5], ['marginal', 1.0]], [[33.169, 0.238, 7.307, 0.1], ['unstable', 1.516322]], [[15.317, 1.272, 2.461, 0.1], ['stable', 0.962778]]], [[[0.0, 2.791, 6.117, 0.4], ['marginal', 1.0]], [[34.91, 3.761, 2.904, 1.0], ['unstable', 3.083824]], [[0.0, 1.0, 1.0, 0.5], ['marginal', 1.0]], [[5.819, 1.841, 1.025, 0.4], ['unstable', 1.132705]], [[0.0, 1.0, 2.0008, 1.0], ['unstable', 1.0008]], [[47.409, 1.216, 0.706, 0.01], ['stable', 0.999046]], [[0.0, 1.79, 3.179, 0.01], ['marginal', 1.0]], [[1.0, 1.0, 3.0, 0.1], ['stable', 0.961803]]], [[[8.315, 3.625, 0.483, 0.01], ['stable', 0.999448]], [[1.0, 1.0, 3.0, 0.1], ['stable', 0.961803]], [[0.0, 2.181, 7.092, 0.01], ['marginal', 1.0]], [[0.0, 1.763, 0.013, 0.8], ['marginal', 1.0]], [[0.0, 1.0, 2.0008, 1.0], ['unstable', 1.0008]], [[0.0, 3.996, 2.36, 1.0], ['marginal', 1.0]], [[40.419, 1.422, 6.433, 0.1], ['stable', 0.912058]], [[6.621, 1.513, 5.261, 0.8], ['unstable', 1.00942]]], [[[21.305, 1.348, 3.845, 0.2], ['unstable', 1.030398]], [[25.334, 1.767, 5.476, 0.8], ['unstable', 2.774282]], [[0.0, 0.88, 2.713, 0.01], ['marginal', 1.0]], [[57.915, 0.684, 2.142, 0.1], ['unstable', 1.238367]], [[0.0, 1.692, 2.361, 0.05], ['marginal', 1.0]], [[1.0, 1.0, 3.0, 0.1], ['stable', 0.961803]], [[0.0, 1.0, 2.0008, 1.0], ['unstable', 1.0008]], [[0.0, 2.091, 5.492, 0.4], ['marginal', 1.0]]], [[[3.053, 1.003, 2.137, 0.1], ['stable', 0.90409]], [[0.0, 0.824, 2.657, 0.1], ['marginal', 1.0]], [[33.574, 1.956, 6.795, 0.1], ['stable', 0.907884]], [[59.414, 2.945, 0.495, 0.2], ['unstable', 1.331678]], [[0.0, 3.877, 2.569, 0.1], ['marginal', 1.0]], [[0.0, 1.0, 2.0008, 1.0], ['unstable', 1.0008]], [[1.0, 1.0, 3.0, 0.1], ['stable', 0.961803]], [[0.0, 2.0, 1.0, 0.1], ['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 fixtureActualExpectedOutcome
case 0['marginal', 1.0008]['unstable', 1.0008]Failed
case 1['stable', 0.961803]['stable', 0.961803]Passed
case 2['marginal', 1.0]['marginal', 1.0]Passed
case 3['stable', 0.957792]['stable', 0.957792]Passed
case 4['marginal', 1.0]['marginal', 1.0]Passed
case 5['marginal', 1.0]['marginal', 1.0]Passed
case 6['unstable', 1.516322]['unstable', 1.516322]Passed
case 7['stable', 0.962778]['stable', 0.962778]Passed

SHA-256 / 3fd219bd5c1f281e7ff39d24df0dc6156aa4ce773894535b32b653df89e5b65e

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]], [[1.0, 1.0, 3.0, 0.1], ['stable', 0.961803]], [[0.0, 2.0, 1.0, 0.1], ['marginal', 1.0]], [[48.787, 2.932, 7.285, 0.05], ['stable', 0.957792]], [[0.0, 3.074, 0.67, 0.1], ['marginal', 1.0]], [[0.0, 1.0, 1.0, 0.5], ['marginal', 1.0]], [[33.169, 0.238, 7.307, 0.1], ['unstable', 1.516322]], [[15.317, 1.272, 2.461, 0.1], ['stable', 0.962778]]], [[[0.0, 2.791, 6.117, 0.4], ['marginal', 1.0]], [[34.91, 3.761, 2.904, 1.0], ['unstable', 3.083824]], [[0.0, 1.0, 1.0, 0.5], ['marginal', 1.0]], [[5.819, 1.841, 1.025, 0.4], ['unstable', 1.132705]], [[0.0, 1.0, 2.0008, 1.0], ['unstable', 1.0008]], [[47.409, 1.216, 0.706, 0.01], ['stable', 0.999046]], [[0.0, 1.79, 3.179, 0.01], ['marginal', 1.0]], [[1.0, 1.0, 3.0, 0.1], ['stable', 0.961803]]], [[[8.315, 3.625, 0.483, 0.01], ['stable', 0.999448]], [[1.0, 1.0, 3.0, 0.1], ['stable', 0.961803]], [[0.0, 2.181, 7.092, 0.01], ['marginal', 1.0]], [[0.0, 1.763, 0.013, 0.8], ['marginal', 1.0]], [[0.0, 1.0, 2.0008, 1.0], ['unstable', 1.0008]], [[0.0, 3.996, 2.36, 1.0], ['marginal', 1.0]], [[40.419, 1.422, 6.433, 0.1], ['stable', 0.912058]], [[6.621, 1.513, 5.261, 0.8], ['unstable', 1.00942]]], [[[21.305, 1.348, 3.845, 0.2], ['unstable', 1.030398]], [[25.334, 1.767, 5.476, 0.8], ['unstable', 2.774282]], [[0.0, 0.88, 2.713, 0.01], ['marginal', 1.0]], [[57.915, 0.684, 2.142, 0.1], ['unstable', 1.238367]], [[0.0, 1.692, 2.361, 0.05], ['marginal', 1.0]], [[1.0, 1.0, 3.0, 0.1], ['stable', 0.961803]], [[0.0, 1.0, 2.0008, 1.0], ['unstable', 1.0008]], [[0.0, 2.091, 5.492, 0.4], ['marginal', 1.0]]], [[[3.053, 1.003, 2.137, 0.1], ['stable', 0.90409]], [[0.0, 0.824, 2.657, 0.1], ['marginal', 1.0]], [[33.574, 1.956, 6.795, 0.1], ['stable', 0.907884]], [[59.414, 2.945, 0.495, 0.2], ['unstable', 1.331678]], [[0.0, 3.877, 2.569, 0.1], ['marginal', 1.0]], [[0.0, 1.0, 2.0008, 1.0], ['unstable', 1.0008]], [[1.0, 1.0, 3.0, 0.1], ['stable', 0.961803]], [[0.0, 2.0, 1.0, 0.1], ['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 fixtureActualExpectedOutcome
case 0['unstable', 1.0008]['unstable', 1.0008]Passed
case 1['stable', 0.961803]['stable', 0.961803]Passed
case 2['marginal', 1.0]['marginal', 1.0]Passed
case 3['stable', 0.957792]['stable', 0.957792]Passed
case 4['marginal', 1.0]['marginal', 1.0]Passed
case 5['marginal', 1.0]['marginal', 1.0]Passed
case 6['unstable', 1.516322]['unstable', 1.516322]Passed
case 7['stable', 0.962778]['stable', 0.962778]Passed

SHA-256 / 47776036aaedf8d8d000cd3b6bbef61dd315068978a54f6aacc7c661442ce869

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.077194+00:00.

Case digest / ba1b368de146d8ea0cb1742c284c65f41c3d175d0f5687681edf740fb8ba46ce