FA-87866 / Inverse kinematics solvers / Open access
Target bearing reflects the goal across the diagonal · case 01
Targets off the 45 degree diagonal are reached at their mirror image.
ROOT CAUSE
The target bearing is evaluated as atan2(px,py) with the arguments swapped.
VERIFIED REPAIR
Use atan2(py,px) for the bearing of the target.
Unsuccessful approach: Replacing atan2 by atan(py/px) loses the quadrant for targets with negative x.
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(px,py)-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]]], [['general elbow down', [[2, 1, 2, 1, 'down']], [53.1301, -90.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]]], [['fully extended on x axis', [[2, 1, 3, 0, 'up']], [0.0, 0.0]], ['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]]], [['fully extended diagonal', [[3, 4, 4.2, 5.6, 'up']], [53.1301, 0.0]], ['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]]], [['inner boundary folded', [[3, 1, 2, 0, 'down']], [0.0, -180.0]], ['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 fixture | Actual | Expected | Outcome |
|---|---|---|---|
| general elbow up | [36.8699, 90.0] | [0.0, 90.0] | Failed |
| general elbow down | [90.0, -90.0] | [53.1301, -90.0] | Failed |
| fully extended on x axis | [90.0, 0.0] | [0.0, 0.0] | Failed |
| fully extended diagonal | [36.8699, 0.0] | [53.1301, 0.0] | Failed |
| outside workspace | unreachable | unreachable | Passed |
| between reach and reach squared | unreachable | unreachable | Passed |
| third quadrant target | [-172.5772, 112.0243] | [150.5529, 112.0243] | Failed |
SHA-256 / 8451b191113d74b1504119d0e178047466b5a93379546fd8ec56ab8c918e95e0
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.atan(py/px) if px else 0.0)-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]]], [['general elbow down', [[2, 1, 2, 1, 'down']], [53.1301, -90.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]]], [['fully extended on x axis', [[2, 1, 3, 0, 'up']], [0.0, 0.0]], ['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]]], [['fully extended diagonal', [[3, 4, 4.2, 5.6, 'up']], [53.1301, 0.0]], ['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]]], [['inner boundary folded', [[3, 1, 2, 0, 'down']], [0.0, -180.0]], ['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 fixture | Actual | Expected | Outcome |
|---|---|---|---|
| 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 workspace | unreachable | unreachable | Passed |
| between reach and reach squared | unreachable | unreachable | Passed |
| third quadrant target | [-29.4471, 112.0243] | [150.5529, 112.0243] | Failed |
SHA-256 / ac35d448d3b8fc1af04d4b51e16a4e07ca22238623eef2e334eb80b2e7cb4af2
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]]], [['general elbow down', [[2, 1, 2, 1, 'down']], [53.1301, -90.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]]], [['fully extended on x axis', [[2, 1, 3, 0, 'up']], [0.0, 0.0]], ['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]]], [['fully extended diagonal', [[3, 4, 4.2, 5.6, 'up']], [53.1301, 0.0]], ['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]]], [['inner boundary folded', [[3, 1, 2, 0, 'down']], [0.0, -180.0]], ['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 fixture | Actual | Expected | Outcome |
|---|---|---|---|
| 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 workspace | unreachable | unreachable | Passed |
| between reach and reach squared | unreachable | unreachable | Passed |
| third quadrant target | [150.5529, 112.0243] | [150.5529, 112.0243] | Passed |
SHA-256 / b600a6294716243912a2ecf6d412df62f85e613aa1a1ac51e8c5c55c949cb150
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.982648+00:00.
Case digest / d0099da88e9027602e13c5dacc0268c0967a8e1f8e01e4e499fafa1bc235194e