{"abstract":"Each joint rotates away from the target by the angle it should close.","category":"Inverse kinematics solvers","checks":7,"contract":"Input [lengths, angles, target, iters, tol, lim]: planar serial chain with relative joint angles in degrees and a symmetric joint limit +/-lim degrees. Each iteration first stops if the tip is within tol of target, else counts one sweep that visits joints from the tip joint down to the base, re-evaluating forward kinematics before each joint, rotating by the signed angle between tip and target vectors and clamping the joint. Return [angles (3 decimals), sweeps performed, final error (4 decimals)].","evaluation_group":"w2-inverse_kinematics_solvers-cyclic-coordinate-descent","failed_approach":"Subtracting two atan2 bearings is not wrapped and turns the long way when the difference crosses pi.","family":"w2-inverse_kinematics_solvers-cyclic-coordinate-descent-signed-rotation-angle","id":"FA-87946","implementations":{"attempt":{"sha256":"74da680536650a17c26b9cdb2283a6a5a6ee61b6f1f6e49de059b3ab63aff10c","source":"\"\"\"Failure Map reference implementation. Python standard library only.\"\"\"\nimport json\nimport math\nN = 1\nobservations = []\ndef solve(x):\n    ls,qs,t,iters,tol,lim=x\n    qs=[math.radians(q) for q in qs]\n    lim=math.radians(lim)\n    def fk(qs):\n        pts=[(0.0,0.0)]\n        a=0.0\n        for l,q in zip(ls,qs):\n            a+=q\n            px,py=pts[-1]\n            pts.append((px+l*math.cos(a),py+l*math.sin(a)))\n        return pts\n    used=0\n    for it in range(iters):\n        pts=fk(qs)\n        ex,ey=pts[-1]\n        if math.hypot(t[0]-ex,t[1]-ey)<=tol: break\n        used+=1\n        for j in range(len(ls)-1,-1,-1):\n            pts=fk(qs)\n            jx,jy=pts[j]\n            ex,ey=pts[-1]\n            ve=(ex-jx,ey-jy)\n            vt=(t[0]-jx,t[1]-jy)\n            ang=math.atan2(vt[1],vt[0])-math.atan2(ve[1],ve[0])\n            qs[j]=max(-lim,min(lim,qs[j]+ang))\n    pts=fk(qs)\n    err=math.hypot(t[0]-pts[-1][0],t[1]-pts[-1][1])\n    return [[round(math.degrees(q),3) for q in qs],used,round(err,4)]\ndef check(label, actual, expected):\n    observations.append({\"check\": label, \"actual\": actual, \"expected\": expected, \"passed\": actual == expected})\nfixtures = [[['three links one sweep', [[[1, 1, 1], [0, 0, 0], [1.5, 1.5], 1, 0.001, 170]], [[5.996, 17.348, 108.435], 1, 0.3586]], ['three links several sweeps', [[[1, 1, 1], [10, 10, 10], [-1, 2], 4, 0.001, 170]], [[63.011, 58.047, 42.064], 4, 0.0426]], ['target behind base', [[[1, 1], [0, 0], [-1.5, -0.2], 3, 0.001, 175]], [[-121.779, -101.253], 3, 0.2445]], ['tight limits', [[[1, 1, 1], [0, 0, 0], [0.5, 2.5], 3, 0.001, 45]], [[36.006, 45.0, 37.294], 3, 0.0448]], ['already converged', [[[1, 1], [0, 90], [1, 1], 5, 0.001, 170]], [[0.0, 90.0], 0, 0.0]], ['converges early', [[[2, 1], [0, 0], [2, 1], 10, 0.01, 170]], [[0.0, 90.0], 1, 0.0]], ['unreachable stretch', [[[1, 1], [30, 30], [4, 0], 3, 0.001, 170]], [[8.551, -17.103], 3, 2.0222]]], [['three links several sweeps', [[[1, 1, 1], [10, 10, 10], [-1, 2], 4, 0.001, 170]], [[63.011, 58.047, 42.064], 4, 0.0426]], ['already converged', [[[1, 1], [0, 90], [1, 1], 5, 0.001, 170]], [[0.0, 90.0], 0, 0.0]], ['converges early', [[[2, 1], [0, 0], [2, 1], 10, 0.01, 170]], [[0.0, 90.0], 1, 0.0]], ['unreachable stretch', [[[1, 1], [30, 30], [4, 0], 3, 0.001, 170]], [[8.551, -17.103], 3, 2.0222]], ['four links curl', [[[1, 0.8, 0.6, 0.4], [20, 20, 20, 20], [0.3, 1.2], 3, 0.001, 120]], [[36.469, 53.792, 70.335, 118.779], 3, 0.001]], ['limit blocks tip joint', [[[1, 1], [0, 0], [0, -1.8], 2, 0.001, 30]], [[-30.0, -30.0], 2, 1.4333]], ['zero iterations', [[[1, 1], [0, 0], [0, 2], 0, 0.001, 170]], [[0.0, 0.0], 0, 2.8284]]], [['target behind base', [[[1, 1], [0, 0], [-1.5, -0.2], 3, 0.001, 175]], [[-121.779, -101.253], 3, 0.2445]], ['four links curl', [[[1, 0.8, 0.6, 0.4], [20, 20, 20, 20], [0.3, 1.2], 3, 0.001, 120]], [[36.469, 53.792, 70.335, 118.779], 3, 0.001]], ['limit blocks tip joint', [[[1, 1], [0, 0], [0, -1.8], 2, 0.001, 30]], [[-30.0, -30.0], 2, 1.4333]], ['zero iterations', [[[1, 1], [0, 0], [0, 2], 0, 0.001, 170]], [[0.0, 0.0], 0, 2.8284]], ['target crossing pi', [[[1, 1, 1], [170, 0, 0], [-2, -0.5], 2, 0.001, 180]], [[168.982, -6.514, 102.831], 2, 0.0176]], ['loose tolerance', [[[1, 1, 1], [5, 5, 5], [2.9, 0.4], 3, 0.2, 170]], [[5.0, 5.0, 5.0], 0, 0.1285]], ['negative limits start', [[[1.5, 1], [-60, -60], [1, 1.5], 2, 0.001, 90]], [[90.0, -90.0], 1, 0.0]]], [['three links one sweep', [[[1, 1, 1], [0, 0, 0], [1.5, 1.5], 1, 0.001, 170]], [[5.996, 17.348, 108.435], 1, 0.3586]], ['three links several sweeps', [[[1, 1, 1], [10, 10, 10], [-1, 2], 4, 0.001, 170]], [[63.011, 58.047, 42.064], 4, 0.0426]], ['tight limits', [[[1, 1, 1], [0, 0, 0], [0.5, 2.5], 3, 0.001, 45]], [[36.006, 45.0, 37.294], 3, 0.0448]], ['target crossing pi', [[[1, 1, 1], [170, 0, 0], [-2, -0.5], 2, 0.001, 180]], [[168.982, -6.514, 102.831], 2, 0.0176]], ['loose tolerance', [[[1, 1, 1], [5, 5, 5], [2.9, 0.4], 3, 0.2, 170]], [[5.0, 5.0, 5.0], 0, 0.1285]], ['negative limits start', [[[1.5, 1], [-60, -60], [1, 1.5], 2, 0.001, 90]], [[90.0, -90.0], 1, 0.0]], ['tight elbow limit', [[[1, 1, 1], [0, 0, 0], [-1, 1], 3, 0.001, 60]], [[60.0, 60.0, 60.0], 3, 0.7321]]], [['three links one sweep', [[[1, 1, 1], [0, 0, 0], [1.5, 1.5], 1, 0.001, 170]], [[5.996, 17.348, 108.435], 1, 0.3586]], ['three links several sweeps', [[[1, 1, 1], [10, 10, 10], [-1, 2], 4, 0.001, 170]], [[63.011, 58.047, 42.064], 4, 0.0426]], ['target behind base', [[[1, 1], [0, 0], [-1.5, -0.2], 3, 0.001, 175]], [[-121.779, -101.253], 3, 0.2445]], ['tight limits', [[[1, 1, 1], [0, 0, 0], [0.5, 2.5], 3, 0.001, 45]], [[36.006, 45.0, 37.294], 3, 0.0448]], ['already converged', [[[1, 1], [0, 90], [1, 1], 5, 0.001, 170]], [[0.0, 90.0], 0, 0.0]], ['converges early', [[[2, 1], [0, 0], [2, 1], 10, 0.01, 170]], [[0.0, 90.0], 1, 0.0]], ['tight elbow limit', [[[1, 1, 1], [0, 0, 0], [-1, 1], 3, 0.001, 60]], [[60.0, 60.0, 60.0], 3, 0.7321]]]]\nfor label, args, expected in fixtures[N-1]:\n    check(label, solve(*args), expected)\nprint(json.dumps({\"observations\": observations, \"passed\": all(x[\"passed\"] for x in observations)}, ensure_ascii=False))\nraise SystemExit(0 if all(x[\"passed\"] for x in observations) else 1)\n"},"broken":{"sha256":"9d8de75125ed6d9101d30b5ddc503f8fab1cf4d727708c5f64aeabb3f0eae76c","source":"\"\"\"Failure Map reference implementation. Python standard library only.\"\"\"\nimport json\nimport math\nN = 1\nobservations = []\ndef solve(x):\n    ls,qs,t,iters,tol,lim=x\n    qs=[math.radians(q) for q in qs]\n    lim=math.radians(lim)\n    def fk(qs):\n        pts=[(0.0,0.0)]\n        a=0.0\n        for l,q in zip(ls,qs):\n            a+=q\n            px,py=pts[-1]\n            pts.append((px+l*math.cos(a),py+l*math.sin(a)))\n        return pts\n    used=0\n    for it in range(iters):\n        pts=fk(qs)\n        ex,ey=pts[-1]\n        if math.hypot(t[0]-ex,t[1]-ey)<=tol: break\n        used+=1\n        for j in range(len(ls)-1,-1,-1):\n            pts=fk(qs)\n            jx,jy=pts[j]\n            ex,ey=pts[-1]\n            ve=(ex-jx,ey-jy)\n            vt=(t[0]-jx,t[1]-jy)\n            ang=math.atan2(vt[0]*ve[1]-vt[1]*ve[0],ve[0]*vt[0]+ve[1]*vt[1])\n            qs[j]=max(-lim,min(lim,qs[j]+ang))\n    pts=fk(qs)\n    err=math.hypot(t[0]-pts[-1][0],t[1]-pts[-1][1])\n    return [[round(math.degrees(q),3) for q in qs],used,round(err,4)]\ndef check(label, actual, expected):\n    observations.append({\"check\": label, \"actual\": actual, \"expected\": expected, \"passed\": actual == expected})\nfixtures = [[['three links one sweep', [[[1, 1, 1], [0, 0, 0], [1.5, 1.5], 1, 0.001, 170]], [[5.996, 17.348, 108.435], 1, 0.3586]], ['three links several sweeps', [[[1, 1, 1], [10, 10, 10], [-1, 2], 4, 0.001, 170]], [[63.011, 58.047, 42.064], 4, 0.0426]], ['target behind base', [[[1, 1], [0, 0], [-1.5, -0.2], 3, 0.001, 175]], [[-121.779, -101.253], 3, 0.2445]], ['tight limits', [[[1, 1, 1], [0, 0, 0], [0.5, 2.5], 3, 0.001, 45]], [[36.006, 45.0, 37.294], 3, 0.0448]], ['already converged', [[[1, 1], [0, 90], [1, 1], 5, 0.001, 170]], [[0.0, 90.0], 0, 0.0]], ['converges early', [[[2, 1], [0, 0], [2, 1], 10, 0.01, 170]], [[0.0, 90.0], 1, 0.0]], ['unreachable stretch', [[[1, 1], [30, 30], [4, 0], 3, 0.001, 170]], [[8.551, -17.103], 3, 2.0222]]], [['three links several sweeps', [[[1, 1, 1], [10, 10, 10], [-1, 2], 4, 0.001, 170]], [[63.011, 58.047, 42.064], 4, 0.0426]], ['already converged', [[[1, 1], [0, 90], [1, 1], 5, 0.001, 170]], [[0.0, 90.0], 0, 0.0]], ['converges early', [[[2, 1], [0, 0], [2, 1], 10, 0.01, 170]], [[0.0, 90.0], 1, 0.0]], ['unreachable stretch', [[[1, 1], [30, 30], [4, 0], 3, 0.001, 170]], [[8.551, -17.103], 3, 2.0222]], ['four links curl', [[[1, 0.8, 0.6, 0.4], [20, 20, 20, 20], [0.3, 1.2], 3, 0.001, 120]], [[36.469, 53.792, 70.335, 118.779], 3, 0.001]], ['limit blocks tip joint', [[[1, 1], [0, 0], [0, -1.8], 2, 0.001, 30]], [[-30.0, -30.0], 2, 1.4333]], ['zero iterations', [[[1, 1], [0, 0], [0, 2], 0, 0.001, 170]], [[0.0, 0.0], 0, 2.8284]]], [['target behind base', [[[1, 1], [0, 0], [-1.5, -0.2], 3, 0.001, 175]], [[-121.779, -101.253], 3, 0.2445]], ['four links curl', [[[1, 0.8, 0.6, 0.4], [20, 20, 20, 20], [0.3, 1.2], 3, 0.001, 120]], [[36.469, 53.792, 70.335, 118.779], 3, 0.001]], ['limit blocks tip joint', [[[1, 1], [0, 0], [0, -1.8], 2, 0.001, 30]], [[-30.0, -30.0], 2, 1.4333]], ['zero iterations', [[[1, 1], [0, 0], [0, 2], 0, 0.001, 170]], [[0.0, 0.0], 0, 2.8284]], ['target crossing pi', [[[1, 1, 1], [170, 0, 0], [-2, -0.5], 2, 0.001, 180]], [[168.982, -6.514, 102.831], 2, 0.0176]], ['loose tolerance', [[[1, 1, 1], [5, 5, 5], [2.9, 0.4], 3, 0.2, 170]], [[5.0, 5.0, 5.0], 0, 0.1285]], ['negative limits start', [[[1.5, 1], [-60, -60], [1, 1.5], 2, 0.001, 90]], [[90.0, -90.0], 1, 0.0]]], [['three links one sweep', [[[1, 1, 1], [0, 0, 0], [1.5, 1.5], 1, 0.001, 170]], [[5.996, 17.348, 108.435], 1, 0.3586]], ['three links several sweeps', [[[1, 1, 1], [10, 10, 10], [-1, 2], 4, 0.001, 170]], [[63.011, 58.047, 42.064], 4, 0.0426]], ['tight limits', [[[1, 1, 1], [0, 0, 0], [0.5, 2.5], 3, 0.001, 45]], [[36.006, 45.0, 37.294], 3, 0.0448]], ['target crossing pi', [[[1, 1, 1], [170, 0, 0], [-2, -0.5], 2, 0.001, 180]], [[168.982, -6.514, 102.831], 2, 0.0176]], ['loose tolerance', [[[1, 1, 1], [5, 5, 5], [2.9, 0.4], 3, 0.2, 170]], [[5.0, 5.0, 5.0], 0, 0.1285]], ['negative limits start', [[[1.5, 1], [-60, -60], [1, 1.5], 2, 0.001, 90]], [[90.0, -90.0], 1, 0.0]], ['tight elbow limit', [[[1, 1, 1], [0, 0, 0], [-1, 1], 3, 0.001, 60]], [[60.0, 60.0, 60.0], 3, 0.7321]]], [['three links one sweep', [[[1, 1, 1], [0, 0, 0], [1.5, 1.5], 1, 0.001, 170]], [[5.996, 17.348, 108.435], 1, 0.3586]], ['three links several sweeps', [[[1, 1, 1], [10, 10, 10], [-1, 2], 4, 0.001, 170]], [[63.011, 58.047, 42.064], 4, 0.0426]], ['target behind base', [[[1, 1], [0, 0], [-1.5, -0.2], 3, 0.001, 175]], [[-121.779, -101.253], 3, 0.2445]], ['tight limits', [[[1, 1, 1], [0, 0, 0], [0.5, 2.5], 3, 0.001, 45]], [[36.006, 45.0, 37.294], 3, 0.0448]], ['already converged', [[[1, 1], [0, 90], [1, 1], 5, 0.001, 170]], [[0.0, 90.0], 0, 0.0]], ['converges early', [[[2, 1], [0, 0], [2, 1], 10, 0.01, 170]], [[0.0, 90.0], 1, 0.0]], ['tight elbow limit', [[[1, 1, 1], [0, 0, 0], [-1, 1], 3, 0.001, 60]], [[60.0, 60.0, 60.0], 3, 0.7321]]]]\nfor label, args, expected in fixtures[N-1]:\n    check(label, solve(*args), expected)\nprint(json.dumps({\"observations\": observations, \"passed\": all(x[\"passed\"] for x in observations)}, ensure_ascii=False))\nraise SystemExit(0 if all(x[\"passed\"] for x in observations) else 1)\n"},"fixed":{"sha256":"0a7640291e8602260c561571beb60d1a44bc7196e779df137aaf67c9fc5f0fbd","source":"\"\"\"Failure Map reference implementation. Python standard library only.\"\"\"\nimport json\nimport math\nN = 1\nobservations = []\ndef solve(x):\n    ls,qs,t,iters,tol,lim=x\n    qs=[math.radians(q) for q in qs]\n    lim=math.radians(lim)\n    def fk(qs):\n        pts=[(0.0,0.0)]\n        a=0.0\n        for l,q in zip(ls,qs):\n            a+=q\n            px,py=pts[-1]\n            pts.append((px+l*math.cos(a),py+l*math.sin(a)))\n        return pts\n    used=0\n    for it in range(iters):\n        pts=fk(qs)\n        ex,ey=pts[-1]\n        if math.hypot(t[0]-ex,t[1]-ey)<=tol: break\n        used+=1\n        for j in range(len(ls)-1,-1,-1):\n            pts=fk(qs)\n            jx,jy=pts[j]\n            ex,ey=pts[-1]\n            ve=(ex-jx,ey-jy)\n            vt=(t[0]-jx,t[1]-jy)\n            ang=math.atan2(ve[0]*vt[1]-ve[1]*vt[0],ve[0]*vt[0]+ve[1]*vt[1])\n            qs[j]=max(-lim,min(lim,qs[j]+ang))\n    pts=fk(qs)\n    err=math.hypot(t[0]-pts[-1][0],t[1]-pts[-1][1])\n    return [[round(math.degrees(q),3) for q in qs],used,round(err,4)]\ndef check(label, actual, expected):\n    observations.append({\"check\": label, \"actual\": actual, \"expected\": expected, \"passed\": actual == expected})\nfixtures = [[['three links one sweep', [[[1, 1, 1], [0, 0, 0], [1.5, 1.5], 1, 0.001, 170]], [[5.996, 17.348, 108.435], 1, 0.3586]], ['three links several sweeps', [[[1, 1, 1], [10, 10, 10], [-1, 2], 4, 0.001, 170]], [[63.011, 58.047, 42.064], 4, 0.0426]], ['target behind base', [[[1, 1], [0, 0], [-1.5, -0.2], 3, 0.001, 175]], [[-121.779, -101.253], 3, 0.2445]], ['tight limits', [[[1, 1, 1], [0, 0, 0], [0.5, 2.5], 3, 0.001, 45]], [[36.006, 45.0, 37.294], 3, 0.0448]], ['already converged', [[[1, 1], [0, 90], [1, 1], 5, 0.001, 170]], [[0.0, 90.0], 0, 0.0]], ['converges early', [[[2, 1], [0, 0], [2, 1], 10, 0.01, 170]], [[0.0, 90.0], 1, 0.0]], ['unreachable stretch', [[[1, 1], [30, 30], [4, 0], 3, 0.001, 170]], [[8.551, -17.103], 3, 2.0222]]], [['three links several sweeps', [[[1, 1, 1], [10, 10, 10], [-1, 2], 4, 0.001, 170]], [[63.011, 58.047, 42.064], 4, 0.0426]], ['already converged', [[[1, 1], [0, 90], [1, 1], 5, 0.001, 170]], [[0.0, 90.0], 0, 0.0]], ['converges early', [[[2, 1], [0, 0], [2, 1], 10, 0.01, 170]], [[0.0, 90.0], 1, 0.0]], ['unreachable stretch', [[[1, 1], [30, 30], [4, 0], 3, 0.001, 170]], [[8.551, -17.103], 3, 2.0222]], ['four links curl', [[[1, 0.8, 0.6, 0.4], [20, 20, 20, 20], [0.3, 1.2], 3, 0.001, 120]], [[36.469, 53.792, 70.335, 118.779], 3, 0.001]], ['limit blocks tip joint', [[[1, 1], [0, 0], [0, -1.8], 2, 0.001, 30]], [[-30.0, -30.0], 2, 1.4333]], ['zero iterations', [[[1, 1], [0, 0], [0, 2], 0, 0.001, 170]], [[0.0, 0.0], 0, 2.8284]]], [['target behind base', [[[1, 1], [0, 0], [-1.5, -0.2], 3, 0.001, 175]], [[-121.779, -101.253], 3, 0.2445]], ['four links curl', [[[1, 0.8, 0.6, 0.4], [20, 20, 20, 20], [0.3, 1.2], 3, 0.001, 120]], [[36.469, 53.792, 70.335, 118.779], 3, 0.001]], ['limit blocks tip joint', [[[1, 1], [0, 0], [0, -1.8], 2, 0.001, 30]], [[-30.0, -30.0], 2, 1.4333]], ['zero iterations', [[[1, 1], [0, 0], [0, 2], 0, 0.001, 170]], [[0.0, 0.0], 0, 2.8284]], ['target crossing pi', [[[1, 1, 1], [170, 0, 0], [-2, -0.5], 2, 0.001, 180]], [[168.982, -6.514, 102.831], 2, 0.0176]], ['loose tolerance', [[[1, 1, 1], [5, 5, 5], [2.9, 0.4], 3, 0.2, 170]], [[5.0, 5.0, 5.0], 0, 0.1285]], ['negative limits start', [[[1.5, 1], [-60, -60], [1, 1.5], 2, 0.001, 90]], [[90.0, -90.0], 1, 0.0]]], [['three links one sweep', [[[1, 1, 1], [0, 0, 0], [1.5, 1.5], 1, 0.001, 170]], [[5.996, 17.348, 108.435], 1, 0.3586]], ['three links several sweeps', [[[1, 1, 1], [10, 10, 10], [-1, 2], 4, 0.001, 170]], [[63.011, 58.047, 42.064], 4, 0.0426]], ['tight limits', [[[1, 1, 1], [0, 0, 0], [0.5, 2.5], 3, 0.001, 45]], [[36.006, 45.0, 37.294], 3, 0.0448]], ['target crossing pi', [[[1, 1, 1], [170, 0, 0], [-2, -0.5], 2, 0.001, 180]], [[168.982, -6.514, 102.831], 2, 0.0176]], ['loose tolerance', [[[1, 1, 1], [5, 5, 5], [2.9, 0.4], 3, 0.2, 170]], [[5.0, 5.0, 5.0], 0, 0.1285]], ['negative limits start', [[[1.5, 1], [-60, -60], [1, 1.5], 2, 0.001, 90]], [[90.0, -90.0], 1, 0.0]], ['tight elbow limit', [[[1, 1, 1], [0, 0, 0], [-1, 1], 3, 0.001, 60]], [[60.0, 60.0, 60.0], 3, 0.7321]]], [['three links one sweep', [[[1, 1, 1], [0, 0, 0], [1.5, 1.5], 1, 0.001, 170]], [[5.996, 17.348, 108.435], 1, 0.3586]], ['three links several sweeps', [[[1, 1, 1], [10, 10, 10], [-1, 2], 4, 0.001, 170]], [[63.011, 58.047, 42.064], 4, 0.0426]], ['target behind base', [[[1, 1], [0, 0], [-1.5, -0.2], 3, 0.001, 175]], [[-121.779, -101.253], 3, 0.2445]], ['tight limits', [[[1, 1, 1], [0, 0, 0], [0.5, 2.5], 3, 0.001, 45]], [[36.006, 45.0, 37.294], 3, 0.0448]], ['already converged', [[[1, 1], [0, 90], [1, 1], 5, 0.001, 170]], [[0.0, 90.0], 0, 0.0]], ['converges early', [[[2, 1], [0, 0], [2, 1], 10, 0.01, 170]], [[0.0, 90.0], 1, 0.0]], ['tight elbow limit', [[[1, 1, 1], [0, 0, 0], [-1, 1], 3, 0.001, 60]], [[60.0, 60.0, 60.0], 3, 0.7321]]]]\nfor label, args, expected in fixtures[N-1]:\n    check(label, solve(*args), expected)\nprint(json.dumps({\"observations\": observations, \"passed\": all(x[\"passed\"] for x in observations)}, ensure_ascii=False))\nraise SystemExit(0 if all(x[\"passed\"] for x in observations) else 1)\n"}},"limitations":"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.","method":"Deterministic executable model with adversarial boundary fixtures.","provenance":{"created_by":"Failure Map","dependencies":"Python standard library","family":"w2-inverse_kinematics_solvers-cyclic-coordinate-descent-signed-rotation-angle","generated_at":"2026-09-29T14:51:03.622570+00:00","license":"CC0-1.0","python":"3.12.14","seed":1,"split":"open-access"},"relevance":"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.","repair":"Use atan2(ve x vt, ve . vt) for the signed rotation from tip vector to target vector.","root_cause":"The cross product is taken as target x tip instead of tip x target.","sha256":"c566c9a52e9c493e16a3f7a868c7e36973ec291476399ab714a626ca64212048","title":"CCD rotation angle has the wrong handedness · case 01","variant":1,"variant_policy":"Five numbered records share a model and may reuse boundary fixtures.","verification":{"attempt":{"elapsed_ms":40.818,"exit_code":1,"observations":[{"actual":[[5.996,17.348,108.435],1,0.3586],"check":"three links one sweep","expected":[[5.996,17.348,108.435],1,0.3586],"passed":true},{"actual":[[126.565,170.0,170.0],4,1.2665],"check":"three links several sweeps","expected":[[63.011,58.047,42.064],4,0.0426],"passed":false},{"actual":[[-121.779,-101.253],3,0.2445],"check":"target behind base","expected":[[-121.779,-101.253],3,0.2445],"passed":true},{"actual":[[36.006,45.0,37.294],3,0.0448],"check":"tight limits","expected":[[36.006,45.0,37.294],3,0.0448],"passed":true},{"actual":[[0.0,90.0],0,0.0],"check":"already converged","expected":[[0.0,90.0],0,0.0],"passed":true},{"actual":[[0.0,90.0],1,0.0],"check":"converges early","expected":[[0.0,90.0],1,0.0],"passed":true},{"actual":[[8.551,-17.103],3,2.0222],"check":"unreachable stretch","expected":[[8.551,-17.103],3,2.0222],"passed":true}],"passed":false,"stderr":"","stdout":"{\"observations\": [{\"check\": \"three links one sweep\", \"actual\": [[5.996, 17.348, 108.435], 1, 0.3586], \"expected\": [[5.996, 17.348, 108.435], 1, 0.3586], \"passed\": true}, {\"check\": \"three links several sweeps\", \"actual\": [[126.565, 170.0, 170.0], 4, 1.2665], \"expected\": [[63.011, 58.047, 42.064], 4, 0.0426], \"passed\": false}, {\"check\": \"target behind base\", \"actual\": [[-121.779, -101.253], 3, 0.2445], \"expected\": [[-121.779, -101.253], 3, 0.2445], \"passed\": true}, {\"check\": \"tight limits\", \"actual\": [[36.006, 45.0, 37.294], 3, 0.0448], \"expected\": [[36.006, 45.0, 37.294], 3, 0.0448], \"passed\": true}, {\"check\": \"already converged\", \"actual\": [[0.0, 90.0], 0, 0.0], \"expected\": [[0.0, 90.0], 0, 0.0], \"passed\": true}, {\"check\": \"converges early\", \"actual\": [[0.0, 90.0], 1, 0.0], \"expected\": [[0.0, 90.0], 1, 0.0], \"passed\": true}, {\"check\": \"unreachable stretch\", \"actual\": [[8.551, -17.103], 3, 2.0222], \"expected\": [[8.551, -17.103], 3, 2.0222], \"passed\": true}], \"passed\": false}\n"},"broken":{"elapsed_ms":43.0,"exit_code":1,"observations":[{"actual":[[135.0,-125.783,-108.435],1,2.1281],"check":"three links one sweep","expected":[[5.996,17.348,108.435],1,0.3586],"passed":false},{"actual":[[-63.939,-170.0,-148.49],4,3.0428],"check":"three links several sweeps","expected":[[63.011,58.047,42.064],4,0.0426],"passed":false},{"actual":[[-127.595,30.054],3,1.756],"check":"target behind base","expected":[[-121.779,-101.253],3,0.2445],"passed":false},{"actual":[[-45.0,45.0,-45.0],3,4.3572],"check":"tight limits","expected":[[36.006,45.0,37.294],3,0.0448],"passed":false},{"actual":[[0.0,90.0],0,0.0],"check":"already converged","expected":[[0.0,90.0],0,0.0],"passed":true},{"actual":[[-121.924,66.279],10,4.3161],"check":"converges early","expected":[[0.0,90.0],1,0.0],"passed":false},{"actual":[[25.927,91.853],3,3.8038],"check":"unreachable stretch","expected":[[8.551,-17.103],3,2.0222],"passed":false}],"passed":false,"stderr":"","stdout":"{\"observations\": [{\"check\": \"three links one sweep\", \"actual\": [[135.0, -125.783, -108.435], 1, 2.1281], \"expected\": [[5.996, 17.348, 108.435], 1, 0.3586], \"passed\": false}, {\"check\": \"three links several sweeps\", \"actual\": [[-63.939, -170.0, -148.49], 4, 3.0428], \"expected\": [[63.011, 58.047, 42.064], 4, 0.0426], \"passed\": false}, {\"check\": \"target behind base\", \"actual\": [[-127.595, 30.054], 3, 1.756], \"expected\": [[-121.779, -101.253], 3, 0.2445], \"passed\": false}, {\"check\": \"tight limits\", \"actual\": [[-45.0, 45.0, -45.0], 3, 4.3572], \"expected\": [[36.006, 45.0, 37.294], 3, 0.0448], \"passed\": false}, {\"check\": \"already converged\", \"actual\": [[0.0, 90.0], 0, 0.0], \"expected\": [[0.0, 90.0], 0, 0.0], \"passed\": true}, {\"check\": \"converges early\", \"actual\": [[-121.924, 66.279], 10, 4.3161], \"expected\": [[0.0, 90.0], 1, 0.0], \"passed\": false}, {\"check\": \"unreachable stretch\", \"actual\": [[25.927, 91.853], 3, 3.8038], \"expected\": [[8.551, -17.103], 3, 2.0222], \"passed\": false}], \"passed\": false}\n"},"fixed":{"elapsed_ms":40.064,"exit_code":0,"observations":[{"actual":[[5.996,17.348,108.435],1,0.3586],"check":"three links one sweep","expected":[[5.996,17.348,108.435],1,0.3586],"passed":true},{"actual":[[63.011,58.047,42.064],4,0.0426],"check":"three links several sweeps","expected":[[63.011,58.047,42.064],4,0.0426],"passed":true},{"actual":[[-121.779,-101.253],3,0.2445],"check":"target behind base","expected":[[-121.779,-101.253],3,0.2445],"passed":true},{"actual":[[36.006,45.0,37.294],3,0.0448],"check":"tight limits","expected":[[36.006,45.0,37.294],3,0.0448],"passed":true},{"actual":[[0.0,90.0],0,0.0],"check":"already converged","expected":[[0.0,90.0],0,0.0],"passed":true},{"actual":[[0.0,90.0],1,0.0],"check":"converges early","expected":[[0.0,90.0],1,0.0],"passed":true},{"actual":[[8.551,-17.103],3,2.0222],"check":"unreachable stretch","expected":[[8.551,-17.103],3,2.0222],"passed":true}],"passed":true,"stderr":"","stdout":"{\"observations\": [{\"check\": \"three links one sweep\", \"actual\": [[5.996, 17.348, 108.435], 1, 0.3586], \"expected\": [[5.996, 17.348, 108.435], 1, 0.3586], \"passed\": true}, {\"check\": \"three links several sweeps\", \"actual\": [[63.011, 58.047, 42.064], 4, 0.0426], \"expected\": [[63.011, 58.047, 42.064], 4, 0.0426], \"passed\": true}, {\"check\": \"target behind base\", \"actual\": [[-121.779, -101.253], 3, 0.2445], \"expected\": [[-121.779, -101.253], 3, 0.2445], \"passed\": true}, {\"check\": \"tight limits\", \"actual\": [[36.006, 45.0, 37.294], 3, 0.0448], \"expected\": [[36.006, 45.0, 37.294], 3, 0.0448], \"passed\": true}, {\"check\": \"already converged\", \"actual\": [[0.0, 90.0], 0, 0.0], \"expected\": [[0.0, 90.0], 0, 0.0], \"passed\": true}, {\"check\": \"converges early\", \"actual\": [[0.0, 90.0], 1, 0.0], \"expected\": [[0.0, 90.0], 1, 0.0], \"passed\": true}, {\"check\": \"unreachable stretch\", \"actual\": [[8.551, -17.103], 3, 2.0222], \"expected\": [[8.551, -17.103], 3, 2.0222], \"passed\": true}], \"passed\": true}\n"}},"verified":true,"visibility":"public"}