FAILURE MAP
← Case archive

FA-88226 / Inverse kinematics solvers / Open access

Damped step is refused at the kinematic singularity · case 01

A straight arm returns "singular" even though damping makes the system solvable.

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

ROOT CAUSE

The guard tests the undamped manipulability instead of the damped matrix determinant.

VERIFIED REPAIR

Guard on det(J^T J + lam^2 I) < 1e-12.

Unsuccessful approach: Raising the determinant threshold to 1e-6 still rejects lightly damped singular poses.

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=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 w<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]], ['small straight damping', [[1, 1, 0, 0, 0.0, 0.3, 0.3, 0.0002]], ['damped', 6.8755, 3.4377]]], [['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]], ['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]], ['small straight damping', [[1, 1, 0, 0, 0.0, 0.3, 0.3, 0.0002]], ['damped', 6.8755, 3.4377]]], [['straight arm damped', [[1, 1, 0, 0, 0.0, 0.3, 0.3, 0.2]], ['damped', 6.8209, 3.4105]], ['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]], ['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]], ['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]], ['small straight damping', [[1, 1, 0, 0, 0.0, 0.3, 0.3, 0.0002]], ['damped', 6.8755, 3.4377]]]]
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['singular', 0.0]['damped', 6.8209, 3.4105]Failed
straight arm undamped['singular', 0.0]['singular', 0.0]Passed
small straight damping['singular', 0.0]['damped', 6.8755, 3.4377]Failed

SHA-256 / af893445471daff6d086854b9d829e334f3a882f6ddc33da65fb4d79f803bbd5

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=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-6: 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]], ['small straight damping', [[1, 1, 0, 0, 0.0, 0.3, 0.3, 0.0002]], ['damped', 6.8755, 3.4377]]], [['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]], ['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]], ['small straight damping', [[1, 1, 0, 0, 0.0, 0.3, 0.3, 0.0002]], ['damped', 6.8755, 3.4377]]], [['straight arm damped', [[1, 1, 0, 0, 0.0, 0.3, 0.3, 0.2]], ['damped', 6.8209, 3.4105]], ['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]], ['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]], ['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]], ['small straight damping', [[1, 1, 0, 0, 0.0, 0.3, 0.3, 0.0002]], ['damped', 6.8755, 3.4377]]]]
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
small straight damping['singular', 0.0]['damped', 6.8755, 3.4377]Failed

SHA-256 / 017cdc94451d58fe5945448439d7bf385b26218e959de5fb8c46691eb35e6193

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]], ['small straight damping', [[1, 1, 0, 0, 0.0, 0.3, 0.3, 0.0002]], ['damped', 6.8755, 3.4377]]], [['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]], ['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]], ['small straight damping', [[1, 1, 0, 0, 0.0, 0.3, 0.3, 0.0002]], ['damped', 6.8755, 3.4377]]], [['straight arm damped', [[1, 1, 0, 0, 0.0, 0.3, 0.3, 0.2]], ['damped', 6.8209, 3.4105]], ['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]], ['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]], ['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]], ['small straight damping', [[1, 1, 0, 0, 0.0, 0.3, 0.3, 0.0002]], ['damped', 6.8755, 3.4377]]]]
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
small straight damping['damped', 6.8755, 3.4377]['damped', 6.8755, 3.4377]Passed

SHA-256 / f4344104c542ac6d389d18fd6fc75aca92ce1036bd0bd9373b7d56784f237266

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

Case digest / 931f25ba886d96808538594b3f547877a9d916ed8452396ce040bbc5e17cda1e