FAILURE MAP
← Case archive

FA-87871 / Inverse kinematics solvers / Open access

Shoulder angle is wrapped to 0..360 instead of the signed interval · case 01

Shoulder commands below zero come back as large positive angles.

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

ROOT CAUSE

The wrap uses d1 % 360, mapping into [0,360) rather than [-180,180).

THE FAILURE

The wrap uses d1 % 360, mapping into [0,360) rather than [-180,180).

Unsuccessful approach: Subtracting 360 only above 180 leaves results below -180 unwrapped.

Case contract

Input [l1,l2,px,py,elbow] with positive link lengths. Return [q1,q2] in degrees (4 decimals) for a planar two-link arm whose end effector reaches (px,py); elbow "up" gives q2>=0, "down" gives q2<=0; q1 is wrapped to [-180,180). Points outside the annulus |l1-l2|<=r<=l1+l2 (1e-9 slack on r squared) return "unreachable".

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):
    l1,l2,px,py,elbow=x
    r2=px*px+py*py
    reach=l1+l2
    if r2>reach*reach+1e-9: return 'unreachable'
    if r2<(l1-l2)**2-1e-9: return 'unreachable'
    c=(r2-l1*l1-l2*l2)/(2*l1*l2)
    c=max(-1.0,min(1.0,c))
    s=math.sqrt(1-c*c)
    if elbow=='down': s=-s
    q2=math.atan2(s,c)
    q1=math.atan2(py,px)-math.atan2(l2*s,l1+l2*c)
    d1=math.degrees(q1)
    d1=d1%360.0
    return [round(d1,4),round(math.degrees(q2),4)]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['general elbow up', [[2, 1, 2, 1, 'up']], [0.0, 90.0]], ['general elbow down', [[2, 1, 2, 1, 'down']], [53.1301, -90.0]], ['fully extended on x axis', [[2, 1, 3, 0, 'up']], [0.0, 0.0]], ['fully extended diagonal', [[3, 4, 4.2, 5.6, 'up']], [53.1301, 0.0]], ['outside workspace', [[2, 1, 3, 1, 'up']], 'unreachable'], ['third quadrant target', [[2, 2, -2, -1, 'up']], [150.5529, 112.0243]], ['second quadrant target', [[2, 1.5, -2, 1, 'down']], [-165.5619, -102.0247]]], [['fully extended diagonal', [[3, 4, 4.2, 5.6, 'up']], [53.1301, 0.0]], ['outside workspace', [[2, 1, 3, 1, 'up']], 'unreachable'], ['between reach and reach squared', [[1, 1, 1.9, 1.2, 'up']], 'unreachable'], ['inside inner hole', [[3, 1, 1, 0, 'up']], 'unreachable'], ['inner boundary folded', [[3, 1, 2, 0, 'down']], [0.0, -180.0]], ['negative y axis', [[1.5, 1, 0, -2, 'up']], [-118.955, 75.5225]], ['wrap past minus 180', [[1, 2, -2.2, -0.3, 'up']], [123.5259, 91.0027]]], [['inside inner hole', [[3, 1, 1, 0, 'up']], 'unreachable'], ['inner boundary folded', [[3, 1, 2, 0, 'down']], [0.0, -180.0]], ['third quadrant target', [[2, 2, -2, -1, 'up']], [150.5529, 112.0243]], ['second quadrant target', [[2, 1.5, -2, 1, 'down']], [-165.5619, -102.0247]], ['negative y axis', [[1.5, 1, 0, -2, 'up']], [-118.955, 75.5225]], ['behind base elbow down', [[2, 1, -2.5, -0.5, 'down']], [-147.3681, -67.9757]], ['equal links near base', [[1, 1, 0.5, 0.2, 'up']], [-52.5785, 148.7597]]], [['second quadrant target', [[2, 1.5, -2, 1, 'down']], [-165.5619, -102.0247]], ['negative y axis', [[1.5, 1, 0, -2, 'up']], [-118.955, 75.5225]], ['behind base elbow down', [[2, 1, -2.5, -0.5, 'down']], [-147.3681, -67.9757]], ['equal links near base', [[1, 1, 0.5, 0.2, 'up']], [-52.5785, 148.7597]], ['long forearm hole', [[1, 3, 0.5, 1, 'down']], 'unreachable'], ['long forearm reachable', [[1, 3, 2.5, 0.5, 'up']], [-95.7981, 125.6853]], ['wrap past minus 180', [[1, 2, -2.2, -0.3, 'up']], [123.5259, 91.0027]]], [['third quadrant target', [[2, 2, -2, -1, 'up']], [150.5529, 112.0243]], ['long forearm hole', [[1, 3, 0.5, 1, 'down']], 'unreachable'], ['long forearm reachable', [[1, 3, 2.5, 0.5, 'up']], [-95.7981, 125.6853]], ['positive y axis', [[2, 2, 0, 3, 'down']], [131.4096, -82.8192]], ['wrap past minus 180', [[1, 2, -2.2, -0.3, 'up']], [123.5259, 91.0027]], ['upper left down', [[3, 2, -1, 4, 'down']], [131.2512, -70.5288]], ['near full reach', [[5, 3, 7.9, 0.5, 'down']], [10.0523, -17.1893]]]]
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
general elbow up[0.0, 90.0][0.0, 90.0]Passed
general elbow down[53.1301, -90.0][53.1301, -90.0]Passed
fully extended on x axis[0.0, 0.0][0.0, 0.0]Passed
fully extended diagonal[53.1301, 0.0][53.1301, 0.0]Passed
outside workspaceunreachableunreachablePassed
third quadrant target[150.5529, 112.0243][150.5529, 112.0243]Passed
second quadrant target[194.4381, -102.0247][-165.5619, -102.0247]Failed

SHA-256 / 0393b4ae8471b89b3756ad5da9a7a9f4343bc82cdc42c872310ffa8ed3bb2bc7

2 / The unsuccessful fix

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(x):
    l1,l2,px,py,elbow=x
    r2=px*px+py*py
    reach=l1+l2
    if r2>reach*reach+1e-9: return 'unreachable'
    if r2<(l1-l2)**2-1e-9: return 'unreachable'
    c=(r2-l1*l1-l2*l2)/(2*l1*l2)
    c=max(-1.0,min(1.0,c))
    s=math.sqrt(1-c*c)
    if elbow=='down': s=-s
    q2=math.atan2(s,c)
    q1=math.atan2(py,px)-math.atan2(l2*s,l1+l2*c)
    d1=math.degrees(q1)
    d1=d1-360.0 if d1>180.0 else d1
    return [round(d1,4),round(math.degrees(q2),4)]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['general elbow up', [[2, 1, 2, 1, 'up']], [0.0, 90.0]], ['general elbow down', [[2, 1, 2, 1, 'down']], [53.1301, -90.0]], ['fully extended on x axis', [[2, 1, 3, 0, 'up']], [0.0, 0.0]], ['fully extended diagonal', [[3, 4, 4.2, 5.6, 'up']], [53.1301, 0.0]], ['outside workspace', [[2, 1, 3, 1, 'up']], 'unreachable'], ['third quadrant target', [[2, 2, -2, -1, 'up']], [150.5529, 112.0243]], ['second quadrant target', [[2, 1.5, -2, 1, 'down']], [-165.5619, -102.0247]]], [['fully extended diagonal', [[3, 4, 4.2, 5.6, 'up']], [53.1301, 0.0]], ['outside workspace', [[2, 1, 3, 1, 'up']], 'unreachable'], ['between reach and reach squared', [[1, 1, 1.9, 1.2, 'up']], 'unreachable'], ['inside inner hole', [[3, 1, 1, 0, 'up']], 'unreachable'], ['inner boundary folded', [[3, 1, 2, 0, 'down']], [0.0, -180.0]], ['negative y axis', [[1.5, 1, 0, -2, 'up']], [-118.955, 75.5225]], ['wrap past minus 180', [[1, 2, -2.2, -0.3, 'up']], [123.5259, 91.0027]]], [['inside inner hole', [[3, 1, 1, 0, 'up']], 'unreachable'], ['inner boundary folded', [[3, 1, 2, 0, 'down']], [0.0, -180.0]], ['third quadrant target', [[2, 2, -2, -1, 'up']], [150.5529, 112.0243]], ['second quadrant target', [[2, 1.5, -2, 1, 'down']], [-165.5619, -102.0247]], ['negative y axis', [[1.5, 1, 0, -2, 'up']], [-118.955, 75.5225]], ['behind base elbow down', [[2, 1, -2.5, -0.5, 'down']], [-147.3681, -67.9757]], ['equal links near base', [[1, 1, 0.5, 0.2, 'up']], [-52.5785, 148.7597]]], [['second quadrant target', [[2, 1.5, -2, 1, 'down']], [-165.5619, -102.0247]], ['negative y axis', [[1.5, 1, 0, -2, 'up']], [-118.955, 75.5225]], ['behind base elbow down', [[2, 1, -2.5, -0.5, 'down']], [-147.3681, -67.9757]], ['equal links near base', [[1, 1, 0.5, 0.2, 'up']], [-52.5785, 148.7597]], ['long forearm hole', [[1, 3, 0.5, 1, 'down']], 'unreachable'], ['long forearm reachable', [[1, 3, 2.5, 0.5, 'up']], [-95.7981, 125.6853]], ['wrap past minus 180', [[1, 2, -2.2, -0.3, 'up']], [123.5259, 91.0027]]], [['third quadrant target', [[2, 2, -2, -1, 'up']], [150.5529, 112.0243]], ['long forearm hole', [[1, 3, 0.5, 1, 'down']], 'unreachable'], ['long forearm reachable', [[1, 3, 2.5, 0.5, 'up']], [-95.7981, 125.6853]], ['positive y axis', [[2, 2, 0, 3, 'down']], [131.4096, -82.8192]], ['wrap past minus 180', [[1, 2, -2.2, -0.3, 'up']], [123.5259, 91.0027]], ['upper left down', [[3, 2, -1, 4, 'down']], [131.2512, -70.5288]], ['near full reach', [[5, 3, 7.9, 0.5, 'down']], [10.0523, -17.1893]]]]
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
general elbow up[0.0, 90.0][0.0, 90.0]Passed
general elbow down[53.1301, -90.0][53.1301, -90.0]Passed
fully extended on x axis[0.0, 0.0][0.0, 0.0]Passed
fully extended diagonal[53.1301, 0.0][53.1301, 0.0]Passed
outside workspaceunreachableunreachablePassed
third quadrant target[-209.4471, 112.0243][150.5529, 112.0243]Failed
second quadrant target[-165.5619, -102.0247][-165.5619, -102.0247]Passed

SHA-256 / ad123a31d760f5e1950dd94f517b1bf527e04778185091b3e9c304911dfcbcca

HELD IN THE MEMBER ARCHIVE

The verified repair and its recorded checks are member-only.

This mechanism has 7 recorded checks per implementation. The open-access tier publishes the failure and the unsuccessful fix; the repaired source that passes every check, and the observations that prove it, are available to members.

Every case sharing this mechanism uses the same contract and the same repair, so this one record is held back for all of them.

Member access is invitation-based. Sign in with your invited account to inspect the repair.

Sign in to the archive ↗

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

Case digest / 7bd65eaac15a5cd3dde2b08e466b731a8291666fc982982251fe18f316a983a2