FAILURE MAP
← Case archive

FA-88166 / Inverse kinematics solvers / Open access

Hexapod femur limits in degrees are compared with radians · case 01

Pitch limits almost never trigger, allowing poses beyond the servo range.

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

ROOT CAUSE

The limit test uses the radian pitch a1+a2 against degree bounds.

VERIFIED REPAIR

Compare the degree pitch with fmin and fmax.

Unsuccessful approach: Joining the comparisons with "and" can never be true.

Case contract

Input [coxa,femur,tibia,x,y,z,fmin,fmax]: foot target in the leg root frame (z up). Coxa yaw is atan2(y,x); the femur/tibia pair works in the vertical plane at horizontal distance hypot(x,y)-coxa. With D the distance from the femur pivot to the foot, D>femur+tibia or D<|femur-tibia| or D==0 is "unreachable". Femur pitch is atan2(z,h) plus the triangle angle at the femur pivot (knee up); tibia angle is the interior knee angle minus 180 (0 = straight). Femur pitch outside [fmin,fmax] returns "limit". Degrees rounded to 4.

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):
    cx,fe,ti,px,py,pz,fmin,fmax=x
    ca=math.atan2(py,px)
    h=math.hypot(px,py)-cx
    D=math.hypot(h,pz)
    if D>fe+ti or D<abs(fe-ti) or D==0: return 'unreachable'
    a1=math.atan2(pz,h)
    a2=math.acos(max(-1.0,min(1.0,(fe*fe+D*D-ti*ti)/(2*fe*D))))
    fa=math.degrees(a1+a2)
    ta=math.degrees(math.acos(max(-1.0,min(1.0,(fe*fe+ti*ti-D*D)/(2*fe*ti)))))-180.0
    if a1+a2<fmin or a1+a2>fmax: return 'limit'
    return [round(math.degrees(ca),4),round(fa,4),round(ta,4)]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['nominal stance', [[3, 6, 8, 10, 2, -6, -90, 90]], [11.3099, 18.0529, -97.294]], ['foot straight out', [[3, 6, 8, 12, 0, -3, -90, 90]], [0.0, 38.5667, -95.9792]], ['foot to the side', [[2, 5, 7, 1, 9, -4, -90, 90]], [83.6598, 29.4436, -96.745]], ['foot behind', [[2, 5, 7, -8, -3, -5, -90, 90]], [-159.444, 20.4684, -95.0617]], ['foot out of reach', [[3, 6, 8, 18, 0, -2, -90, 90]], 'unreachable'], ['foot inside coxa reach', [[3, 6, 8, 16, 0, 0, -90, 90]], [0.0, 25.3317, -44.0486]], ['high foot', [[3, 6, 8, 9, 3, 4, -90, 90]], 'limit']], [['foot behind', [[2, 5, 7, -8, -3, -5, -90, 90]], [-159.444, 20.4684, -95.0617]], ['foot out of reach', [[3, 6, 8, 18, 0, -2, -90, 90]], 'unreachable'], ['foot inside coxa reach', [[3, 6, 8, 16, 0, 0, -90, 90]], [0.0, 25.3317, -44.0486]], ['high foot', [[3, 6, 8, 9, 3, 4, -90, 90]], 'limit'], ['femur limit exceeded', [[3, 6, 8, 5, 0, 6, -90, 30]], 'limit'], ['deep foot', [[3, 6, 8, 5, 5, -9, -90, 90]], [45.0, -11.6013, -91.4483]], ['tight femur limits', [[3, 6, 8, 10, 2, -6, -10, 10]], 'limit']], [['high foot', [[3, 6, 8, 9, 3, 4, -90, 90]], 'limit'], ['femur limit exceeded', [[3, 6, 8, 5, 0, 6, -90, 30]], 'limit'], ['deep foot', [[3, 6, 8, 5, 5, -9, -90, 90]], [45.0, -11.6013, -91.4483]], ['tight femur limits', [[3, 6, 8, 10, 2, -6, -10, 10]], 'limit'], ['long tibia near', [[2, 4, 9, 6, 1, -8, -90, 90]], [9.4623, 14.4416, -103.1099]], ['diagonal foot', [[2.5, 5, 6, -6, 6, -3, -90, 90]], [135.0, 33.0338, -105.6409]], ['hole too close', [[3, 6, 12, 4, 0, -1, -90, 90]], 'unreachable']], [['nominal stance', [[3, 6, 8, 10, 2, -6, -90, 90]], [11.3099, 18.0529, -97.294]], ['high foot', [[3, 6, 8, 9, 3, 4, -90, 90]], 'limit'], ['long tibia near', [[2, 4, 9, 6, 1, -8, -90, 90]], [9.4623, 14.4416, -103.1099]], ['diagonal foot', [[2.5, 5, 6, -6, 6, -3, -90, 90]], [135.0, 33.0338, -105.6409]], ['hole too close', [[3, 6, 12, 4, 0, -1, -90, 90]], 'unreachable'], ['foot far below', [[3, 6, 8, 15, 0, -10, -90, 90]], 'unreachable'], ['femur at lower bound', [[3, 6, 8, 11, 0, -8, -60, 90]], [0.0, -2.44, -73.0422]]], [['nominal stance', [[3, 6, 8, 10, 2, -6, -90, 90]], [11.3099, 18.0529, -97.294]], ['foot straight out', [[3, 6, 8, 12, 0, -3, -90, 90]], [0.0, 38.5667, -95.9792]], ['foot to the side', [[2, 5, 7, 1, 9, -4, -90, 90]], [83.6598, 29.4436, -96.745]], ['foot behind', [[2, 5, 7, -8, -3, -5, -90, 90]], [-159.444, 20.4684, -95.0617]], ['femur limit exceeded', [[3, 6, 8, 5, 0, 6, -90, 30]], 'limit'], ['foot far below', [[3, 6, 8, 15, 0, -10, -90, 90]], 'unreachable'], ['femur at lower bound', [[3, 6, 8, 11, 0, -8, -60, 90]], [0.0, -2.44, -73.0422]]]]
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
nominal stance[11.3099, 18.0529, -97.294][11.3099, 18.0529, -97.294]Passed
foot straight out[0.0, 38.5667, -95.9792][0.0, 38.5667, -95.9792]Passed
foot to the side[83.6598, 29.4436, -96.745][83.6598, 29.4436, -96.745]Passed
foot behind[-159.444, 20.4684, -95.0617][-159.444, 20.4684, -95.0617]Passed
foot out of reachunreachableunreachablePassed
foot inside coxa reach[0.0, 25.3317, -44.0486][0.0, 25.3317, -44.0486]Passed
high foot[18.4349, 102.457, -115.8921]limitFailed

SHA-256 / 7f3bd22ab75957db3173544a1c650196d81901d0eaed46defb4a42cc3ec4178d

2 / The unsuccessful fix

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(x):
    cx,fe,ti,px,py,pz,fmin,fmax=x
    ca=math.atan2(py,px)
    h=math.hypot(px,py)-cx
    D=math.hypot(h,pz)
    if D>fe+ti or D<abs(fe-ti) or D==0: return 'unreachable'
    a1=math.atan2(pz,h)
    a2=math.acos(max(-1.0,min(1.0,(fe*fe+D*D-ti*ti)/(2*fe*D))))
    fa=math.degrees(a1+a2)
    ta=math.degrees(math.acos(max(-1.0,min(1.0,(fe*fe+ti*ti-D*D)/(2*fe*ti)))))-180.0
    if fa<fmin and fa>fmax: return 'limit'
    return [round(math.degrees(ca),4),round(fa,4),round(ta,4)]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['nominal stance', [[3, 6, 8, 10, 2, -6, -90, 90]], [11.3099, 18.0529, -97.294]], ['foot straight out', [[3, 6, 8, 12, 0, -3, -90, 90]], [0.0, 38.5667, -95.9792]], ['foot to the side', [[2, 5, 7, 1, 9, -4, -90, 90]], [83.6598, 29.4436, -96.745]], ['foot behind', [[2, 5, 7, -8, -3, -5, -90, 90]], [-159.444, 20.4684, -95.0617]], ['foot out of reach', [[3, 6, 8, 18, 0, -2, -90, 90]], 'unreachable'], ['foot inside coxa reach', [[3, 6, 8, 16, 0, 0, -90, 90]], [0.0, 25.3317, -44.0486]], ['high foot', [[3, 6, 8, 9, 3, 4, -90, 90]], 'limit']], [['foot behind', [[2, 5, 7, -8, -3, -5, -90, 90]], [-159.444, 20.4684, -95.0617]], ['foot out of reach', [[3, 6, 8, 18, 0, -2, -90, 90]], 'unreachable'], ['foot inside coxa reach', [[3, 6, 8, 16, 0, 0, -90, 90]], [0.0, 25.3317, -44.0486]], ['high foot', [[3, 6, 8, 9, 3, 4, -90, 90]], 'limit'], ['femur limit exceeded', [[3, 6, 8, 5, 0, 6, -90, 30]], 'limit'], ['deep foot', [[3, 6, 8, 5, 5, -9, -90, 90]], [45.0, -11.6013, -91.4483]], ['tight femur limits', [[3, 6, 8, 10, 2, -6, -10, 10]], 'limit']], [['high foot', [[3, 6, 8, 9, 3, 4, -90, 90]], 'limit'], ['femur limit exceeded', [[3, 6, 8, 5, 0, 6, -90, 30]], 'limit'], ['deep foot', [[3, 6, 8, 5, 5, -9, -90, 90]], [45.0, -11.6013, -91.4483]], ['tight femur limits', [[3, 6, 8, 10, 2, -6, -10, 10]], 'limit'], ['long tibia near', [[2, 4, 9, 6, 1, -8, -90, 90]], [9.4623, 14.4416, -103.1099]], ['diagonal foot', [[2.5, 5, 6, -6, 6, -3, -90, 90]], [135.0, 33.0338, -105.6409]], ['hole too close', [[3, 6, 12, 4, 0, -1, -90, 90]], 'unreachable']], [['nominal stance', [[3, 6, 8, 10, 2, -6, -90, 90]], [11.3099, 18.0529, -97.294]], ['high foot', [[3, 6, 8, 9, 3, 4, -90, 90]], 'limit'], ['long tibia near', [[2, 4, 9, 6, 1, -8, -90, 90]], [9.4623, 14.4416, -103.1099]], ['diagonal foot', [[2.5, 5, 6, -6, 6, -3, -90, 90]], [135.0, 33.0338, -105.6409]], ['hole too close', [[3, 6, 12, 4, 0, -1, -90, 90]], 'unreachable'], ['foot far below', [[3, 6, 8, 15, 0, -10, -90, 90]], 'unreachable'], ['femur at lower bound', [[3, 6, 8, 11, 0, -8, -60, 90]], [0.0, -2.44, -73.0422]]], [['nominal stance', [[3, 6, 8, 10, 2, -6, -90, 90]], [11.3099, 18.0529, -97.294]], ['foot straight out', [[3, 6, 8, 12, 0, -3, -90, 90]], [0.0, 38.5667, -95.9792]], ['foot to the side', [[2, 5, 7, 1, 9, -4, -90, 90]], [83.6598, 29.4436, -96.745]], ['foot behind', [[2, 5, 7, -8, -3, -5, -90, 90]], [-159.444, 20.4684, -95.0617]], ['femur limit exceeded', [[3, 6, 8, 5, 0, 6, -90, 30]], 'limit'], ['foot far below', [[3, 6, 8, 15, 0, -10, -90, 90]], 'unreachable'], ['femur at lower bound', [[3, 6, 8, 11, 0, -8, -60, 90]], [0.0, -2.44, -73.0422]]]]
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
nominal stance[11.3099, 18.0529, -97.294][11.3099, 18.0529, -97.294]Passed
foot straight out[0.0, 38.5667, -95.9792][0.0, 38.5667, -95.9792]Passed
foot to the side[83.6598, 29.4436, -96.745][83.6598, 29.4436, -96.745]Passed
foot behind[-159.444, 20.4684, -95.0617][-159.444, 20.4684, -95.0617]Passed
foot out of reachunreachableunreachablePassed
foot inside coxa reach[0.0, 25.3317, -44.0486][0.0, 25.3317, -44.0486]Passed
high foot[18.4349, 102.457, -115.8921]limitFailed

SHA-256 / 585dee5ee318ef42a0149e2d641fc256a5cf400c4a9c3fd991560aa4121295a6

3 / The verified repair

Exit 0
"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(x):
    cx,fe,ti,px,py,pz,fmin,fmax=x
    ca=math.atan2(py,px)
    h=math.hypot(px,py)-cx
    D=math.hypot(h,pz)
    if D>fe+ti or D<abs(fe-ti) or D==0: return 'unreachable'
    a1=math.atan2(pz,h)
    a2=math.acos(max(-1.0,min(1.0,(fe*fe+D*D-ti*ti)/(2*fe*D))))
    fa=math.degrees(a1+a2)
    ta=math.degrees(math.acos(max(-1.0,min(1.0,(fe*fe+ti*ti-D*D)/(2*fe*ti)))))-180.0
    if fa<fmin or fa>fmax: return 'limit'
    return [round(math.degrees(ca),4),round(fa,4),round(ta,4)]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['nominal stance', [[3, 6, 8, 10, 2, -6, -90, 90]], [11.3099, 18.0529, -97.294]], ['foot straight out', [[3, 6, 8, 12, 0, -3, -90, 90]], [0.0, 38.5667, -95.9792]], ['foot to the side', [[2, 5, 7, 1, 9, -4, -90, 90]], [83.6598, 29.4436, -96.745]], ['foot behind', [[2, 5, 7, -8, -3, -5, -90, 90]], [-159.444, 20.4684, -95.0617]], ['foot out of reach', [[3, 6, 8, 18, 0, -2, -90, 90]], 'unreachable'], ['foot inside coxa reach', [[3, 6, 8, 16, 0, 0, -90, 90]], [0.0, 25.3317, -44.0486]], ['high foot', [[3, 6, 8, 9, 3, 4, -90, 90]], 'limit']], [['foot behind', [[2, 5, 7, -8, -3, -5, -90, 90]], [-159.444, 20.4684, -95.0617]], ['foot out of reach', [[3, 6, 8, 18, 0, -2, -90, 90]], 'unreachable'], ['foot inside coxa reach', [[3, 6, 8, 16, 0, 0, -90, 90]], [0.0, 25.3317, -44.0486]], ['high foot', [[3, 6, 8, 9, 3, 4, -90, 90]], 'limit'], ['femur limit exceeded', [[3, 6, 8, 5, 0, 6, -90, 30]], 'limit'], ['deep foot', [[3, 6, 8, 5, 5, -9, -90, 90]], [45.0, -11.6013, -91.4483]], ['tight femur limits', [[3, 6, 8, 10, 2, -6, -10, 10]], 'limit']], [['high foot', [[3, 6, 8, 9, 3, 4, -90, 90]], 'limit'], ['femur limit exceeded', [[3, 6, 8, 5, 0, 6, -90, 30]], 'limit'], ['deep foot', [[3, 6, 8, 5, 5, -9, -90, 90]], [45.0, -11.6013, -91.4483]], ['tight femur limits', [[3, 6, 8, 10, 2, -6, -10, 10]], 'limit'], ['long tibia near', [[2, 4, 9, 6, 1, -8, -90, 90]], [9.4623, 14.4416, -103.1099]], ['diagonal foot', [[2.5, 5, 6, -6, 6, -3, -90, 90]], [135.0, 33.0338, -105.6409]], ['hole too close', [[3, 6, 12, 4, 0, -1, -90, 90]], 'unreachable']], [['nominal stance', [[3, 6, 8, 10, 2, -6, -90, 90]], [11.3099, 18.0529, -97.294]], ['high foot', [[3, 6, 8, 9, 3, 4, -90, 90]], 'limit'], ['long tibia near', [[2, 4, 9, 6, 1, -8, -90, 90]], [9.4623, 14.4416, -103.1099]], ['diagonal foot', [[2.5, 5, 6, -6, 6, -3, -90, 90]], [135.0, 33.0338, -105.6409]], ['hole too close', [[3, 6, 12, 4, 0, -1, -90, 90]], 'unreachable'], ['foot far below', [[3, 6, 8, 15, 0, -10, -90, 90]], 'unreachable'], ['femur at lower bound', [[3, 6, 8, 11, 0, -8, -60, 90]], [0.0, -2.44, -73.0422]]], [['nominal stance', [[3, 6, 8, 10, 2, -6, -90, 90]], [11.3099, 18.0529, -97.294]], ['foot straight out', [[3, 6, 8, 12, 0, -3, -90, 90]], [0.0, 38.5667, -95.9792]], ['foot to the side', [[2, 5, 7, 1, 9, -4, -90, 90]], [83.6598, 29.4436, -96.745]], ['foot behind', [[2, 5, 7, -8, -3, -5, -90, 90]], [-159.444, 20.4684, -95.0617]], ['femur limit exceeded', [[3, 6, 8, 5, 0, 6, -90, 30]], 'limit'], ['foot far below', [[3, 6, 8, 15, 0, -10, -90, 90]], 'unreachable'], ['femur at lower bound', [[3, 6, 8, 11, 0, -8, -60, 90]], [0.0, -2.44, -73.0422]]]]
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
nominal stance[11.3099, 18.0529, -97.294][11.3099, 18.0529, -97.294]Passed
foot straight out[0.0, 38.5667, -95.9792][0.0, 38.5667, -95.9792]Passed
foot to the side[83.6598, 29.4436, -96.745][83.6598, 29.4436, -96.745]Passed
foot behind[-159.444, 20.4684, -95.0617][-159.444, 20.4684, -95.0617]Passed
foot out of reachunreachableunreachablePassed
foot inside coxa reach[0.0, 25.3317, -44.0486][0.0, 25.3317, -44.0486]Passed
high footlimitlimitPassed

SHA-256 / 4796ffe855408275d32f2c98d2708e9de4ee63a5cbce4d4ee78d9a94d16d38e4

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

Case digest / b5c8cf48b1bb3d3b149d3ec1ee60d49b90064b19e4790f987211110555841179