FAILURE MAP
← Case archive

FA-88016 / Inverse kinematics solvers / Open access

Equal-cost branches select the later candidate · case 01

Among symmetric solutions the last one listed is chosen, flipping the arm unexpectedly.

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

ROOT CAUSE

The comparison accepts cost <= best, so later ties replace the incumbent.

VERIFIED REPAIR

Replace only when cost < best - 1e-9.

Unsuccessful approach: A 0.1% relative margin also rejects genuinely cheaper candidates.

Case contract

Input [candidates, current, limits, weights] with joint angles in degrees. A limits entry is null for a continuous joint (distance is the shortest wrap-around difference) or [lo,hi] for a limited joint (inclusive bounds, distance is the plain difference because motion cannot pass the stop). Cost is sum w*d^2. Return [index into candidates, cost rounded 4] for the cheapest feasible candidate, earliest wins ties within 1e-9, or "none".

Why this case matters

Inverse kinematics code turns task-space goals into joint commands; a wrong branch, sign, limit or update order sends a real arm to the wrong pose.

1 / The failure

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(x):
    cands,cur,lims,w=x
    best=None
    bi=None
    for k,c in enumerate(cands):
        ok=all(l is None or l[0]<=q<=l[1] for q,l in zip(c,lims))
        if not ok: continue
        cost=0.0
        for q,p,wi,l in zip(c,cur,w,lims):
            if l is None:
                d=abs(q-p)%360.0
                d=min(d,360.0-d)
            else:
                d=abs(q-p)
            cost+=wi*d*d
        if best is None or cost<=best:
            best=cost
            bi=k
    if bi is None: return 'none'
    return [bi,round(best,4)]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['limit boundary candidate', [[[[170, 10], [0, 10]], [160, 10], [[-170, 170], None], [1, 1]]], [0, 100.0]], ['all infeasible', [[[[200, 0], [-200, 0]], [0, 0], [[-170, 170], None], [1, 1]]], 'none'], ['continuous wrap shorter', [[[[0, 170], [0, -170]], [0, 175], [[-90, 90], None], [1, 1]]], [0, 25.0]], ['multi-turn limited joint', [[[[-200, 0], [150, 0]], [170, 0], [[-270, 270], None], [1, 1]]], [1, 400.0]], ['wide limited joint far', [[[[250, 0], [-100, 0]], [-120, 0], [[-270, 270], None], [1, 1]]], [1, 400.0]], ['exact tie keeps first', [[[[10, 0], [-10, 0]], [0, 0], [[-90, 90], None], [1, 1]]], [0, 100.0]], ['slightly better later', [[[[10, 0], [9.999, 0]], [0, 0], [[-90, 90], [-90, 90]], [1, 1]]], [1, 99.98]]], [['multi-turn limited joint', [[[[-200, 0], [150, 0]], [170, 0], [[-270, 270], None], [1, 1]]], [1, 400.0]], ['wide limited joint far', [[[[250, 0], [-100, 0]], [-120, 0], [[-270, 270], None], [1, 1]]], [1, 400.0]], ['weighted wrist', [[[[10, 50], [40, 0]], [0, 0], [[-90, 90], [-90, 90]], [1, 0.1]]], [0, 350.0]], ['exact tie keeps first', [[[[10, 0], [-10, 0]], [0, 0], [[-90, 90], None], [1, 1]]], [0, 100.0]], ['near tie within tolerance', [[[[10, 0], [0, 10.0000000001]], [0, 0], [None, None], [1, 1]]], [0, 100.0]], ['slightly better later', [[[[10, 0], [9.999, 0]], [0, 0], [[-90, 90], [-90, 90]], [1, 1]]], [1, 99.98]], ['infeasible first candidate', [[[[95, 0], [30, 0], [20, 5]], [0, 0], [[-90, 90], None], [1, 1]]], [2, 425.0]]], [['exact tie keeps first', [[[[10, 0], [-10, 0]], [0, 0], [[-90, 90], None], [1, 1]]], [0, 100.0]], ['near tie within tolerance', [[[[10, 0], [0, 10.0000000001]], [0, 0], [None, None], [1, 1]]], [0, 100.0]], ['slightly better later', [[[[10, 0], [9.999, 0]], [0, 0], [[-90, 90], [-90, 90]], [1, 1]]], [1, 99.98]], ['infeasible first candidate', [[[[95, 0], [30, 0], [20, 5]], [0, 0], [[-90, 90], None], [1, 1]]], [2, 425.0]], ['lower bound inclusive', [[[[-90, 0], [60, 60]], [-80, 0], [[-90, 90], [-90, 90]], [1, 1]]], [0, 100.0]], ['continuous across 180', [[[[0, -179], [0, 100]], [0, 179], [None, None], [1, 1]]], [0, 4.0]], ['three joints mixed', [[[[10, 350, -20], [15, -5, -25], [100, 0, 0]], [12, 0, -22], [[-120, 120], None, [-45, 45]], [2, 1, 1]]], [1, 52.0]]], [['limit boundary candidate', [[[[170, 10], [0, 10]], [160, 10], [[-170, 170], None], [1, 1]]], [0, 100.0]], ['exact tie keeps first', [[[[10, 0], [-10, 0]], [0, 0], [[-90, 90], None], [1, 1]]], [0, 100.0]], ['slightly better later', [[[[10, 0], [9.999, 0]], [0, 0], [[-90, 90], [-90, 90]], [1, 1]]], [1, 99.98]], ['continuous across 180', [[[[0, -179], [0, 100]], [0, 179], [None, None], [1, 1]]], [0, 4.0]], ['three joints mixed', [[[[10, 350, -20], [15, -5, -25], [100, 0, 0]], [12, 0, -22], [[-120, 120], None, [-45, 45]], [2, 1, 1]]], [1, 52.0]], ['heavy weight flips choice', [[[[5, 40], [30, 0]], [0, 0], [None, None], [10, 1]]], [0, 1850.0]], ['continuous large difference', [[[[0, 300], [0, 200]], [0, 0], [[-10, 10], None], [1, 1]]], [0, 3600.0]]], [['limit boundary candidate', [[[[170, 10], [0, 10]], [160, 10], [[-170, 170], None], [1, 1]]], [0, 100.0]], ['all infeasible', [[[[200, 0], [-200, 0]], [0, 0], [[-170, 170], None], [1, 1]]], 'none'], ['continuous wrap shorter', [[[[0, 170], [0, -170]], [0, 175], [[-90, 90], None], [1, 1]]], [0, 25.0]], ['multi-turn limited joint', [[[[-200, 0], [150, 0]], [170, 0], [[-270, 270], None], [1, 1]]], [1, 400.0]], ['exact tie keeps first', [[[[10, 0], [-10, 0]], [0, 0], [[-90, 90], None], [1, 1]]], [0, 100.0]], ['slightly better later', [[[[10, 0], [9.999, 0]], [0, 0], [[-90, 90], [-90, 90]], [1, 1]]], [1, 99.98]], ['continuous large difference', [[[[0, 300], [0, 200]], [0, 0], [[-10, 10], None], [1, 1]]], [0, 3600.0]]]]
for label, args, expected in fixtures[N-1]:
    check(label, 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
limit boundary candidate[0, 100.0][0, 100.0]Passed
all infeasiblenonenonePassed
continuous wrap shorter[0, 25.0][0, 25.0]Passed
multi-turn limited joint[1, 400.0][1, 400.0]Passed
wide limited joint far[1, 400.0][1, 400.0]Passed
exact tie keeps first[1, 100.0][0, 100.0]Failed
slightly better later[1, 99.98][1, 99.98]Passed

SHA-256 / 618cff4e29f120c5afe9ed62dc2f4c737ec632a34eb833478e8f9cae44df000f

2 / The unsuccessful fix

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(x):
    cands,cur,lims,w=x
    best=None
    bi=None
    for k,c in enumerate(cands):
        ok=all(l is None or l[0]<=q<=l[1] for q,l in zip(c,lims))
        if not ok: continue
        cost=0.0
        for q,p,wi,l in zip(c,cur,w,lims):
            if l is None:
                d=abs(q-p)%360.0
                d=min(d,360.0-d)
            else:
                d=abs(q-p)
            cost+=wi*d*d
        if best is None or cost<best*0.999:
            best=cost
            bi=k
    if bi is None: return 'none'
    return [bi,round(best,4)]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['limit boundary candidate', [[[[170, 10], [0, 10]], [160, 10], [[-170, 170], None], [1, 1]]], [0, 100.0]], ['all infeasible', [[[[200, 0], [-200, 0]], [0, 0], [[-170, 170], None], [1, 1]]], 'none'], ['continuous wrap shorter', [[[[0, 170], [0, -170]], [0, 175], [[-90, 90], None], [1, 1]]], [0, 25.0]], ['multi-turn limited joint', [[[[-200, 0], [150, 0]], [170, 0], [[-270, 270], None], [1, 1]]], [1, 400.0]], ['wide limited joint far', [[[[250, 0], [-100, 0]], [-120, 0], [[-270, 270], None], [1, 1]]], [1, 400.0]], ['exact tie keeps first', [[[[10, 0], [-10, 0]], [0, 0], [[-90, 90], None], [1, 1]]], [0, 100.0]], ['slightly better later', [[[[10, 0], [9.999, 0]], [0, 0], [[-90, 90], [-90, 90]], [1, 1]]], [1, 99.98]]], [['multi-turn limited joint', [[[[-200, 0], [150, 0]], [170, 0], [[-270, 270], None], [1, 1]]], [1, 400.0]], ['wide limited joint far', [[[[250, 0], [-100, 0]], [-120, 0], [[-270, 270], None], [1, 1]]], [1, 400.0]], ['weighted wrist', [[[[10, 50], [40, 0]], [0, 0], [[-90, 90], [-90, 90]], [1, 0.1]]], [0, 350.0]], ['exact tie keeps first', [[[[10, 0], [-10, 0]], [0, 0], [[-90, 90], None], [1, 1]]], [0, 100.0]], ['near tie within tolerance', [[[[10, 0], [0, 10.0000000001]], [0, 0], [None, None], [1, 1]]], [0, 100.0]], ['slightly better later', [[[[10, 0], [9.999, 0]], [0, 0], [[-90, 90], [-90, 90]], [1, 1]]], [1, 99.98]], ['infeasible first candidate', [[[[95, 0], [30, 0], [20, 5]], [0, 0], [[-90, 90], None], [1, 1]]], [2, 425.0]]], [['exact tie keeps first', [[[[10, 0], [-10, 0]], [0, 0], [[-90, 90], None], [1, 1]]], [0, 100.0]], ['near tie within tolerance', [[[[10, 0], [0, 10.0000000001]], [0, 0], [None, None], [1, 1]]], [0, 100.0]], ['slightly better later', [[[[10, 0], [9.999, 0]], [0, 0], [[-90, 90], [-90, 90]], [1, 1]]], [1, 99.98]], ['infeasible first candidate', [[[[95, 0], [30, 0], [20, 5]], [0, 0], [[-90, 90], None], [1, 1]]], [2, 425.0]], ['lower bound inclusive', [[[[-90, 0], [60, 60]], [-80, 0], [[-90, 90], [-90, 90]], [1, 1]]], [0, 100.0]], ['continuous across 180', [[[[0, -179], [0, 100]], [0, 179], [None, None], [1, 1]]], [0, 4.0]], ['three joints mixed', [[[[10, 350, -20], [15, -5, -25], [100, 0, 0]], [12, 0, -22], [[-120, 120], None, [-45, 45]], [2, 1, 1]]], [1, 52.0]]], [['limit boundary candidate', [[[[170, 10], [0, 10]], [160, 10], [[-170, 170], None], [1, 1]]], [0, 100.0]], ['exact tie keeps first', [[[[10, 0], [-10, 0]], [0, 0], [[-90, 90], None], [1, 1]]], [0, 100.0]], ['slightly better later', [[[[10, 0], [9.999, 0]], [0, 0], [[-90, 90], [-90, 90]], [1, 1]]], [1, 99.98]], ['continuous across 180', [[[[0, -179], [0, 100]], [0, 179], [None, None], [1, 1]]], [0, 4.0]], ['three joints mixed', [[[[10, 350, -20], [15, -5, -25], [100, 0, 0]], [12, 0, -22], [[-120, 120], None, [-45, 45]], [2, 1, 1]]], [1, 52.0]], ['heavy weight flips choice', [[[[5, 40], [30, 0]], [0, 0], [None, None], [10, 1]]], [0, 1850.0]], ['continuous large difference', [[[[0, 300], [0, 200]], [0, 0], [[-10, 10], None], [1, 1]]], [0, 3600.0]]], [['limit boundary candidate', [[[[170, 10], [0, 10]], [160, 10], [[-170, 170], None], [1, 1]]], [0, 100.0]], ['all infeasible', [[[[200, 0], [-200, 0]], [0, 0], [[-170, 170], None], [1, 1]]], 'none'], ['continuous wrap shorter', [[[[0, 170], [0, -170]], [0, 175], [[-90, 90], None], [1, 1]]], [0, 25.0]], ['multi-turn limited joint', [[[[-200, 0], [150, 0]], [170, 0], [[-270, 270], None], [1, 1]]], [1, 400.0]], ['exact tie keeps first', [[[[10, 0], [-10, 0]], [0, 0], [[-90, 90], None], [1, 1]]], [0, 100.0]], ['slightly better later', [[[[10, 0], [9.999, 0]], [0, 0], [[-90, 90], [-90, 90]], [1, 1]]], [1, 99.98]], ['continuous large difference', [[[[0, 300], [0, 200]], [0, 0], [[-10, 10], None], [1, 1]]], [0, 3600.0]]]]
for label, args, expected in fixtures[N-1]:
    check(label, 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
limit boundary candidate[0, 100.0][0, 100.0]Passed
all infeasiblenonenonePassed
continuous wrap shorter[0, 25.0][0, 25.0]Passed
multi-turn limited joint[1, 400.0][1, 400.0]Passed
wide limited joint far[1, 400.0][1, 400.0]Passed
exact tie keeps first[0, 100.0][0, 100.0]Passed
slightly better later[0, 100.0][1, 99.98]Failed

SHA-256 / 5163b8fc0baf73996ebc17e2077e28a811b78cb6b0631ec6209ada318ca4f09c

3 / The verified repair

Exit 0
"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(x):
    cands,cur,lims,w=x
    best=None
    bi=None
    for k,c in enumerate(cands):
        ok=all(l is None or l[0]<=q<=l[1] for q,l in zip(c,lims))
        if not ok: continue
        cost=0.0
        for q,p,wi,l in zip(c,cur,w,lims):
            if l is None:
                d=abs(q-p)%360.0
                d=min(d,360.0-d)
            else:
                d=abs(q-p)
            cost+=wi*d*d
        if best is None or cost<best-1e-9:
            best=cost
            bi=k
    if bi is None: return 'none'
    return [bi,round(best,4)]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['limit boundary candidate', [[[[170, 10], [0, 10]], [160, 10], [[-170, 170], None], [1, 1]]], [0, 100.0]], ['all infeasible', [[[[200, 0], [-200, 0]], [0, 0], [[-170, 170], None], [1, 1]]], 'none'], ['continuous wrap shorter', [[[[0, 170], [0, -170]], [0, 175], [[-90, 90], None], [1, 1]]], [0, 25.0]], ['multi-turn limited joint', [[[[-200, 0], [150, 0]], [170, 0], [[-270, 270], None], [1, 1]]], [1, 400.0]], ['wide limited joint far', [[[[250, 0], [-100, 0]], [-120, 0], [[-270, 270], None], [1, 1]]], [1, 400.0]], ['exact tie keeps first', [[[[10, 0], [-10, 0]], [0, 0], [[-90, 90], None], [1, 1]]], [0, 100.0]], ['slightly better later', [[[[10, 0], [9.999, 0]], [0, 0], [[-90, 90], [-90, 90]], [1, 1]]], [1, 99.98]]], [['multi-turn limited joint', [[[[-200, 0], [150, 0]], [170, 0], [[-270, 270], None], [1, 1]]], [1, 400.0]], ['wide limited joint far', [[[[250, 0], [-100, 0]], [-120, 0], [[-270, 270], None], [1, 1]]], [1, 400.0]], ['weighted wrist', [[[[10, 50], [40, 0]], [0, 0], [[-90, 90], [-90, 90]], [1, 0.1]]], [0, 350.0]], ['exact tie keeps first', [[[[10, 0], [-10, 0]], [0, 0], [[-90, 90], None], [1, 1]]], [0, 100.0]], ['near tie within tolerance', [[[[10, 0], [0, 10.0000000001]], [0, 0], [None, None], [1, 1]]], [0, 100.0]], ['slightly better later', [[[[10, 0], [9.999, 0]], [0, 0], [[-90, 90], [-90, 90]], [1, 1]]], [1, 99.98]], ['infeasible first candidate', [[[[95, 0], [30, 0], [20, 5]], [0, 0], [[-90, 90], None], [1, 1]]], [2, 425.0]]], [['exact tie keeps first', [[[[10, 0], [-10, 0]], [0, 0], [[-90, 90], None], [1, 1]]], [0, 100.0]], ['near tie within tolerance', [[[[10, 0], [0, 10.0000000001]], [0, 0], [None, None], [1, 1]]], [0, 100.0]], ['slightly better later', [[[[10, 0], [9.999, 0]], [0, 0], [[-90, 90], [-90, 90]], [1, 1]]], [1, 99.98]], ['infeasible first candidate', [[[[95, 0], [30, 0], [20, 5]], [0, 0], [[-90, 90], None], [1, 1]]], [2, 425.0]], ['lower bound inclusive', [[[[-90, 0], [60, 60]], [-80, 0], [[-90, 90], [-90, 90]], [1, 1]]], [0, 100.0]], ['continuous across 180', [[[[0, -179], [0, 100]], [0, 179], [None, None], [1, 1]]], [0, 4.0]], ['three joints mixed', [[[[10, 350, -20], [15, -5, -25], [100, 0, 0]], [12, 0, -22], [[-120, 120], None, [-45, 45]], [2, 1, 1]]], [1, 52.0]]], [['limit boundary candidate', [[[[170, 10], [0, 10]], [160, 10], [[-170, 170], None], [1, 1]]], [0, 100.0]], ['exact tie keeps first', [[[[10, 0], [-10, 0]], [0, 0], [[-90, 90], None], [1, 1]]], [0, 100.0]], ['slightly better later', [[[[10, 0], [9.999, 0]], [0, 0], [[-90, 90], [-90, 90]], [1, 1]]], [1, 99.98]], ['continuous across 180', [[[[0, -179], [0, 100]], [0, 179], [None, None], [1, 1]]], [0, 4.0]], ['three joints mixed', [[[[10, 350, -20], [15, -5, -25], [100, 0, 0]], [12, 0, -22], [[-120, 120], None, [-45, 45]], [2, 1, 1]]], [1, 52.0]], ['heavy weight flips choice', [[[[5, 40], [30, 0]], [0, 0], [None, None], [10, 1]]], [0, 1850.0]], ['continuous large difference', [[[[0, 300], [0, 200]], [0, 0], [[-10, 10], None], [1, 1]]], [0, 3600.0]]], [['limit boundary candidate', [[[[170, 10], [0, 10]], [160, 10], [[-170, 170], None], [1, 1]]], [0, 100.0]], ['all infeasible', [[[[200, 0], [-200, 0]], [0, 0], [[-170, 170], None], [1, 1]]], 'none'], ['continuous wrap shorter', [[[[0, 170], [0, -170]], [0, 175], [[-90, 90], None], [1, 1]]], [0, 25.0]], ['multi-turn limited joint', [[[[-200, 0], [150, 0]], [170, 0], [[-270, 270], None], [1, 1]]], [1, 400.0]], ['exact tie keeps first', [[[[10, 0], [-10, 0]], [0, 0], [[-90, 90], None], [1, 1]]], [0, 100.0]], ['slightly better later', [[[[10, 0], [9.999, 0]], [0, 0], [[-90, 90], [-90, 90]], [1, 1]]], [1, 99.98]], ['continuous large difference', [[[[0, 300], [0, 200]], [0, 0], [[-10, 10], None], [1, 1]]], [0, 3600.0]]]]
for label, args, expected in fixtures[N-1]:
    check(label, 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
limit boundary candidate[0, 100.0][0, 100.0]Passed
all infeasiblenonenonePassed
continuous wrap shorter[0, 25.0][0, 25.0]Passed
multi-turn limited joint[1, 400.0][1, 400.0]Passed
wide limited joint far[1, 400.0][1, 400.0]Passed
exact tie keeps first[0, 100.0][0, 100.0]Passed
slightly better later[1, 99.98][1, 99.98]Passed

SHA-256 / 7b79b0579a45be3667c7f1475e910d4cbb9b119b1d29bb1201d8b858163f1ee6

Verification & scope

Deterministic planar or low-dimensional teaching model with a stipulated convention; not a general robotics library. 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:51:04.264201+00:00.

Case digest / cd12b8a47131e105ca9ca5a245d67edc8bdaf0ea3d2fad76437a0fdcb5aa35cf