FAILURE MAP
← Case archive

FA-87851 / Inverse kinematics solvers / Open access

Elbow angle uses the interior triangle angle instead of the joint angle · case 01

Every reachable pose has the elbow bent to the supplementary angle and misses the target.

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

ROOT CAUSE

The law of cosines is written as the angle between the links (l1^2+l2^2-r^2) rather than the relative joint rotation.

VERIFIED REPAIR

Use cos q2 = (r^2-l1^2-l2^2)/(2 l1 l2).

Unsuccessful approach: Dropping the factor 2 in the denominator doubles the cosine and saturates the clamp.

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=(l1*l1+l2*l2-r2)/(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+180.0)%360.0-180.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'], ['between reach and reach squared', [[1, 1, 1.9, 1.2, 'up']], 'unreachable'], ['third quadrant target', [[2, 2, -2, -1, 'up']], [150.5529, 112.0243]]], [['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]], ['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]]], [['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]]], [['third quadrant target', [[2, 2, -2, -1, 'up']], [150.5529, 112.0243]], ['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]], ['positive y axis', [[2, 2, 0, 3, 'down']], [131.4096, -82.8192]]], [['second quadrant target', [[2, 1.5, -2, 1, 'down']], [-165.5619, -102.0247]], ['equal links near base', [[1, 1, 0.5, 0.2, 'up']], [-52.5785, 148.7597]], ['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, 180.0][0.0, 0.0]Failed
fully extended diagonal[-126.8699, 180.0][53.1301, 0.0]Failed
outside workspaceunreachableunreachablePassed
between reach and reach squaredunreachableunreachablePassed
third quadrant target[172.5772, 67.9757][150.5529, 112.0243]Failed

SHA-256 / 40a843e85172a302459ee01e370bcc6fc0932cf5f22940abe9feb10d0d5260bc

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)/(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+180.0)%360.0-180.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'], ['between reach and reach squared', [[1, 1, 1.9, 1.2, 'up']], 'unreachable'], ['third quadrant target', [[2, 2, -2, -1, 'up']], [150.5529, 112.0243]]], [['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]], ['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]]], [['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]]], [['third quadrant target', [[2, 2, -2, -1, 'up']], [150.5529, 112.0243]], ['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]], ['positive y axis', [[2, 2, 0, 3, 'down']], [131.4096, -82.8192]]], [['second quadrant target', [[2, 1.5, -2, 1, 'down']], [-165.5619, -102.0247]], ['equal links near base', [[1, 1, 0.5, 0.2, 'up']], [-52.5785, 148.7597]], ['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
between reach and reach squaredunreachableunreachablePassed
third quadrant target[137.2699, 138.5904][150.5529, 112.0243]Failed

SHA-256 / 990b6a2b15d1091e4e0428b49847e1f2068ec81067dfde5b98047b9d64f993e6

3 / The verified repair

Exit 0
"""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+180.0)%360.0-180.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'], ['between reach and reach squared', [[1, 1, 1.9, 1.2, 'up']], 'unreachable'], ['third quadrant target', [[2, 2, -2, -1, 'up']], [150.5529, 112.0243]]], [['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]], ['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]]], [['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]]], [['third quadrant target', [[2, 2, -2, -1, 'up']], [150.5529, 112.0243]], ['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]], ['positive y axis', [[2, 2, 0, 3, 'down']], [131.4096, -82.8192]]], [['second quadrant target', [[2, 1.5, -2, 1, 'down']], [-165.5619, -102.0247]], ['equal links near base', [[1, 1, 0.5, 0.2, 'up']], [-52.5785, 148.7597]], ['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
between reach and reach squaredunreachableunreachablePassed
third quadrant target[150.5529, 112.0243][150.5529, 112.0243]Passed

SHA-256 / 8da3f8b65b59204295f36bc88baa62a23b22b78aed803793d595d51153f7a02d

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

Case digest / 4e6f6f627babb8a944f5d4284386f34530d1183debc30dbf8f44de9e76e7164a