FAILURE MAP
← Case archive

FA-88211 / Inverse kinematics solvers / Open access

Negative elbow angles are treated as singular · case 01

Every elbow-down pose is damped as if it sat on the singularity.

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

ROOT CAUSE

The manipulability drops the absolute value, so negative sin q2 falls below the threshold.

VERIFIED REPAIR

Take the absolute value of l1 l2 sin q2.

Unsuccessful approach: Squaring the measure compares a squared quantity with an unsquared threshold.

Case contract

Input [l1,l2,q1,q2,vx,vy,w0,lam0]: resolved-rate step for a planar 2R arm. Manipulability w=|l1 l2 sin q2|. Damping lam^2 = 0 when w>=w0, else lam0^2 (1-w/w0)^2. Solve (J^T J + lam^2 I) qdot = J^T v; if that matrix determinant is below 1e-12 return ["singular", w]. Return [status, qdot1, qdot2] in degrees per second, status "damped" when lam^2>0 else "exact".

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,vx,vy,w0,lam0=x
    a=math.radians(q1)
    b=math.radians(q1+q2)
    J=[[-l1*math.sin(a)-l2*math.sin(b),-l2*math.sin(b)],[l1*math.cos(a)+l2*math.cos(b),l2*math.cos(b)]]
    w=l1*l2*math.sin(math.radians(q2))
    lam2=0.0 if w>=w0 else lam0*lam0*(1.0-w/w0)**2
    m11=J[0][0]**2+J[1][0]**2+lam2
    m12=J[0][0]*J[0][1]+J[1][0]*J[1][1]
    m22=J[0][1]**2+J[1][1]**2+lam2
    det=m11*m22-m12*m12
    if det<1e-12: return ['singular',round(w,6)]
    g1=J[0][0]*vx+J[1][0]*vy
    g2=J[0][1]*vx+J[1][1]*vy
    d1=(m22*g1-m12*g2)/det
    d2=(-m12*g1+m11*g2)/det
    return ['damped' if lam2>0 else 'exact',round(math.degrees(d1),4),round(math.degrees(d2),4)]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['well conditioned', [[1, 1, 30, 90, 0.1, 0.2, 0.3, 0.2]], ['exact', 7.0591, -17.7507]], ['near singular damped', [[1, 1, 30, 5, 0.1, 0.2, 0.3, 0.2]], ['damped', 11.4896, -16.5029]], ['negative elbow near singular', [[1, 1, 30, -5, 0.1, 0.2, 0.3, 0.2]], ['damped', -5.2725, 18.1241]], ['negative elbow well conditioned', [[1, 1, 10, -80, -0.2, 0.1, 0.3, 0.2]], ['exact', 9.4468, -19.8957]], ['straight arm damped', [[1, 1, 0, 0, 0.0, 0.3, 0.3, 0.2]], ['damped', 6.8209, 3.4105]], ['straight arm undamped', [[1, 1, 0, 0, 0.0, 0.3, 0.3, 0.0]], ['singular', 0.0]], ['inside zone no damping gain', [[1, 1, 45, 10, 0.2, -0.1, 0.5, 0.0]], ['exact', 10.8225, -34.1537]]], [['negative elbow near singular', [[1, 1, 30, -5, 0.1, 0.2, 0.3, 0.2]], ['damped', -5.2725, 18.1241]], ['negative elbow well conditioned', [[1, 1, 10, -80, -0.2, 0.1, 0.3, 0.2]], ['exact', 9.4468, -19.8957]], ['straight arm undamped', [[1, 1, 0, 0, 0.0, 0.3, 0.3, 0.0]], ['singular', 0.0]], ['inside zone no damping gain', [[1, 1, 45, 10, 0.2, -0.1, 0.5, 0.0]], ['exact', 10.8225, -34.1537]], ['folded arm', [[1, 1, 60, 175, 0.05, 0.05, 0.2, 0.1]], ['damped', -32.2582, 0.8237]], ['long links', [[2, 1.5, -20, 40, 0.3, 0.3, 0.5, 0.3]], ['exact', 17.1371, -27.792]], ['exact threshold', [[1, 2, 10, 30, 0.1, 0.1, 1.0, 0.4]], ['damped', 16.144, -22.7815]]], [['negative elbow near singular', [[1, 1, 30, -5, 0.1, 0.2, 0.3, 0.2]], ['damped', -5.2725, 18.1241]], ['folded arm', [[1, 1, 60, 175, 0.05, 0.05, 0.2, 0.1]], ['damped', -32.2582, 0.8237]], ['long links', [[2, 1.5, -20, 40, 0.3, 0.3, 0.5, 0.3]], ['exact', 17.1371, -27.792]], ['exact threshold', [[1, 2, 10, 30, 0.1, 0.1, 1.0, 0.4]], ['damped', 16.144, -22.7815]], ['large damping', [[1, 1, 90, 8, -0.1, 0.0, 0.8, 0.6]], ['damped', 2.2347, 0.9844]], ['slightly above threshold', [[1, 1, 0, 20, 0.2, 0.2, 0.3, 0.5]], ['exact', 42.9429, -76.4473]], ['small straight damping', [[1, 1, 0, 0, 0.0, 0.3, 0.3, 0.0002]], ['damped', 6.8755, 3.4377]]], [['well conditioned', [[1, 1, 30, 90, 0.1, 0.2, 0.3, 0.2]], ['exact', 7.0591, -17.7507]], ['near singular damped', [[1, 1, 30, 5, 0.1, 0.2, 0.3, 0.2]], ['damped', 11.4896, -16.5029]], ['negative elbow well conditioned', [[1, 1, 10, -80, -0.2, 0.1, 0.3, 0.2]], ['exact', 9.4468, -19.8957]], ['large damping', [[1, 1, 90, 8, -0.1, 0.0, 0.8, 0.6]], ['damped', 2.2347, 0.9844]], ['slightly above threshold', [[1, 1, 0, 20, 0.2, 0.2, 0.3, 0.5]], ['exact', 42.9429, -76.4473]], ['small straight damping', [[1, 1, 0, 0, 0.0, 0.3, 0.3, 0.0002]], ['damped', 6.8755, 3.4377]], ['mid zone', [[1.5, 1, 20, 12, 0.0, -0.2, 0.6, 0.25]], ['damped', -11.2758, 17.8448]]], [['well conditioned', [[1, 1, 30, 90, 0.1, 0.2, 0.3, 0.2]], ['exact', 7.0591, -17.7507]], ['near singular damped', [[1, 1, 30, 5, 0.1, 0.2, 0.3, 0.2]], ['damped', 11.4896, -16.5029]], ['negative elbow near singular', [[1, 1, 30, -5, 0.1, 0.2, 0.3, 0.2]], ['damped', -5.2725, 18.1241]], ['negative elbow well conditioned', [[1, 1, 10, -80, -0.2, 0.1, 0.3, 0.2]], ['exact', 9.4468, -19.8957]], ['straight arm damped', [[1, 1, 0, 0, 0.0, 0.3, 0.3, 0.2]], ['damped', 6.8209, 3.4105]], ['straight arm undamped', [[1, 1, 0, 0, 0.0, 0.3, 0.3, 0.0]], ['singular', 0.0]], ['slightly above threshold', [[1, 1, 0, 20, 0.2, 0.2, 0.3, 0.5]], ['exact', 42.9429, -76.4473]]]]
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
well conditioned['exact', 7.0591, -17.7507]['exact', 7.0591, -17.7507]Passed
near singular damped['damped', 11.4896, -16.5029]['damped', 11.4896, -16.5029]Passed
negative elbow near singular['damped', 0.3661, 6.7816]['damped', -5.2725, 18.1241]Failed
negative elbow well conditioned['damped', 2.0937, -6.4982]['exact', 9.4468, -19.8957]Failed
straight arm damped['damped', 6.8209, 3.4105]['damped', 6.8209, 3.4105]Passed
straight arm undamped['singular', 0.0]['singular', 0.0]Passed
inside zone no damping gain['exact', 10.8225, -34.1537]['exact', 10.8225, -34.1537]Passed

SHA-256 / 9a2950bc2522e63dc7d9e3ef6471765d4de31297b923b99d95a2f92e84dab916

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,vx,vy,w0,lam0=x
    a=math.radians(q1)
    b=math.radians(q1+q2)
    J=[[-l1*math.sin(a)-l2*math.sin(b),-l2*math.sin(b)],[l1*math.cos(a)+l2*math.cos(b),l2*math.cos(b)]]
    w=(l1*l2*math.sin(math.radians(q2)))**2
    lam2=0.0 if w>=w0 else lam0*lam0*(1.0-w/w0)**2
    m11=J[0][0]**2+J[1][0]**2+lam2
    m12=J[0][0]*J[0][1]+J[1][0]*J[1][1]
    m22=J[0][1]**2+J[1][1]**2+lam2
    det=m11*m22-m12*m12
    if det<1e-12: return ['singular',round(w,6)]
    g1=J[0][0]*vx+J[1][0]*vy
    g2=J[0][1]*vx+J[1][1]*vy
    d1=(m22*g1-m12*g2)/det
    d2=(-m12*g1+m11*g2)/det
    return ['damped' if lam2>0 else 'exact',round(math.degrees(d1),4),round(math.degrees(d2),4)]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['well conditioned', [[1, 1, 30, 90, 0.1, 0.2, 0.3, 0.2]], ['exact', 7.0591, -17.7507]], ['near singular damped', [[1, 1, 30, 5, 0.1, 0.2, 0.3, 0.2]], ['damped', 11.4896, -16.5029]], ['negative elbow near singular', [[1, 1, 30, -5, 0.1, 0.2, 0.3, 0.2]], ['damped', -5.2725, 18.1241]], ['negative elbow well conditioned', [[1, 1, 10, -80, -0.2, 0.1, 0.3, 0.2]], ['exact', 9.4468, -19.8957]], ['straight arm damped', [[1, 1, 0, 0, 0.0, 0.3, 0.3, 0.2]], ['damped', 6.8209, 3.4105]], ['straight arm undamped', [[1, 1, 0, 0, 0.0, 0.3, 0.3, 0.0]], ['singular', 0.0]], ['inside zone no damping gain', [[1, 1, 45, 10, 0.2, -0.1, 0.5, 0.0]], ['exact', 10.8225, -34.1537]]], [['negative elbow near singular', [[1, 1, 30, -5, 0.1, 0.2, 0.3, 0.2]], ['damped', -5.2725, 18.1241]], ['negative elbow well conditioned', [[1, 1, 10, -80, -0.2, 0.1, 0.3, 0.2]], ['exact', 9.4468, -19.8957]], ['straight arm undamped', [[1, 1, 0, 0, 0.0, 0.3, 0.3, 0.0]], ['singular', 0.0]], ['inside zone no damping gain', [[1, 1, 45, 10, 0.2, -0.1, 0.5, 0.0]], ['exact', 10.8225, -34.1537]], ['folded arm', [[1, 1, 60, 175, 0.05, 0.05, 0.2, 0.1]], ['damped', -32.2582, 0.8237]], ['long links', [[2, 1.5, -20, 40, 0.3, 0.3, 0.5, 0.3]], ['exact', 17.1371, -27.792]], ['exact threshold', [[1, 2, 10, 30, 0.1, 0.1, 1.0, 0.4]], ['damped', 16.144, -22.7815]]], [['negative elbow near singular', [[1, 1, 30, -5, 0.1, 0.2, 0.3, 0.2]], ['damped', -5.2725, 18.1241]], ['folded arm', [[1, 1, 60, 175, 0.05, 0.05, 0.2, 0.1]], ['damped', -32.2582, 0.8237]], ['long links', [[2, 1.5, -20, 40, 0.3, 0.3, 0.5, 0.3]], ['exact', 17.1371, -27.792]], ['exact threshold', [[1, 2, 10, 30, 0.1, 0.1, 1.0, 0.4]], ['damped', 16.144, -22.7815]], ['large damping', [[1, 1, 90, 8, -0.1, 0.0, 0.8, 0.6]], ['damped', 2.2347, 0.9844]], ['slightly above threshold', [[1, 1, 0, 20, 0.2, 0.2, 0.3, 0.5]], ['exact', 42.9429, -76.4473]], ['small straight damping', [[1, 1, 0, 0, 0.0, 0.3, 0.3, 0.0002]], ['damped', 6.8755, 3.4377]]], [['well conditioned', [[1, 1, 30, 90, 0.1, 0.2, 0.3, 0.2]], ['exact', 7.0591, -17.7507]], ['near singular damped', [[1, 1, 30, 5, 0.1, 0.2, 0.3, 0.2]], ['damped', 11.4896, -16.5029]], ['negative elbow well conditioned', [[1, 1, 10, -80, -0.2, 0.1, 0.3, 0.2]], ['exact', 9.4468, -19.8957]], ['large damping', [[1, 1, 90, 8, -0.1, 0.0, 0.8, 0.6]], ['damped', 2.2347, 0.9844]], ['slightly above threshold', [[1, 1, 0, 20, 0.2, 0.2, 0.3, 0.5]], ['exact', 42.9429, -76.4473]], ['small straight damping', [[1, 1, 0, 0, 0.0, 0.3, 0.3, 0.0002]], ['damped', 6.8755, 3.4377]], ['mid zone', [[1.5, 1, 20, 12, 0.0, -0.2, 0.6, 0.25]], ['damped', -11.2758, 17.8448]]], [['well conditioned', [[1, 1, 30, 90, 0.1, 0.2, 0.3, 0.2]], ['exact', 7.0591, -17.7507]], ['near singular damped', [[1, 1, 30, 5, 0.1, 0.2, 0.3, 0.2]], ['damped', 11.4896, -16.5029]], ['negative elbow near singular', [[1, 1, 30, -5, 0.1, 0.2, 0.3, 0.2]], ['damped', -5.2725, 18.1241]], ['negative elbow well conditioned', [[1, 1, 10, -80, -0.2, 0.1, 0.3, 0.2]], ['exact', 9.4468, -19.8957]], ['straight arm damped', [[1, 1, 0, 0, 0.0, 0.3, 0.3, 0.2]], ['damped', 6.8209, 3.4105]], ['straight arm undamped', [[1, 1, 0, 0, 0.0, 0.3, 0.3, 0.0]], ['singular', 0.0]], ['slightly above threshold', [[1, 1, 0, 20, 0.2, 0.2, 0.3, 0.5]], ['exact', 42.9429, -76.4473]]]]
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
well conditioned['exact', 7.0591, -17.7507]['exact', 7.0591, -17.7507]Passed
near singular damped['damped', 7.4564, -8.4624]['damped', 11.4896, -16.5029]Failed
negative elbow near singular['damped', -1.5284, 10.6119]['damped', -5.2725, 18.1241]Failed
negative elbow well conditioned['exact', 9.4468, -19.8957]['exact', 9.4468, -19.8957]Passed
straight arm damped['damped', 6.8209, 3.4105]['damped', 6.8209, 3.4105]Passed
straight arm undamped['singular', 0.0]['singular', 0.0]Passed
inside zone no damping gain['exact', 10.8225, -34.1537]['exact', 10.8225, -34.1537]Passed

SHA-256 / 5cd8b20d9401c9453b41afd05d474f8b6e8cc9af0599b19e3a7627009c0bef13

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,vx,vy,w0,lam0=x
    a=math.radians(q1)
    b=math.radians(q1+q2)
    J=[[-l1*math.sin(a)-l2*math.sin(b),-l2*math.sin(b)],[l1*math.cos(a)+l2*math.cos(b),l2*math.cos(b)]]
    w=abs(l1*l2*math.sin(math.radians(q2)))
    lam2=0.0 if w>=w0 else lam0*lam0*(1.0-w/w0)**2
    m11=J[0][0]**2+J[1][0]**2+lam2
    m12=J[0][0]*J[0][1]+J[1][0]*J[1][1]
    m22=J[0][1]**2+J[1][1]**2+lam2
    det=m11*m22-m12*m12
    if det<1e-12: return ['singular',round(w,6)]
    g1=J[0][0]*vx+J[1][0]*vy
    g2=J[0][1]*vx+J[1][1]*vy
    d1=(m22*g1-m12*g2)/det
    d2=(-m12*g1+m11*g2)/det
    return ['damped' if lam2>0 else 'exact',round(math.degrees(d1),4),round(math.degrees(d2),4)]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['well conditioned', [[1, 1, 30, 90, 0.1, 0.2, 0.3, 0.2]], ['exact', 7.0591, -17.7507]], ['near singular damped', [[1, 1, 30, 5, 0.1, 0.2, 0.3, 0.2]], ['damped', 11.4896, -16.5029]], ['negative elbow near singular', [[1, 1, 30, -5, 0.1, 0.2, 0.3, 0.2]], ['damped', -5.2725, 18.1241]], ['negative elbow well conditioned', [[1, 1, 10, -80, -0.2, 0.1, 0.3, 0.2]], ['exact', 9.4468, -19.8957]], ['straight arm damped', [[1, 1, 0, 0, 0.0, 0.3, 0.3, 0.2]], ['damped', 6.8209, 3.4105]], ['straight arm undamped', [[1, 1, 0, 0, 0.0, 0.3, 0.3, 0.0]], ['singular', 0.0]], ['inside zone no damping gain', [[1, 1, 45, 10, 0.2, -0.1, 0.5, 0.0]], ['exact', 10.8225, -34.1537]]], [['negative elbow near singular', [[1, 1, 30, -5, 0.1, 0.2, 0.3, 0.2]], ['damped', -5.2725, 18.1241]], ['negative elbow well conditioned', [[1, 1, 10, -80, -0.2, 0.1, 0.3, 0.2]], ['exact', 9.4468, -19.8957]], ['straight arm undamped', [[1, 1, 0, 0, 0.0, 0.3, 0.3, 0.0]], ['singular', 0.0]], ['inside zone no damping gain', [[1, 1, 45, 10, 0.2, -0.1, 0.5, 0.0]], ['exact', 10.8225, -34.1537]], ['folded arm', [[1, 1, 60, 175, 0.05, 0.05, 0.2, 0.1]], ['damped', -32.2582, 0.8237]], ['long links', [[2, 1.5, -20, 40, 0.3, 0.3, 0.5, 0.3]], ['exact', 17.1371, -27.792]], ['exact threshold', [[1, 2, 10, 30, 0.1, 0.1, 1.0, 0.4]], ['damped', 16.144, -22.7815]]], [['negative elbow near singular', [[1, 1, 30, -5, 0.1, 0.2, 0.3, 0.2]], ['damped', -5.2725, 18.1241]], ['folded arm', [[1, 1, 60, 175, 0.05, 0.05, 0.2, 0.1]], ['damped', -32.2582, 0.8237]], ['long links', [[2, 1.5, -20, 40, 0.3, 0.3, 0.5, 0.3]], ['exact', 17.1371, -27.792]], ['exact threshold', [[1, 2, 10, 30, 0.1, 0.1, 1.0, 0.4]], ['damped', 16.144, -22.7815]], ['large damping', [[1, 1, 90, 8, -0.1, 0.0, 0.8, 0.6]], ['damped', 2.2347, 0.9844]], ['slightly above threshold', [[1, 1, 0, 20, 0.2, 0.2, 0.3, 0.5]], ['exact', 42.9429, -76.4473]], ['small straight damping', [[1, 1, 0, 0, 0.0, 0.3, 0.3, 0.0002]], ['damped', 6.8755, 3.4377]]], [['well conditioned', [[1, 1, 30, 90, 0.1, 0.2, 0.3, 0.2]], ['exact', 7.0591, -17.7507]], ['near singular damped', [[1, 1, 30, 5, 0.1, 0.2, 0.3, 0.2]], ['damped', 11.4896, -16.5029]], ['negative elbow well conditioned', [[1, 1, 10, -80, -0.2, 0.1, 0.3, 0.2]], ['exact', 9.4468, -19.8957]], ['large damping', [[1, 1, 90, 8, -0.1, 0.0, 0.8, 0.6]], ['damped', 2.2347, 0.9844]], ['slightly above threshold', [[1, 1, 0, 20, 0.2, 0.2, 0.3, 0.5]], ['exact', 42.9429, -76.4473]], ['small straight damping', [[1, 1, 0, 0, 0.0, 0.3, 0.3, 0.0002]], ['damped', 6.8755, 3.4377]], ['mid zone', [[1.5, 1, 20, 12, 0.0, -0.2, 0.6, 0.25]], ['damped', -11.2758, 17.8448]]], [['well conditioned', [[1, 1, 30, 90, 0.1, 0.2, 0.3, 0.2]], ['exact', 7.0591, -17.7507]], ['near singular damped', [[1, 1, 30, 5, 0.1, 0.2, 0.3, 0.2]], ['damped', 11.4896, -16.5029]], ['negative elbow near singular', [[1, 1, 30, -5, 0.1, 0.2, 0.3, 0.2]], ['damped', -5.2725, 18.1241]], ['negative elbow well conditioned', [[1, 1, 10, -80, -0.2, 0.1, 0.3, 0.2]], ['exact', 9.4468, -19.8957]], ['straight arm damped', [[1, 1, 0, 0, 0.0, 0.3, 0.3, 0.2]], ['damped', 6.8209, 3.4105]], ['straight arm undamped', [[1, 1, 0, 0, 0.0, 0.3, 0.3, 0.0]], ['singular', 0.0]], ['slightly above threshold', [[1, 1, 0, 20, 0.2, 0.2, 0.3, 0.5]], ['exact', 42.9429, -76.4473]]]]
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
well conditioned['exact', 7.0591, -17.7507]['exact', 7.0591, -17.7507]Passed
near singular damped['damped', 11.4896, -16.5029]['damped', 11.4896, -16.5029]Passed
negative elbow near singular['damped', -5.2725, 18.1241]['damped', -5.2725, 18.1241]Passed
negative elbow well conditioned['exact', 9.4468, -19.8957]['exact', 9.4468, -19.8957]Passed
straight arm damped['damped', 6.8209, 3.4105]['damped', 6.8209, 3.4105]Passed
straight arm undamped['singular', 0.0]['singular', 0.0]Passed
inside zone no damping gain['exact', 10.8225, -34.1537]['exact', 10.8225, -34.1537]Passed

SHA-256 / 6e9985182e462982f02d273034a87899e4141ab3d1901faa733bd5030b09f764

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

Case digest / 9c51684b005ca70d41e4acfb33b6657b3932809894b871987103bd423913d151