FA-88336 / Inverse kinematics solvers / Open access
Adaptive gain divides by the norm instead of the squared norm · case 01
Steps are too long or too short depending on the magnitude of J J^T e.
ROOT CAUSE
The denominator uses sqrt(h1^2+h2^2).
VERIFIED REPAIR
Divide by h1^2 + h2^2.
Unsuccessful approach: Adding 1 to the denominator as a regulariser biases every gain low.
Case contract
Input [l1,l2,q1,q2,tx,ty]: one Jacobian-transpose step for a planar 2R arm with the error-optimal gain alpha = <e, J J^T e> / <J J^T e, J J^T e>, dq = alpha J^T e. When <J J^T e, J J^T e> < 1e-15 return ["stationary", q1, q2]. Otherwise return ["step", alpha (6 decimals), q1 and q2 updated in degrees (4 decimals)].
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,q1,q2,tx,ty=x
a=math.radians(q1)
b=math.radians(q1+q2)
ex=tx-(l1*math.cos(a)+l2*math.cos(b))
ey=ty-(l1*math.sin(a)+l2*math.sin(b))
j11=-l1*math.sin(a)-l2*math.sin(b)
j12=-l2*math.sin(b)
j21=l1*math.cos(a)+l2*math.cos(b)
j22=l2*math.cos(b)
g1=j11*ex+j21*ey
g2=j12*ex+j22*ey
h1=j11*g1+j12*g2
h2=j21*g1+j22*g2
den=h1*h1+h2*h2
if den<1e-15: return ['stationary',q1,q2]
alpha=(ex*h1+ey*h2)/math.sqrt(den)
return ['step',round(alpha,6),round(q1+math.degrees(alpha*g1),4),round(q2+math.degrees(alpha*g2),4)]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['moderate error', [[1, 1, 30, 60, 0.5, 1.6]], ['step', 0.263207, 39.5859, 65.5199]], ['at target', [[1, 1, 0, 90, 1, 1]], ['stationary', 0, 90]], ['large error', [[1, 1, 0, 45, -1.5, 0.2]], ['step', 0.287793, 23.1194, 76.4812]], ['unequal links', [[2, 1, 20, -40, 1.5, 2.0]], ['step', 0.11371, 53.3906, -32.7888]], ['negative angles', [[1.5, 1, -30, -60, 0.8, -2]], ['step', 0.183895, -42.6234, -65.2581]], ['behind base', [[1, 1, 120, 30, -1, -1]], ['step', 0.217338, 154.0209, 53.2367]], ['small error', [[1, 1, 10, 10, 1.9, 0.6]], ['step', 0.201469, 12.0193, 11.0115]]], [['moderate error', [[1, 1, 30, 60, 0.5, 1.6]], ['step', 0.263207, 39.5859, 65.5199]], ['large error', [[1, 1, 0, 45, -1.5, 0.2]], ['step', 0.287793, 23.1194, 76.4812]], ['negative angles', [[1.5, 1, -30, -60, 0.8, -2]], ['step', 0.183895, -42.6234, -65.2581]], ['behind base', [[1, 1, 120, 30, -1, -1]], ['step', 0.217338, 154.0209, 53.2367]], ['small error', [[1, 1, 10, 10, 1.9, 0.6]], ['step', 0.201469, 12.0193, 11.0115]], ['long forearm', [[0.5, 2, 70, -20, -0.5, 2.2]], ['step', 0.098879, 93.8254, -1.5747]], ['straight arm radial error', [[1, 1, 0, 0, 1.5, 0]], ['stationary', 0, 0]]], [['moderate error', [[1, 1, 30, 60, 0.5, 1.6]], ['step', 0.263207, 39.5859, 65.5199]], ['at target', [[1, 1, 0, 90, 1, 1]], ['stationary', 0, 90]], ['large error', [[1, 1, 0, 45, -1.5, 0.2]], ['step', 0.287793, 23.1194, 76.4812]], ['unequal links', [[2, 1, 20, -40, 1.5, 2.0]], ['step', 0.11371, 53.3906, -32.7888]], ['negative angles', [[1.5, 1, -30, -60, 0.8, -2]], ['step', 0.183895, -42.6234, -65.2581]], ['long forearm', [[0.5, 2, 70, -20, -0.5, 2.2]], ['step', 0.098879, 93.8254, -1.5747]], ['straight arm radial error', [[1, 1, 0, 0, 1.5, 0]], ['stationary', 0, 0]]], [['at target', [[1, 1, 0, 90, 1, 1]], ['stationary', 0, 90]], ['large error', [[1, 1, 0, 45, -1.5, 0.2]], ['step', 0.287793, 23.1194, 76.4812]], ['unequal links', [[2, 1, 20, -40, 1.5, 2.0]], ['step', 0.11371, 53.3906, -32.7888]], ['negative angles', [[1.5, 1, -30, -60, 0.8, -2]], ['step', 0.183895, -42.6234, -65.2581]], ['behind base', [[1, 1, 120, 30, -1, -1]], ['step', 0.217338, 154.0209, 53.2367]], ['small error', [[1, 1, 10, 10, 1.9, 0.6]], ['step', 0.201469, 12.0193, 11.0115]], ['long forearm', [[0.5, 2, 70, -20, -0.5, 2.2]], ['step', 0.098879, 93.8254, -1.5747]]], [['moderate error', [[1, 1, 30, 60, 0.5, 1.6]], ['step', 0.263207, 39.5859, 65.5199]], ['at target', [[1, 1, 0, 90, 1, 1]], ['stationary', 0, 90]], ['negative angles', [[1.5, 1, -30, -60, 0.8, -2]], ['step', 0.183895, -42.6234, -65.2581]], ['behind base', [[1, 1, 120, 30, -1, -1]], ['step', 0.217338, 154.0209, 53.2367]], ['small error', [[1, 1, 10, 10, 1.9, 0.6]], ['step', 0.201469, 12.0193, 11.0115]], ['long forearm', [[0.5, 2, 70, -20, -0.5, 2.2]], ['step', 0.098879, 93.8254, -1.5747]], ['straight arm radial error', [[1, 1, 0, 0, 1.5, 0]], ['stationary', 0, 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 fixture | Actual | Expected | Outcome |
|---|---|---|---|
| moderate error | ['step', 0.376309, 43.705, 67.8919] | ['step', 0.263207, 39.5859, 65.5199] | Failed |
| at target | ['stationary', 0, 90] | ['stationary', 0, 90] | Passed |
| large error | ['step', 1.270732, 102.0821, 184.0033] | ['step', 0.287793, 23.1194, 76.4812] | Failed |
| unequal links | ['step', 1.76808, 539.1933, 72.1268] | ['step', 0.11371, 53.3906, -32.7888] | Failed |
| negative angles | ['step', 0.556558, -68.2048, -75.9135] | ['step', 0.183895, -42.6234, -65.2581] | Failed |
| behind base | ['step', 1.542404, 361.4402, 194.9067] | ['step', 0.217338, 154.0209, 53.2367] | Failed |
| small error | ['step', 0.087818, 10.8802, 10.4409] | ['step', 0.201469, 12.0193, 11.0115] | Failed |
SHA-256 / 791f5398eda76e2563610b739f553e7f85e30bad2132250c1711d1e61795a32d
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,q1,q2,tx,ty=x
a=math.radians(q1)
b=math.radians(q1+q2)
ex=tx-(l1*math.cos(a)+l2*math.cos(b))
ey=ty-(l1*math.sin(a)+l2*math.sin(b))
j11=-l1*math.sin(a)-l2*math.sin(b)
j12=-l2*math.sin(b)
j21=l1*math.cos(a)+l2*math.cos(b)
j22=l2*math.cos(b)
g1=j11*ex+j21*ey
g2=j12*ex+j22*ey
h1=j11*g1+j12*g2
h2=j21*g1+j22*g2
den=h1*h1+h2*h2
if den<1e-15: return ['stationary',q1,q2]
alpha=(ex*h1+ey*h2)/(den+1.0)
return ['step',round(alpha,6),round(q1+math.degrees(alpha*g1),4),round(q2+math.degrees(alpha*g2),4)]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['moderate error', [[1, 1, 30, 60, 0.5, 1.6]], ['step', 0.263207, 39.5859, 65.5199]], ['at target', [[1, 1, 0, 90, 1, 1]], ['stationary', 0, 90]], ['large error', [[1, 1, 0, 45, -1.5, 0.2]], ['step', 0.287793, 23.1194, 76.4812]], ['unequal links', [[2, 1, 20, -40, 1.5, 2.0]], ['step', 0.11371, 53.3906, -32.7888]], ['negative angles', [[1.5, 1, -30, -60, 0.8, -2]], ['step', 0.183895, -42.6234, -65.2581]], ['behind base', [[1, 1, 120, 30, -1, -1]], ['step', 0.217338, 154.0209, 53.2367]], ['small error', [[1, 1, 10, 10, 1.9, 0.6]], ['step', 0.201469, 12.0193, 11.0115]]], [['moderate error', [[1, 1, 30, 60, 0.5, 1.6]], ['step', 0.263207, 39.5859, 65.5199]], ['large error', [[1, 1, 0, 45, -1.5, 0.2]], ['step', 0.287793, 23.1194, 76.4812]], ['negative angles', [[1.5, 1, -30, -60, 0.8, -2]], ['step', 0.183895, -42.6234, -65.2581]], ['behind base', [[1, 1, 120, 30, -1, -1]], ['step', 0.217338, 154.0209, 53.2367]], ['small error', [[1, 1, 10, 10, 1.9, 0.6]], ['step', 0.201469, 12.0193, 11.0115]], ['long forearm', [[0.5, 2, 70, -20, -0.5, 2.2]], ['step', 0.098879, 93.8254, -1.5747]], ['straight arm radial error', [[1, 1, 0, 0, 1.5, 0]], ['stationary', 0, 0]]], [['moderate error', [[1, 1, 30, 60, 0.5, 1.6]], ['step', 0.263207, 39.5859, 65.5199]], ['at target', [[1, 1, 0, 90, 1, 1]], ['stationary', 0, 90]], ['large error', [[1, 1, 0, 45, -1.5, 0.2]], ['step', 0.287793, 23.1194, 76.4812]], ['unequal links', [[2, 1, 20, -40, 1.5, 2.0]], ['step', 0.11371, 53.3906, -32.7888]], ['negative angles', [[1.5, 1, -30, -60, 0.8, -2]], ['step', 0.183895, -42.6234, -65.2581]], ['long forearm', [[0.5, 2, 70, -20, -0.5, 2.2]], ['step', 0.098879, 93.8254, -1.5747]], ['straight arm radial error', [[1, 1, 0, 0, 1.5, 0]], ['stationary', 0, 0]]], [['at target', [[1, 1, 0, 90, 1, 1]], ['stationary', 0, 90]], ['large error', [[1, 1, 0, 45, -1.5, 0.2]], ['step', 0.287793, 23.1194, 76.4812]], ['unequal links', [[2, 1, 20, -40, 1.5, 2.0]], ['step', 0.11371, 53.3906, -32.7888]], ['negative angles', [[1.5, 1, -30, -60, 0.8, -2]], ['step', 0.183895, -42.6234, -65.2581]], ['behind base', [[1, 1, 120, 30, -1, -1]], ['step', 0.217338, 154.0209, 53.2367]], ['small error', [[1, 1, 10, 10, 1.9, 0.6]], ['step', 0.201469, 12.0193, 11.0115]], ['long forearm', [[0.5, 2, 70, -20, -0.5, 2.2]], ['step', 0.098879, 93.8254, -1.5747]]], [['moderate error', [[1, 1, 30, 60, 0.5, 1.6]], ['step', 0.263207, 39.5859, 65.5199]], ['at target', [[1, 1, 0, 90, 1, 1]], ['stationary', 0, 90]], ['negative angles', [[1.5, 1, -30, -60, 0.8, -2]], ['step', 0.183895, -42.6234, -65.2581]], ['behind base', [[1, 1, 120, 30, -1, -1]], ['step', 0.217338, 154.0209, 53.2367]], ['small error', [[1, 1, 10, 10, 1.9, 0.6]], ['step', 0.201469, 12.0193, 11.0115]], ['long forearm', [[0.5, 2, 70, -20, -0.5, 2.2]], ['step', 0.098879, 93.8254, -1.5747]], ['straight arm radial error', [[1, 1, 0, 0, 1.5, 0]], ['stationary', 0, 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 fixture | Actual | Expected | Outcome |
|---|---|---|---|
| moderate error | ['step', 0.176741, 36.4368, 63.7066] | ['step', 0.263207, 39.5859, 65.5199] | Failed |
| at target | ['stationary', 0, 90] | ['stationary', 0, 90] | Passed |
| large error | ['step', 0.273752, 21.9914, 74.9453] | ['step', 0.287793, 23.1194, 76.4812] | Failed |
| unequal links | ['step', 0.113241, 53.2531, -32.8185] | ['step', 0.11371, 53.3906, -32.7888] | Failed |
| negative angles | ['step', 0.165794, -41.3809, -64.7405] | ['step', 0.183895, -42.6234, -65.2581] | Failed |
| behind base | ['step', 0.213106, 153.3586, 52.7843] | ['step', 0.217338, 154.0209, 53.2367] | Failed |
| small error | ['step', 0.032167, 10.3224, 10.1615] | ['step', 0.201469, 12.0193, 11.0115] | Failed |
SHA-256 / 72018d865c34cf116bdd70f226b4a928cceb9e23f5f68b751306b004868af11a
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,q1,q2,tx,ty=x
a=math.radians(q1)
b=math.radians(q1+q2)
ex=tx-(l1*math.cos(a)+l2*math.cos(b))
ey=ty-(l1*math.sin(a)+l2*math.sin(b))
j11=-l1*math.sin(a)-l2*math.sin(b)
j12=-l2*math.sin(b)
j21=l1*math.cos(a)+l2*math.cos(b)
j22=l2*math.cos(b)
g1=j11*ex+j21*ey
g2=j12*ex+j22*ey
h1=j11*g1+j12*g2
h2=j21*g1+j22*g2
den=h1*h1+h2*h2
if den<1e-15: return ['stationary',q1,q2]
alpha=(ex*h1+ey*h2)/den
return ['step',round(alpha,6),round(q1+math.degrees(alpha*g1),4),round(q2+math.degrees(alpha*g2),4)]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['moderate error', [[1, 1, 30, 60, 0.5, 1.6]], ['step', 0.263207, 39.5859, 65.5199]], ['at target', [[1, 1, 0, 90, 1, 1]], ['stationary', 0, 90]], ['large error', [[1, 1, 0, 45, -1.5, 0.2]], ['step', 0.287793, 23.1194, 76.4812]], ['unequal links', [[2, 1, 20, -40, 1.5, 2.0]], ['step', 0.11371, 53.3906, -32.7888]], ['negative angles', [[1.5, 1, -30, -60, 0.8, -2]], ['step', 0.183895, -42.6234, -65.2581]], ['behind base', [[1, 1, 120, 30, -1, -1]], ['step', 0.217338, 154.0209, 53.2367]], ['small error', [[1, 1, 10, 10, 1.9, 0.6]], ['step', 0.201469, 12.0193, 11.0115]]], [['moderate error', [[1, 1, 30, 60, 0.5, 1.6]], ['step', 0.263207, 39.5859, 65.5199]], ['large error', [[1, 1, 0, 45, -1.5, 0.2]], ['step', 0.287793, 23.1194, 76.4812]], ['negative angles', [[1.5, 1, -30, -60, 0.8, -2]], ['step', 0.183895, -42.6234, -65.2581]], ['behind base', [[1, 1, 120, 30, -1, -1]], ['step', 0.217338, 154.0209, 53.2367]], ['small error', [[1, 1, 10, 10, 1.9, 0.6]], ['step', 0.201469, 12.0193, 11.0115]], ['long forearm', [[0.5, 2, 70, -20, -0.5, 2.2]], ['step', 0.098879, 93.8254, -1.5747]], ['straight arm radial error', [[1, 1, 0, 0, 1.5, 0]], ['stationary', 0, 0]]], [['moderate error', [[1, 1, 30, 60, 0.5, 1.6]], ['step', 0.263207, 39.5859, 65.5199]], ['at target', [[1, 1, 0, 90, 1, 1]], ['stationary', 0, 90]], ['large error', [[1, 1, 0, 45, -1.5, 0.2]], ['step', 0.287793, 23.1194, 76.4812]], ['unequal links', [[2, 1, 20, -40, 1.5, 2.0]], ['step', 0.11371, 53.3906, -32.7888]], ['negative angles', [[1.5, 1, -30, -60, 0.8, -2]], ['step', 0.183895, -42.6234, -65.2581]], ['long forearm', [[0.5, 2, 70, -20, -0.5, 2.2]], ['step', 0.098879, 93.8254, -1.5747]], ['straight arm radial error', [[1, 1, 0, 0, 1.5, 0]], ['stationary', 0, 0]]], [['at target', [[1, 1, 0, 90, 1, 1]], ['stationary', 0, 90]], ['large error', [[1, 1, 0, 45, -1.5, 0.2]], ['step', 0.287793, 23.1194, 76.4812]], ['unequal links', [[2, 1, 20, -40, 1.5, 2.0]], ['step', 0.11371, 53.3906, -32.7888]], ['negative angles', [[1.5, 1, -30, -60, 0.8, -2]], ['step', 0.183895, -42.6234, -65.2581]], ['behind base', [[1, 1, 120, 30, -1, -1]], ['step', 0.217338, 154.0209, 53.2367]], ['small error', [[1, 1, 10, 10, 1.9, 0.6]], ['step', 0.201469, 12.0193, 11.0115]], ['long forearm', [[0.5, 2, 70, -20, -0.5, 2.2]], ['step', 0.098879, 93.8254, -1.5747]]], [['moderate error', [[1, 1, 30, 60, 0.5, 1.6]], ['step', 0.263207, 39.5859, 65.5199]], ['at target', [[1, 1, 0, 90, 1, 1]], ['stationary', 0, 90]], ['negative angles', [[1.5, 1, -30, -60, 0.8, -2]], ['step', 0.183895, -42.6234, -65.2581]], ['behind base', [[1, 1, 120, 30, -1, -1]], ['step', 0.217338, 154.0209, 53.2367]], ['small error', [[1, 1, 10, 10, 1.9, 0.6]], ['step', 0.201469, 12.0193, 11.0115]], ['long forearm', [[0.5, 2, 70, -20, -0.5, 2.2]], ['step', 0.098879, 93.8254, -1.5747]], ['straight arm radial error', [[1, 1, 0, 0, 1.5, 0]], ['stationary', 0, 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 fixture | Actual | Expected | Outcome |
|---|---|---|---|
| moderate error | ['step', 0.263207, 39.5859, 65.5199] | ['step', 0.263207, 39.5859, 65.5199] | Passed |
| at target | ['stationary', 0, 90] | ['stationary', 0, 90] | Passed |
| large error | ['step', 0.287793, 23.1194, 76.4812] | ['step', 0.287793, 23.1194, 76.4812] | Passed |
| unequal links | ['step', 0.11371, 53.3906, -32.7888] | ['step', 0.11371, 53.3906, -32.7888] | Passed |
| negative angles | ['step', 0.183895, -42.6234, -65.2581] | ['step', 0.183895, -42.6234, -65.2581] | Passed |
| behind base | ['step', 0.217338, 154.0209, 53.2367] | ['step', 0.217338, 154.0209, 53.2367] | Passed |
| small error | ['step', 0.201469, 12.0193, 11.0115] | ['step', 0.201469, 12.0193, 11.0115] | Passed |
SHA-256 / 1b923daa195fa5466b9f7ae2e04ebbccafbf5b2dfab356a024694f41bb50c6ef
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:07.144645+00:00.
Case digest / 75292eba769ac76fe0e5c3b81b309bd9933cbd5463ab6382556923ac7c993a32