FAILURE MAP
← Case archive

FA-88006 / Inverse kinematics solvers / Open access

Continuous joints are charged the long way around · case 01

A candidate 10 degrees away across the +/-180 seam costs as if it were 350 degrees away.

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

ROOT CAUSE

The modulus is taken but the complementary angle is never considered.

VERIFIED REPAIR

Take min(d, 360-d) after reducing modulo 360.

Unsuccessful approach: Subtracting 180 from large differences produces a distance that is neither path.

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
            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]], ['weighted wrist', [[[[10, 50], [40, 0]], [0, 0], [[-90, 90], [-90, 90]], [1, 0.1]]], [0, 350.0]], ['continuous across 180', [[[[0, -179], [0, 100]], [0, 179], [None, None], [1, 1]]], [0, 4.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]], ['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]], ['continuous large difference', [[[[0, 300], [0, 200]], [0, 0], [[-10, 10], None], [1, 1]]], [0, 3600.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]], ['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]], ['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]], ['continuous across 180', [[[[0, -179], [0, 100]], [0, 179], [None, None], [1, 1]]], [0, 4.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]]]]
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
weighted wrist[0, 350.0][0, 350.0]Passed
continuous across 180[1, 6241.0][0, 4.0]Failed

SHA-256 / a6fa472af70e386cdf698b299ceaabc16283d80b4f21b2427f80c48047036035

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=d if d<=180.0 else d-180.0
            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]], ['weighted wrist', [[[[10, 50], [40, 0]], [0, 0], [[-90, 90], [-90, 90]], [1, 0.1]]], [0, 350.0]], ['continuous across 180', [[[[0, -179], [0, 100]], [0, 179], [None, None], [1, 1]]], [0, 4.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]], ['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]], ['continuous large difference', [[[[0, 300], [0, 200]], [0, 0], [[-10, 10], None], [1, 1]]], [0, 3600.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]], ['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]], ['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]], ['continuous across 180', [[[[0, -179], [0, 100]], [0, 179], [None, None], [1, 1]]], [0, 4.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]]]]
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
weighted wrist[0, 350.0][0, 350.0]Passed
continuous across 180[1, 6241.0][0, 4.0]Failed

SHA-256 / 7e01d050d7a252d31c6b624e399db489c2d66e232e33a6866dcf8514746711e3

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]], ['weighted wrist', [[[[10, 50], [40, 0]], [0, 0], [[-90, 90], [-90, 90]], [1, 0.1]]], [0, 350.0]], ['continuous across 180', [[[[0, -179], [0, 100]], [0, 179], [None, None], [1, 1]]], [0, 4.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]], ['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]], ['continuous large difference', [[[[0, 300], [0, 200]], [0, 0], [[-10, 10], None], [1, 1]]], [0, 3600.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]], ['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]], ['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]], ['continuous across 180', [[[[0, -179], [0, 100]], [0, 179], [None, None], [1, 1]]], [0, 4.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]]]]
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
weighted wrist[0, 350.0][0, 350.0]Passed
continuous across 180[0, 4.0][0, 4.0]Passed

SHA-256 / 776d28d2015bff1455e0317bc8427729a8f0bcb6e710959403a7aa1dd8b77840

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

Case digest / c2a2ccb72a3a77b69c93b21d73492c4e5c76bb62980587b7b264358c7db6b0ad