FAILURE MAP
← Case archive

FA-86941 / Physics integrator stability / Open access

Effective spring mass uses the larger inverse mass only · case 01

Two light bodies connected by a stiff spring get a substep that is too long.

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

ROOT CAUSE

The combined inverse mass is max(im1, im2) instead of im1+im2.

VERIFIED REPAIR

Sum inverse masses (reduced mass is its reciprocal).

Unsuccessful approach: Averaging the inverse masses halves the effective stiffness frequency term.

Case contract

solve(springs, safety, dt_frame, max_sub): springs are [k, inv_mass_a, inv_mass_b] (inverse mass 0 = static). A spring with k>0 and nonzero summed inverse mass w limits the substep to safety*2/sqrt(k*w). n=ceil(dt_frame/h_min) clamped to [1,max_sub]; return [n, dt_frame/n rounded to 6, whether n had to be clamped down]. No limiting spring gives [1, dt_frame, False].

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(springs, safety, dt_frame, max_sub):
    hc = None
    for k, im1, im2 in springs:
        w = max(im1, im2)
        if w == 0 or k <= 0:
            continue
        h = safety * 2.0 / math.sqrt(k * w)
        if hc is None or h < hc:
            hc = h
    if hc is None:
        return [1, round(dt_frame, 6), False]
    n = math.ceil(dt_frame / hc - 1e-12)
    clamped = n > max_sub
    n = min(max(n, 1), max_sub)
    return [n, round(dt_frame / n, 6), clamped]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[[[[[51.4, 0.26, 0.0], [194.4, 1.517, 0.0]], 0.5, 0.1, 5], [2, 0.05, False]], [[[[135.8, 1.611, 1.207]], 0.9, 0.05, 1], [1, 0.05, False]], [[[[172.3, 1.054, 0.0]], 0.5, 0.1, 12], [2, 0.05, False]], [[[[71.3, 0.479, 1.246], [255.9, 0.0, 0.0], [268.0, 1.892, 0.0], [0.0, 0.578, 1.103]], 0.8, 0.05, 2], [1, 0.05, False]], [[[[186.5, 1.518, 1.502], [130.9, 1.524, 1.598]], 0.9, 0.1, 3], [2, 0.05, False]], [[[[90.1, 0.0, 0.128], [0.0, 0.747, 0.0], [181.9, 1.365, 1.578], [308.2, 1.098, 0.0]], 0.5, 0.1, 7], [3, 0.033333, False]], [[[[63.5, 0.263, 1.804]], 0.5, 0.1, 7], [2, 0.05, False]], [[[[176.6, 0.318, 1.727]], 0.9, 0.1, 1], [1, 0.1, True]]], [[[[[374.4, 0.273, 0.663], [0.0, 0.0, 0.931], [293.6, 1.292, 1.969]], 0.5, 0.1, 11], [4, 0.025, False]], [[[[294.1, 0.26, 0.93], [0.0, 0.111, 0.0], [295.9, 0.0, 0.0]], 0.9, 0.1, 6], [2, 0.05, False]], [[[[22.5, 0.986, 1.718]], 0.8, 0.05, 7], [1, 0.05, False]], [[[[192.5, 0.226, 1.644], [299.3, 0.0, 0.204]], 0.8, 0.016666666666666666, 4], [1, 0.016667, False]], [[[[97.9, 0.409, 0.102], [187.1, 0.0, 1.876], [175.5, 0.772, 1.165]], 0.9, 0.1, 3], [2, 0.05, False]], [[[[211.7, 1.064, 0.812], [384.0, 1.693, 1.686]], 0.5, 0.1, 7], [4, 0.025, False]], [[[[75.6, 1.034, 0.944], [77.7, 1.269, 0.0], [221.4, 1.38, 0.893]], 0.5, 0.03333333333333333, 7], [1, 0.033333, False]], [[[[246.1, 0.0, 1.235]], 0.9, 0.1, 5], [1, 0.1, False]]], [[[[[0.0, 0.476, 0.0], [18.2, 1.282, 0.988], [243.3, 0.297, 1.244]], 0.5, 0.016666666666666666, 10], [1, 0.016667, False]], [[[[98.4, 0.155, 0.544], [243.2, 0.0, 0.932], [174.9, 0.0, 0.913], [326.4, 1.87, 1.935]], 0.8, 0.05, 8], [2, 0.025, False]], [[[[83.1, 0.423, 0.998], [213.6, 1.913, 0.0], [0.0, 1.148, 0.983], [369.5, 0.121, 0.638]], 0.9, 0.1, 1], [1, 0.1, True]], [[[[160.0, 1.174, 1.024], [222.4, 0.746, 0.568], [234.1, 0.0, 0.0], [305.9, 0.261, 0.0]], 0.8, 0.1, 5], [2, 0.05, False]], [[[[241.8, 1.076, 1.735], [150.9, 0.0, 0.0]], 0.8, 0.05, 2], [1, 0.05, False]], [[[[0.0, 0.898, 0.365], [308.5, 1.46, 0.0], [162.4, 0.916, 1.761], [343.1, 1.271, 0.525]], 0.9, 0.1, 1], [1, 0.1, True]], [[[[159.1, 1.48, 1.426], [208.5, 0.0, 0.0], [243.4, 1.248, 1.55]], 0.5, 0.05, 6], [2, 0.025, False]], [[[[174.6, 0.875, 0.0], [352.2, 1.283, 0.639], [253.3, 0.0, 0.557]], 0.9, 0.05, 8], [1, 0.05, False]]], [[[[[76.0, 0.819, 0.139]], 0.8, 0.1, 8], [1, 0.1, False]], [[[[145.7, 0.0, 0.0], [22.3, 0.0, 1.322], [387.7, 0.0, 0.3], [91.8, 1.808, 0.0]], 0.5, 0.016666666666666666, 11], [1, 0.016667, False]], [[[[313.6, 0.0, 0.0], [0.0, 0.166, 0.299], [211.1, 1.062, 1.481]], 0.5, 0.1, 11], [3, 0.033333, False]], [[[[225.4, 0.134, 1.333]], 0.9, 0.1, 6], [2, 0.05, False]], [[[[231.0, 0.0, 1.82], [25.6, 1.068, 0.0], [268.8, 1.477, 0.0]], 0.5, 0.05, 5], [2, 0.025, False]], [[[[0.7, 0.0, 1.518], [157.3, 1.84, 0.368], [155.6, 0.0, 0.884], [325.1, 1.537, 0.59]], 0.5, 0.05, 9], [2, 0.025, False]], [[[[318.6, 1.144, 1.896], [70.8, 1.567, 0.153], [182.0, 0.419, 1.859], [339.9, 0.0, 1.703]], 0.5, 0.1, 12], [4, 0.025, False]], [[[[111.2, 0.217, 0.0], [0.0, 0.0, 0.0]], 0.5, 0.016666666666666666, 8], [1, 0.016667, False]]], [[[[[0.0, 1.613, 0.0], [46.8, 1.168, 1.601], [255.1, 0.0, 0.0], [397.8, 0.0, 0.649]], 0.8, 0.1, 1], [1, 0.1, True]], [[[[246.6, 0.0, 1.678]], 0.8, 0.03333333333333333, 3], [1, 0.033333, False]], [[[[362.8, 1.145, 0.897], [97.0, 0.0, 1.886]], 0.8, 0.03333333333333333, 7], [1, 0.033333, False]], [[[[97.9, 0.752, 1.178], [382.4, 1.927, 1.693], [68.3, 0.0, 0.0]], 0.9, 0.1, 11], [3, 0.033333, False]], [[[[117.1, 1.522, 0.87], [106.7, 1.85, 0.868], [225.8, 1.796, 0.556]], 0.5, 0.03333333333333333, 9], [1, 0.033333, False]], [[[[247.4, 0.0, 0.0]], 0.9, 0.016666666666666666, 5], [1, 0.016667, False]], [[[[180.7, 1.578, 1.59], [1.3, 0.945, 0.0], [192.2, 0.0, 1.838]], 0.9, 0.1, 10], [2, 0.05, False]], [[[[272.0, 1.176, 0.0], [330.9, 1.661, 1.627], [124.0, 0.407, 0.81], [148.1, 0.906, 1.321]], 0.5, 0.03333333333333333, 5], [2, 0.016667, False]]]]
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[2, 0.05, False][2, 0.05, False]Passed
case 1[1, 0.05, False][1, 0.05, False]Passed
case 2[2, 0.05, False][2, 0.05, False]Passed
case 3[1, 0.05, False][1, 0.05, False]Passed
case 4[1, 0.1, False][2, 0.05, False]Failed
case 5[2, 0.05, False][3, 0.033333, False]Failed
case 6[2, 0.05, False][2, 0.05, False]Passed
case 7[1, 0.1, False][1, 0.1, True]Failed

SHA-256 / b53159c70e9316a3384221af8d5070aaf815eac3c7c6507b51b828702a92b99d

2 / The unsuccessful fix

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(springs, safety, dt_frame, max_sub):
    hc = None
    for k, im1, im2 in springs:
        w = (im1 + im2) / 2
        if w == 0 or k <= 0:
            continue
        h = safety * 2.0 / math.sqrt(k * w)
        if hc is None or h < hc:
            hc = h
    if hc is None:
        return [1, round(dt_frame, 6), False]
    n = math.ceil(dt_frame / hc - 1e-12)
    clamped = n > max_sub
    n = min(max(n, 1), max_sub)
    return [n, round(dt_frame / n, 6), clamped]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[[[[[51.4, 0.26, 0.0], [194.4, 1.517, 0.0]], 0.5, 0.1, 5], [2, 0.05, False]], [[[[135.8, 1.611, 1.207]], 0.9, 0.05, 1], [1, 0.05, False]], [[[[172.3, 1.054, 0.0]], 0.5, 0.1, 12], [2, 0.05, False]], [[[[71.3, 0.479, 1.246], [255.9, 0.0, 0.0], [268.0, 1.892, 0.0], [0.0, 0.578, 1.103]], 0.8, 0.05, 2], [1, 0.05, False]], [[[[186.5, 1.518, 1.502], [130.9, 1.524, 1.598]], 0.9, 0.1, 3], [2, 0.05, False]], [[[[90.1, 0.0, 0.128], [0.0, 0.747, 0.0], [181.9, 1.365, 1.578], [308.2, 1.098, 0.0]], 0.5, 0.1, 7], [3, 0.033333, False]], [[[[63.5, 0.263, 1.804]], 0.5, 0.1, 7], [2, 0.05, False]], [[[[176.6, 0.318, 1.727]], 0.9, 0.1, 1], [1, 0.1, True]]], [[[[[374.4, 0.273, 0.663], [0.0, 0.0, 0.931], [293.6, 1.292, 1.969]], 0.5, 0.1, 11], [4, 0.025, False]], [[[[294.1, 0.26, 0.93], [0.0, 0.111, 0.0], [295.9, 0.0, 0.0]], 0.9, 0.1, 6], [2, 0.05, False]], [[[[22.5, 0.986, 1.718]], 0.8, 0.05, 7], [1, 0.05, False]], [[[[192.5, 0.226, 1.644], [299.3, 0.0, 0.204]], 0.8, 0.016666666666666666, 4], [1, 0.016667, False]], [[[[97.9, 0.409, 0.102], [187.1, 0.0, 1.876], [175.5, 0.772, 1.165]], 0.9, 0.1, 3], [2, 0.05, False]], [[[[211.7, 1.064, 0.812], [384.0, 1.693, 1.686]], 0.5, 0.1, 7], [4, 0.025, False]], [[[[75.6, 1.034, 0.944], [77.7, 1.269, 0.0], [221.4, 1.38, 0.893]], 0.5, 0.03333333333333333, 7], [1, 0.033333, False]], [[[[246.1, 0.0, 1.235]], 0.9, 0.1, 5], [1, 0.1, False]]], [[[[[0.0, 0.476, 0.0], [18.2, 1.282, 0.988], [243.3, 0.297, 1.244]], 0.5, 0.016666666666666666, 10], [1, 0.016667, False]], [[[[98.4, 0.155, 0.544], [243.2, 0.0, 0.932], [174.9, 0.0, 0.913], [326.4, 1.87, 1.935]], 0.8, 0.05, 8], [2, 0.025, False]], [[[[83.1, 0.423, 0.998], [213.6, 1.913, 0.0], [0.0, 1.148, 0.983], [369.5, 0.121, 0.638]], 0.9, 0.1, 1], [1, 0.1, True]], [[[[160.0, 1.174, 1.024], [222.4, 0.746, 0.568], [234.1, 0.0, 0.0], [305.9, 0.261, 0.0]], 0.8, 0.1, 5], [2, 0.05, False]], [[[[241.8, 1.076, 1.735], [150.9, 0.0, 0.0]], 0.8, 0.05, 2], [1, 0.05, False]], [[[[0.0, 0.898, 0.365], [308.5, 1.46, 0.0], [162.4, 0.916, 1.761], [343.1, 1.271, 0.525]], 0.9, 0.1, 1], [1, 0.1, True]], [[[[159.1, 1.48, 1.426], [208.5, 0.0, 0.0], [243.4, 1.248, 1.55]], 0.5, 0.05, 6], [2, 0.025, False]], [[[[174.6, 0.875, 0.0], [352.2, 1.283, 0.639], [253.3, 0.0, 0.557]], 0.9, 0.05, 8], [1, 0.05, False]]], [[[[[76.0, 0.819, 0.139]], 0.8, 0.1, 8], [1, 0.1, False]], [[[[145.7, 0.0, 0.0], [22.3, 0.0, 1.322], [387.7, 0.0, 0.3], [91.8, 1.808, 0.0]], 0.5, 0.016666666666666666, 11], [1, 0.016667, False]], [[[[313.6, 0.0, 0.0], [0.0, 0.166, 0.299], [211.1, 1.062, 1.481]], 0.5, 0.1, 11], [3, 0.033333, False]], [[[[225.4, 0.134, 1.333]], 0.9, 0.1, 6], [2, 0.05, False]], [[[[231.0, 0.0, 1.82], [25.6, 1.068, 0.0], [268.8, 1.477, 0.0]], 0.5, 0.05, 5], [2, 0.025, False]], [[[[0.7, 0.0, 1.518], [157.3, 1.84, 0.368], [155.6, 0.0, 0.884], [325.1, 1.537, 0.59]], 0.5, 0.05, 9], [2, 0.025, False]], [[[[318.6, 1.144, 1.896], [70.8, 1.567, 0.153], [182.0, 0.419, 1.859], [339.9, 0.0, 1.703]], 0.5, 0.1, 12], [4, 0.025, False]], [[[[111.2, 0.217, 0.0], [0.0, 0.0, 0.0]], 0.5, 0.016666666666666666, 8], [1, 0.016667, False]]], [[[[[0.0, 1.613, 0.0], [46.8, 1.168, 1.601], [255.1, 0.0, 0.0], [397.8, 0.0, 0.649]], 0.8, 0.1, 1], [1, 0.1, True]], [[[[246.6, 0.0, 1.678]], 0.8, 0.03333333333333333, 3], [1, 0.033333, False]], [[[[362.8, 1.145, 0.897], [97.0, 0.0, 1.886]], 0.8, 0.03333333333333333, 7], [1, 0.033333, False]], [[[[97.9, 0.752, 1.178], [382.4, 1.927, 1.693], [68.3, 0.0, 0.0]], 0.9, 0.1, 11], [3, 0.033333, False]], [[[[117.1, 1.522, 0.87], [106.7, 1.85, 0.868], [225.8, 1.796, 0.556]], 0.5, 0.03333333333333333, 9], [1, 0.033333, False]], [[[[247.4, 0.0, 0.0]], 0.9, 0.016666666666666666, 5], [1, 0.016667, False]], [[[[180.7, 1.578, 1.59], [1.3, 0.945, 0.0], [192.2, 0.0, 1.838]], 0.9, 0.1, 10], [2, 0.05, False]], [[[[272.0, 1.176, 0.0], [330.9, 1.661, 1.627], [124.0, 0.407, 0.81], [148.1, 0.906, 1.321]], 0.5, 0.03333333333333333, 5], [2, 0.016667, False]]]]
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[2, 0.05, False][2, 0.05, False]Passed
case 1[1, 0.05, False][1, 0.05, False]Passed
case 2[1, 0.1, False][2, 0.05, False]Failed
case 3[1, 0.05, False][1, 0.05, False]Passed
case 4[1, 0.1, False][2, 0.05, False]Failed
case 5[2, 0.05, False][3, 0.033333, False]Failed
case 6[1, 0.1, False][2, 0.05, False]Failed
case 7[1, 0.1, False][1, 0.1, True]Failed

SHA-256 / a32481dccb8e96856673f0f615ee6e5eeea7787f4e88a5a3a78047ef9b6d8c82

3 / The verified repair

Exit 0
"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(springs, safety, dt_frame, max_sub):
    hc = None
    for k, im1, im2 in springs:
        w = im1 + im2
        if w == 0 or k <= 0:
            continue
        h = safety * 2.0 / math.sqrt(k * w)
        if hc is None or h < hc:
            hc = h
    if hc is None:
        return [1, round(dt_frame, 6), False]
    n = math.ceil(dt_frame / hc - 1e-12)
    clamped = n > max_sub
    n = min(max(n, 1), max_sub)
    return [n, round(dt_frame / n, 6), clamped]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[[[[[51.4, 0.26, 0.0], [194.4, 1.517, 0.0]], 0.5, 0.1, 5], [2, 0.05, False]], [[[[135.8, 1.611, 1.207]], 0.9, 0.05, 1], [1, 0.05, False]], [[[[172.3, 1.054, 0.0]], 0.5, 0.1, 12], [2, 0.05, False]], [[[[71.3, 0.479, 1.246], [255.9, 0.0, 0.0], [268.0, 1.892, 0.0], [0.0, 0.578, 1.103]], 0.8, 0.05, 2], [1, 0.05, False]], [[[[186.5, 1.518, 1.502], [130.9, 1.524, 1.598]], 0.9, 0.1, 3], [2, 0.05, False]], [[[[90.1, 0.0, 0.128], [0.0, 0.747, 0.0], [181.9, 1.365, 1.578], [308.2, 1.098, 0.0]], 0.5, 0.1, 7], [3, 0.033333, False]], [[[[63.5, 0.263, 1.804]], 0.5, 0.1, 7], [2, 0.05, False]], [[[[176.6, 0.318, 1.727]], 0.9, 0.1, 1], [1, 0.1, True]]], [[[[[374.4, 0.273, 0.663], [0.0, 0.0, 0.931], [293.6, 1.292, 1.969]], 0.5, 0.1, 11], [4, 0.025, False]], [[[[294.1, 0.26, 0.93], [0.0, 0.111, 0.0], [295.9, 0.0, 0.0]], 0.9, 0.1, 6], [2, 0.05, False]], [[[[22.5, 0.986, 1.718]], 0.8, 0.05, 7], [1, 0.05, False]], [[[[192.5, 0.226, 1.644], [299.3, 0.0, 0.204]], 0.8, 0.016666666666666666, 4], [1, 0.016667, False]], [[[[97.9, 0.409, 0.102], [187.1, 0.0, 1.876], [175.5, 0.772, 1.165]], 0.9, 0.1, 3], [2, 0.05, False]], [[[[211.7, 1.064, 0.812], [384.0, 1.693, 1.686]], 0.5, 0.1, 7], [4, 0.025, False]], [[[[75.6, 1.034, 0.944], [77.7, 1.269, 0.0], [221.4, 1.38, 0.893]], 0.5, 0.03333333333333333, 7], [1, 0.033333, False]], [[[[246.1, 0.0, 1.235]], 0.9, 0.1, 5], [1, 0.1, False]]], [[[[[0.0, 0.476, 0.0], [18.2, 1.282, 0.988], [243.3, 0.297, 1.244]], 0.5, 0.016666666666666666, 10], [1, 0.016667, False]], [[[[98.4, 0.155, 0.544], [243.2, 0.0, 0.932], [174.9, 0.0, 0.913], [326.4, 1.87, 1.935]], 0.8, 0.05, 8], [2, 0.025, False]], [[[[83.1, 0.423, 0.998], [213.6, 1.913, 0.0], [0.0, 1.148, 0.983], [369.5, 0.121, 0.638]], 0.9, 0.1, 1], [1, 0.1, True]], [[[[160.0, 1.174, 1.024], [222.4, 0.746, 0.568], [234.1, 0.0, 0.0], [305.9, 0.261, 0.0]], 0.8, 0.1, 5], [2, 0.05, False]], [[[[241.8, 1.076, 1.735], [150.9, 0.0, 0.0]], 0.8, 0.05, 2], [1, 0.05, False]], [[[[0.0, 0.898, 0.365], [308.5, 1.46, 0.0], [162.4, 0.916, 1.761], [343.1, 1.271, 0.525]], 0.9, 0.1, 1], [1, 0.1, True]], [[[[159.1, 1.48, 1.426], [208.5, 0.0, 0.0], [243.4, 1.248, 1.55]], 0.5, 0.05, 6], [2, 0.025, False]], [[[[174.6, 0.875, 0.0], [352.2, 1.283, 0.639], [253.3, 0.0, 0.557]], 0.9, 0.05, 8], [1, 0.05, False]]], [[[[[76.0, 0.819, 0.139]], 0.8, 0.1, 8], [1, 0.1, False]], [[[[145.7, 0.0, 0.0], [22.3, 0.0, 1.322], [387.7, 0.0, 0.3], [91.8, 1.808, 0.0]], 0.5, 0.016666666666666666, 11], [1, 0.016667, False]], [[[[313.6, 0.0, 0.0], [0.0, 0.166, 0.299], [211.1, 1.062, 1.481]], 0.5, 0.1, 11], [3, 0.033333, False]], [[[[225.4, 0.134, 1.333]], 0.9, 0.1, 6], [2, 0.05, False]], [[[[231.0, 0.0, 1.82], [25.6, 1.068, 0.0], [268.8, 1.477, 0.0]], 0.5, 0.05, 5], [2, 0.025, False]], [[[[0.7, 0.0, 1.518], [157.3, 1.84, 0.368], [155.6, 0.0, 0.884], [325.1, 1.537, 0.59]], 0.5, 0.05, 9], [2, 0.025, False]], [[[[318.6, 1.144, 1.896], [70.8, 1.567, 0.153], [182.0, 0.419, 1.859], [339.9, 0.0, 1.703]], 0.5, 0.1, 12], [4, 0.025, False]], [[[[111.2, 0.217, 0.0], [0.0, 0.0, 0.0]], 0.5, 0.016666666666666666, 8], [1, 0.016667, False]]], [[[[[0.0, 1.613, 0.0], [46.8, 1.168, 1.601], [255.1, 0.0, 0.0], [397.8, 0.0, 0.649]], 0.8, 0.1, 1], [1, 0.1, True]], [[[[246.6, 0.0, 1.678]], 0.8, 0.03333333333333333, 3], [1, 0.033333, False]], [[[[362.8, 1.145, 0.897], [97.0, 0.0, 1.886]], 0.8, 0.03333333333333333, 7], [1, 0.033333, False]], [[[[97.9, 0.752, 1.178], [382.4, 1.927, 1.693], [68.3, 0.0, 0.0]], 0.9, 0.1, 11], [3, 0.033333, False]], [[[[117.1, 1.522, 0.87], [106.7, 1.85, 0.868], [225.8, 1.796, 0.556]], 0.5, 0.03333333333333333, 9], [1, 0.033333, False]], [[[[247.4, 0.0, 0.0]], 0.9, 0.016666666666666666, 5], [1, 0.016667, False]], [[[[180.7, 1.578, 1.59], [1.3, 0.945, 0.0], [192.2, 0.0, 1.838]], 0.9, 0.1, 10], [2, 0.05, False]], [[[[272.0, 1.176, 0.0], [330.9, 1.661, 1.627], [124.0, 0.407, 0.81], [148.1, 0.906, 1.321]], 0.5, 0.03333333333333333, 5], [2, 0.016667, False]]]]
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[2, 0.05, False][2, 0.05, False]Passed
case 1[1, 0.05, False][1, 0.05, False]Passed
case 2[2, 0.05, False][2, 0.05, False]Passed
case 3[1, 0.05, False][1, 0.05, False]Passed
case 4[2, 0.05, False][2, 0.05, False]Passed
case 5[3, 0.033333, False][3, 0.033333, False]Passed
case 6[2, 0.05, False][2, 0.05, False]Passed
case 7[1, 0.1, True][1, 0.1, True]Passed

SHA-256 / 82fa96849387d4682c6062ab546768b3db3e8dda872c7b215a009a259f3b5c18

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

Case digest / 06b62c13750099baf187d570514cc44bbbf86f843bfd463b5f7e739250f8e688