FA-88236 / Inverse kinematics solvers / Open access
Trajectory dropout forgets the elbow branch · case 01
After an unreachable waypoint a branch change is no longer counted as a flip.
ROOT CAUSE
The "hold" path clears the remembered branch, so the next accepted solution is treated as the first.
VERIFIED REPAIR
Leave the branch and joint state untouched on "hold".
Unsuccessful approach: Resetting the joint state to q0 on dropout makes the next choice relative to the home pose.
Case contract
Input [l1,l2,q0,targets,maxjump]: planar 2R arm with continuous joints following a list of targets from joint state q0 (degrees). For each target compute the elbow +/- solutions; unreachable targets append "hold". Choose the solution with the smallest max wrapped joint change from the previous state; if that change exceeds maxjump append "jump" and keep the state. Otherwise unwrap the solution to lie within 180 of the previous angles, append it (3 decimals) and count a flip whenever the elbow sign differs from the last accepted one. Return [outputs, flips].
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,q0,targets,maxjump=x
prev=list(q0)
out=[]
flips=0
branch=None
for px,py in targets:
c=(px*px+py*py-l1*l1-l2*l2)/(2*l1*l2)
if abs(c)>1.0:
out.append('hold')
branch=None
continue
sols=[]
for sgn in (1,-1):
s=sgn*math.sqrt(1-c*c)
t2=math.degrees(math.atan2(s,c))
t1=math.degrees(math.atan2(py,px)-math.atan2(l2*s,l1+l2*c))
sols.append([sgn,t1,t2])
def dist(sol):
return max(abs((sol[1]-prev[0]+180.0)%360.0-180.0),abs((sol[2]-prev[1]+180.0)%360.0-180.0))
best=min(sols,key=dist)
if dist(best)>maxjump:
out.append('jump')
continue
if branch is not None and best[0]!=branch: flips+=1
branch=best[0]
t1=prev[0]+((best[1]-prev[0]+180.0)%360.0-180.0)
t2=prev[1]+((best[2]-prev[1]+180.0)%360.0-180.0)
prev=[t1,t2]
out.append([round(t1,3),round(t2,3)])
return [out,flips]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['smooth arc', [[1, 1, [0, 90], [[1, 1], [0.9, 1.1], [0.8, 1.2]], 30]], [[[0.0, 90.0], [5.997, 89.427], [12.456, 87.708]], 0]], ['seam crossing', [[1, 1, [170, 20], [[-1.5, 0.3], [-1.5, -0.3], [-1.4, -0.6]], 40]], [['jump', 'jump', 'jump'], 0]], ['multi-turn start', [[1, 1, [360, 90], [[1, 1], [1.1, 0.9]], 30]], [[[360.0, 90.0], [354.576, 89.427]], 0]], ['dropout in middle', [[1, 1, [0, 90], [[1, 1], [3, 0], [0.9, 1.1]], 30]], [[[0.0, 90.0], 'hold', [5.997, 89.427]], 0]], ['dropout then branch change', [[1, 1, [0, 60], [[1.5, 0.8], [3, 3], [1.6, -0.6]], 200]], [[[-3.716, 63.577], 'hold', [-51.862, 62.613]], 0]], ['large jump rejected', [[1, 1, [0, 90], [[1, 1], [-1, -1], [1, 0.9]], 20]], [[[0.0, 90.0], 'jump', [-5.738, 95.451]], 0]], ['dropout across branch change', [[1, 1, [-93, 83], [[1.9, -0.3], [3, 3], [-1.5, -1.3]], 120]], [[[-24.867, 31.788], 'hold', [-132.051, -14.07]], 1]]], [['dropout in middle', [[1, 1, [0, 90], [[1, 1], [3, 0], [0.9, 1.1]], 30]], [[[0.0, 90.0], 'hold', [5.997, 89.427]], 0]], ['dropout then branch change', [[1, 1, [0, 60], [[1.5, 0.8], [3, 3], [1.6, -0.6]], 200]], [[[-3.716, 63.577], 'hold', [-51.862, 62.613]], 0]], ['large jump rejected', [[1, 1, [0, 90], [[1, 1], [-1, -1], [1, 0.9]], 20]], [[[0.0, 90.0], 'jump', [-5.738, 95.451]], 0]], ['jump exactly at limit', [[2, 1, [0, 0], [[3, 0], [2, 1]], 90]], [[[0.0, 0.0], [0.0, 90.0]], 0]], ['flip through straight', [[1, 1, [0, 30], [[1.9, 0.3], [2, 0], [1.9, -0.3], [1.8, -0.6]], 60]], [[[-6.922, 31.788], [0.0, 0.0], [-24.867, 31.788], [-36.87, 36.87]], 0]], ['start near down branch', [[1, 1, [60, -60], [[1.2, 1.0], [1.3, 0.9]], 50]], [[[78.451, -77.291], [72.456, -75.522]], 0]], ['down to up flip', [[1, 1, [69, -23], [[0.3, -0.8], [1.9, -0.9], [0.1, 0.9]], 164]], [[[-4.734, -129.42], 'hold', [20.581, -233.843]], 1]]], [['jump exactly at limit', [[2, 1, [0, 0], [[3, 0], [2, 1]], 90]], [[[0.0, 0.0], [0.0, 90.0]], 0]], ['flip through straight', [[1, 1, [0, 30], [[1.9, 0.3], [2, 0], [1.9, -0.3], [1.8, -0.6]], 60]], [[[-6.922, 31.788], [0.0, 0.0], [-24.867, 31.788], [-36.87, 36.87]], 0]], ['start near down branch', [[1, 1, [60, -60], [[1.2, 1.0], [1.3, 0.9]], 50]], [[[78.451, -77.291], [72.456, -75.522]], 0]], ['big jump budget', [[1, 1.5, [0, 0], [[-1, 1.5], [1.5, -1], [0.5, 2]], 360]], [[[67.38, 90.0], [-90.0, 90.0], [32.65, 70.529]], 0]], ['negative seam elbow', [[1, 1, [-170, -20], [[-1.5, -0.3], [-1.5, 0.3], [-1.3, 0.8]], 45]], [['jump', 'jump', 'jump'], 0]], ['all unreachable', [[1, 1, [0, 0], [[3, 3], [0, 5]], 30]], [['hold', 'hold'], 0]], ['dropout near base', [[1, 1, [146, 142], [[-0.1, -1.0], [3, 3], [-0.3, 0.2]], 120]], [[[204.455, 119.67], 'hold', [225.924, 200.772]], 1]]], [['big jump budget', [[1, 1.5, [0, 0], [[-1, 1.5], [1.5, -1], [0.5, 2]], 360]], [[[67.38, 90.0], [-90.0, 90.0], [32.65, 70.529]], 0]], ['negative seam elbow', [[1, 1, [-170, -20], [[-1.5, -0.3], [-1.5, 0.3], [-1.3, 0.8]], 45]], [['jump', 'jump', 'jump'], 0]], ['all unreachable', [[1, 1, [0, 0], [[3, 3], [0, 5]], 30]], [['hold', 'hold'], 0]], ['jump then recover', [[1, 1, [0, 120], [[0.2, 1.0], [1.8, 0.2], [0.3, 1.0]], 60]], [[[19.347, 118.685], 'jump', [14.768, 117.065]], 0]], ['second target flips', [[1.5, 1, [0, 45], [[2, 1.2], [2.2, -0.4]], 150]], [[[13.925, 43.114], [-31.604, 54.315]], 0]], ['dropout across branch change', [[1, 1, [-93, 83], [[1.9, -0.3], [3, 3], [-1.5, -1.3]], 120]], [[[-24.867, 31.788], 'hold', [-132.051, -14.07]], 1]], ['down to up flip', [[1, 1, [69, -23], [[0.3, -0.8], [1.9, -0.9], [0.1, 0.9]], 164]], [[[-4.734, -129.42], 'hold', [20.581, -233.843]], 1]]], [['jump then recover', [[1, 1, [0, 120], [[0.2, 1.0], [1.8, 0.2], [0.3, 1.0]], 60]], [[[19.347, 118.685], 'jump', [14.768, 117.065]], 0]], ['second target flips', [[1.5, 1, [0, 45], [[2, 1.2], [2.2, -0.4]], 150]], [[[13.925, 43.114], [-31.604, 54.315]], 0]], ['dropout across branch change', [[1, 1, [-93, 83], [[1.9, -0.3], [3, 3], [-1.5, -1.3]], 120]], [[[-24.867, 31.788], 'hold', [-132.051, -14.07]], 1]], ['down to up flip', [[1, 1, [69, -23], [[0.3, -0.8], [1.9, -0.9], [0.1, 0.9]], 164]], [[[-4.734, -129.42], 'hold', [20.581, -233.843]], 1]], ['wide sweep flips', [[1, 1, [-5, -54], [[0.1, -1.7], [0.7, 1.5], [-1.2, 0.5]], 154]], [[[-55.005, -63.256], [30.841, 68.284], [107.922, 98.917]], 1]], ['rejected then flip', [[1, 1, [171, 101], [[0.8, -1.3], [0.8, 0.7], [0.2, -1.1]], 56]], [['jump', 'jump', [224.293, 112.024]], 0]], ['rejection mid sweep', [[1, 1, [37, 154], [[0.4, 1.3], [-0.5, -0.6], [-0.8, 1.4]], 82]], [[[25.747, 94.301], 'jump', [83.474, 72.542]], 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 |
|---|---|---|---|
| smooth arc | [[[0.0, 90.0], [5.997, 89.427], [12.456, 87.708]], 0] | [[[0.0, 90.0], [5.997, 89.427], [12.456, 87.708]], 0] | Passed |
| seam crossing | [['jump', 'jump', 'jump'], 0] | [['jump', 'jump', 'jump'], 0] | Passed |
| multi-turn start | [[[360.0, 90.0], [354.576, 89.427]], 0] | [[[360.0, 90.0], [354.576, 89.427]], 0] | Passed |
| dropout in middle | [[[0.0, 90.0], 'hold', [5.997, 89.427]], 0] | [[[0.0, 90.0], 'hold', [5.997, 89.427]], 0] | Passed |
| dropout then branch change | [[[-3.716, 63.577], 'hold', [-51.862, 62.613]], 0] | [[[-3.716, 63.577], 'hold', [-51.862, 62.613]], 0] | Passed |
| large jump rejected | [[[0.0, 90.0], 'jump', [-5.738, 95.451]], 0] | [[[0.0, 90.0], 'jump', [-5.738, 95.451]], 0] | Passed |
| dropout across branch change | [[[-24.867, 31.788], 'hold', [-132.051, -14.07]], 0] | [[[-24.867, 31.788], 'hold', [-132.051, -14.07]], 1] | Failed |
SHA-256 / 352c9bd9edbb74a5ba034d409ce2e1d85999f991a452248a6814f78b540d43b2
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,q0,targets,maxjump=x
prev=list(q0)
out=[]
flips=0
branch=None
for px,py in targets:
c=(px*px+py*py-l1*l1-l2*l2)/(2*l1*l2)
if abs(c)>1.0:
out.append('hold')
prev=list(q0)
continue
sols=[]
for sgn in (1,-1):
s=sgn*math.sqrt(1-c*c)
t2=math.degrees(math.atan2(s,c))
t1=math.degrees(math.atan2(py,px)-math.atan2(l2*s,l1+l2*c))
sols.append([sgn,t1,t2])
def dist(sol):
return max(abs((sol[1]-prev[0]+180.0)%360.0-180.0),abs((sol[2]-prev[1]+180.0)%360.0-180.0))
best=min(sols,key=dist)
if dist(best)>maxjump:
out.append('jump')
continue
if branch is not None and best[0]!=branch: flips+=1
branch=best[0]
t1=prev[0]+((best[1]-prev[0]+180.0)%360.0-180.0)
t2=prev[1]+((best[2]-prev[1]+180.0)%360.0-180.0)
prev=[t1,t2]
out.append([round(t1,3),round(t2,3)])
return [out,flips]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['smooth arc', [[1, 1, [0, 90], [[1, 1], [0.9, 1.1], [0.8, 1.2]], 30]], [[[0.0, 90.0], [5.997, 89.427], [12.456, 87.708]], 0]], ['seam crossing', [[1, 1, [170, 20], [[-1.5, 0.3], [-1.5, -0.3], [-1.4, -0.6]], 40]], [['jump', 'jump', 'jump'], 0]], ['multi-turn start', [[1, 1, [360, 90], [[1, 1], [1.1, 0.9]], 30]], [[[360.0, 90.0], [354.576, 89.427]], 0]], ['dropout in middle', [[1, 1, [0, 90], [[1, 1], [3, 0], [0.9, 1.1]], 30]], [[[0.0, 90.0], 'hold', [5.997, 89.427]], 0]], ['dropout then branch change', [[1, 1, [0, 60], [[1.5, 0.8], [3, 3], [1.6, -0.6]], 200]], [[[-3.716, 63.577], 'hold', [-51.862, 62.613]], 0]], ['large jump rejected', [[1, 1, [0, 90], [[1, 1], [-1, -1], [1, 0.9]], 20]], [[[0.0, 90.0], 'jump', [-5.738, 95.451]], 0]], ['dropout across branch change', [[1, 1, [-93, 83], [[1.9, -0.3], [3, 3], [-1.5, -1.3]], 120]], [[[-24.867, 31.788], 'hold', [-132.051, -14.07]], 1]]], [['dropout in middle', [[1, 1, [0, 90], [[1, 1], [3, 0], [0.9, 1.1]], 30]], [[[0.0, 90.0], 'hold', [5.997, 89.427]], 0]], ['dropout then branch change', [[1, 1, [0, 60], [[1.5, 0.8], [3, 3], [1.6, -0.6]], 200]], [[[-3.716, 63.577], 'hold', [-51.862, 62.613]], 0]], ['large jump rejected', [[1, 1, [0, 90], [[1, 1], [-1, -1], [1, 0.9]], 20]], [[[0.0, 90.0], 'jump', [-5.738, 95.451]], 0]], ['jump exactly at limit', [[2, 1, [0, 0], [[3, 0], [2, 1]], 90]], [[[0.0, 0.0], [0.0, 90.0]], 0]], ['flip through straight', [[1, 1, [0, 30], [[1.9, 0.3], [2, 0], [1.9, -0.3], [1.8, -0.6]], 60]], [[[-6.922, 31.788], [0.0, 0.0], [-24.867, 31.788], [-36.87, 36.87]], 0]], ['start near down branch', [[1, 1, [60, -60], [[1.2, 1.0], [1.3, 0.9]], 50]], [[[78.451, -77.291], [72.456, -75.522]], 0]], ['down to up flip', [[1, 1, [69, -23], [[0.3, -0.8], [1.9, -0.9], [0.1, 0.9]], 164]], [[[-4.734, -129.42], 'hold', [20.581, -233.843]], 1]]], [['jump exactly at limit', [[2, 1, [0, 0], [[3, 0], [2, 1]], 90]], [[[0.0, 0.0], [0.0, 90.0]], 0]], ['flip through straight', [[1, 1, [0, 30], [[1.9, 0.3], [2, 0], [1.9, -0.3], [1.8, -0.6]], 60]], [[[-6.922, 31.788], [0.0, 0.0], [-24.867, 31.788], [-36.87, 36.87]], 0]], ['start near down branch', [[1, 1, [60, -60], [[1.2, 1.0], [1.3, 0.9]], 50]], [[[78.451, -77.291], [72.456, -75.522]], 0]], ['big jump budget', [[1, 1.5, [0, 0], [[-1, 1.5], [1.5, -1], [0.5, 2]], 360]], [[[67.38, 90.0], [-90.0, 90.0], [32.65, 70.529]], 0]], ['negative seam elbow', [[1, 1, [-170, -20], [[-1.5, -0.3], [-1.5, 0.3], [-1.3, 0.8]], 45]], [['jump', 'jump', 'jump'], 0]], ['all unreachable', [[1, 1, [0, 0], [[3, 3], [0, 5]], 30]], [['hold', 'hold'], 0]], ['dropout near base', [[1, 1, [146, 142], [[-0.1, -1.0], [3, 3], [-0.3, 0.2]], 120]], [[[204.455, 119.67], 'hold', [225.924, 200.772]], 1]]], [['big jump budget', [[1, 1.5, [0, 0], [[-1, 1.5], [1.5, -1], [0.5, 2]], 360]], [[[67.38, 90.0], [-90.0, 90.0], [32.65, 70.529]], 0]], ['negative seam elbow', [[1, 1, [-170, -20], [[-1.5, -0.3], [-1.5, 0.3], [-1.3, 0.8]], 45]], [['jump', 'jump', 'jump'], 0]], ['all unreachable', [[1, 1, [0, 0], [[3, 3], [0, 5]], 30]], [['hold', 'hold'], 0]], ['jump then recover', [[1, 1, [0, 120], [[0.2, 1.0], [1.8, 0.2], [0.3, 1.0]], 60]], [[[19.347, 118.685], 'jump', [14.768, 117.065]], 0]], ['second target flips', [[1.5, 1, [0, 45], [[2, 1.2], [2.2, -0.4]], 150]], [[[13.925, 43.114], [-31.604, 54.315]], 0]], ['dropout across branch change', [[1, 1, [-93, 83], [[1.9, -0.3], [3, 3], [-1.5, -1.3]], 120]], [[[-24.867, 31.788], 'hold', [-132.051, -14.07]], 1]], ['down to up flip', [[1, 1, [69, -23], [[0.3, -0.8], [1.9, -0.9], [0.1, 0.9]], 164]], [[[-4.734, -129.42], 'hold', [20.581, -233.843]], 1]]], [['jump then recover', [[1, 1, [0, 120], [[0.2, 1.0], [1.8, 0.2], [0.3, 1.0]], 60]], [[[19.347, 118.685], 'jump', [14.768, 117.065]], 0]], ['second target flips', [[1.5, 1, [0, 45], [[2, 1.2], [2.2, -0.4]], 150]], [[[13.925, 43.114], [-31.604, 54.315]], 0]], ['dropout across branch change', [[1, 1, [-93, 83], [[1.9, -0.3], [3, 3], [-1.5, -1.3]], 120]], [[[-24.867, 31.788], 'hold', [-132.051, -14.07]], 1]], ['down to up flip', [[1, 1, [69, -23], [[0.3, -0.8], [1.9, -0.9], [0.1, 0.9]], 164]], [[[-4.734, -129.42], 'hold', [20.581, -233.843]], 1]], ['wide sweep flips', [[1, 1, [-5, -54], [[0.1, -1.7], [0.7, 1.5], [-1.2, 0.5]], 154]], [[[-55.005, -63.256], [30.841, 68.284], [107.922, 98.917]], 1]], ['rejected then flip', [[1, 1, [171, 101], [[0.8, -1.3], [0.8, 0.7], [0.2, -1.1]], 56]], [['jump', 'jump', [224.293, 112.024]], 0]], ['rejection mid sweep', [[1, 1, [37, 154], [[0.4, 1.3], [-0.5, -0.6], [-0.8, 1.4]], 82]], [[[25.747, 94.301], 'jump', [83.474, 72.542]], 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 |
|---|---|---|---|
| smooth arc | [[[0.0, 90.0], [5.997, 89.427], [12.456, 87.708]], 0] | [[[0.0, 90.0], [5.997, 89.427], [12.456, 87.708]], 0] | Passed |
| seam crossing | [['jump', 'jump', 'jump'], 0] | [['jump', 'jump', 'jump'], 0] | Passed |
| multi-turn start | [[[360.0, 90.0], [354.576, 89.427]], 0] | [[[360.0, 90.0], [354.576, 89.427]], 0] | Passed |
| dropout in middle | [[[0.0, 90.0], 'hold', [5.997, 89.427]], 0] | [[[0.0, 90.0], 'hold', [5.997, 89.427]], 0] | Passed |
| dropout then branch change | [[[-3.716, 63.577], 'hold', [-51.862, 62.613]], 0] | [[[-3.716, 63.577], 'hold', [-51.862, 62.613]], 0] | Passed |
| large jump rejected | [[[0.0, 90.0], 'jump', [-5.738, 95.451]], 0] | [[[0.0, 90.0], 'jump', [-5.738, 95.451]], 0] | Passed |
| dropout across branch change | [[[-24.867, 31.788], 'hold', [-146.121, 14.07]], 0] | [[[-24.867, 31.788], 'hold', [-132.051, -14.07]], 1] | Failed |
SHA-256 / 90fd397e6f2676e1fe1ee6268dc449f37dd46dcd2da043ee8d195aff2a4fc246
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,q0,targets,maxjump=x
prev=list(q0)
out=[]
flips=0
branch=None
for px,py in targets:
c=(px*px+py*py-l1*l1-l2*l2)/(2*l1*l2)
if abs(c)>1.0:
out.append('hold')
continue
sols=[]
for sgn in (1,-1):
s=sgn*math.sqrt(1-c*c)
t2=math.degrees(math.atan2(s,c))
t1=math.degrees(math.atan2(py,px)-math.atan2(l2*s,l1+l2*c))
sols.append([sgn,t1,t2])
def dist(sol):
return max(abs((sol[1]-prev[0]+180.0)%360.0-180.0),abs((sol[2]-prev[1]+180.0)%360.0-180.0))
best=min(sols,key=dist)
if dist(best)>maxjump:
out.append('jump')
continue
if branch is not None and best[0]!=branch: flips+=1
branch=best[0]
t1=prev[0]+((best[1]-prev[0]+180.0)%360.0-180.0)
t2=prev[1]+((best[2]-prev[1]+180.0)%360.0-180.0)
prev=[t1,t2]
out.append([round(t1,3),round(t2,3)])
return [out,flips]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['smooth arc', [[1, 1, [0, 90], [[1, 1], [0.9, 1.1], [0.8, 1.2]], 30]], [[[0.0, 90.0], [5.997, 89.427], [12.456, 87.708]], 0]], ['seam crossing', [[1, 1, [170, 20], [[-1.5, 0.3], [-1.5, -0.3], [-1.4, -0.6]], 40]], [['jump', 'jump', 'jump'], 0]], ['multi-turn start', [[1, 1, [360, 90], [[1, 1], [1.1, 0.9]], 30]], [[[360.0, 90.0], [354.576, 89.427]], 0]], ['dropout in middle', [[1, 1, [0, 90], [[1, 1], [3, 0], [0.9, 1.1]], 30]], [[[0.0, 90.0], 'hold', [5.997, 89.427]], 0]], ['dropout then branch change', [[1, 1, [0, 60], [[1.5, 0.8], [3, 3], [1.6, -0.6]], 200]], [[[-3.716, 63.577], 'hold', [-51.862, 62.613]], 0]], ['large jump rejected', [[1, 1, [0, 90], [[1, 1], [-1, -1], [1, 0.9]], 20]], [[[0.0, 90.0], 'jump', [-5.738, 95.451]], 0]], ['dropout across branch change', [[1, 1, [-93, 83], [[1.9, -0.3], [3, 3], [-1.5, -1.3]], 120]], [[[-24.867, 31.788], 'hold', [-132.051, -14.07]], 1]]], [['dropout in middle', [[1, 1, [0, 90], [[1, 1], [3, 0], [0.9, 1.1]], 30]], [[[0.0, 90.0], 'hold', [5.997, 89.427]], 0]], ['dropout then branch change', [[1, 1, [0, 60], [[1.5, 0.8], [3, 3], [1.6, -0.6]], 200]], [[[-3.716, 63.577], 'hold', [-51.862, 62.613]], 0]], ['large jump rejected', [[1, 1, [0, 90], [[1, 1], [-1, -1], [1, 0.9]], 20]], [[[0.0, 90.0], 'jump', [-5.738, 95.451]], 0]], ['jump exactly at limit', [[2, 1, [0, 0], [[3, 0], [2, 1]], 90]], [[[0.0, 0.0], [0.0, 90.0]], 0]], ['flip through straight', [[1, 1, [0, 30], [[1.9, 0.3], [2, 0], [1.9, -0.3], [1.8, -0.6]], 60]], [[[-6.922, 31.788], [0.0, 0.0], [-24.867, 31.788], [-36.87, 36.87]], 0]], ['start near down branch', [[1, 1, [60, -60], [[1.2, 1.0], [1.3, 0.9]], 50]], [[[78.451, -77.291], [72.456, -75.522]], 0]], ['down to up flip', [[1, 1, [69, -23], [[0.3, -0.8], [1.9, -0.9], [0.1, 0.9]], 164]], [[[-4.734, -129.42], 'hold', [20.581, -233.843]], 1]]], [['jump exactly at limit', [[2, 1, [0, 0], [[3, 0], [2, 1]], 90]], [[[0.0, 0.0], [0.0, 90.0]], 0]], ['flip through straight', [[1, 1, [0, 30], [[1.9, 0.3], [2, 0], [1.9, -0.3], [1.8, -0.6]], 60]], [[[-6.922, 31.788], [0.0, 0.0], [-24.867, 31.788], [-36.87, 36.87]], 0]], ['start near down branch', [[1, 1, [60, -60], [[1.2, 1.0], [1.3, 0.9]], 50]], [[[78.451, -77.291], [72.456, -75.522]], 0]], ['big jump budget', [[1, 1.5, [0, 0], [[-1, 1.5], [1.5, -1], [0.5, 2]], 360]], [[[67.38, 90.0], [-90.0, 90.0], [32.65, 70.529]], 0]], ['negative seam elbow', [[1, 1, [-170, -20], [[-1.5, -0.3], [-1.5, 0.3], [-1.3, 0.8]], 45]], [['jump', 'jump', 'jump'], 0]], ['all unreachable', [[1, 1, [0, 0], [[3, 3], [0, 5]], 30]], [['hold', 'hold'], 0]], ['dropout near base', [[1, 1, [146, 142], [[-0.1, -1.0], [3, 3], [-0.3, 0.2]], 120]], [[[204.455, 119.67], 'hold', [225.924, 200.772]], 1]]], [['big jump budget', [[1, 1.5, [0, 0], [[-1, 1.5], [1.5, -1], [0.5, 2]], 360]], [[[67.38, 90.0], [-90.0, 90.0], [32.65, 70.529]], 0]], ['negative seam elbow', [[1, 1, [-170, -20], [[-1.5, -0.3], [-1.5, 0.3], [-1.3, 0.8]], 45]], [['jump', 'jump', 'jump'], 0]], ['all unreachable', [[1, 1, [0, 0], [[3, 3], [0, 5]], 30]], [['hold', 'hold'], 0]], ['jump then recover', [[1, 1, [0, 120], [[0.2, 1.0], [1.8, 0.2], [0.3, 1.0]], 60]], [[[19.347, 118.685], 'jump', [14.768, 117.065]], 0]], ['second target flips', [[1.5, 1, [0, 45], [[2, 1.2], [2.2, -0.4]], 150]], [[[13.925, 43.114], [-31.604, 54.315]], 0]], ['dropout across branch change', [[1, 1, [-93, 83], [[1.9, -0.3], [3, 3], [-1.5, -1.3]], 120]], [[[-24.867, 31.788], 'hold', [-132.051, -14.07]], 1]], ['down to up flip', [[1, 1, [69, -23], [[0.3, -0.8], [1.9, -0.9], [0.1, 0.9]], 164]], [[[-4.734, -129.42], 'hold', [20.581, -233.843]], 1]]], [['jump then recover', [[1, 1, [0, 120], [[0.2, 1.0], [1.8, 0.2], [0.3, 1.0]], 60]], [[[19.347, 118.685], 'jump', [14.768, 117.065]], 0]], ['second target flips', [[1.5, 1, [0, 45], [[2, 1.2], [2.2, -0.4]], 150]], [[[13.925, 43.114], [-31.604, 54.315]], 0]], ['dropout across branch change', [[1, 1, [-93, 83], [[1.9, -0.3], [3, 3], [-1.5, -1.3]], 120]], [[[-24.867, 31.788], 'hold', [-132.051, -14.07]], 1]], ['down to up flip', [[1, 1, [69, -23], [[0.3, -0.8], [1.9, -0.9], [0.1, 0.9]], 164]], [[[-4.734, -129.42], 'hold', [20.581, -233.843]], 1]], ['wide sweep flips', [[1, 1, [-5, -54], [[0.1, -1.7], [0.7, 1.5], [-1.2, 0.5]], 154]], [[[-55.005, -63.256], [30.841, 68.284], [107.922, 98.917]], 1]], ['rejected then flip', [[1, 1, [171, 101], [[0.8, -1.3], [0.8, 0.7], [0.2, -1.1]], 56]], [['jump', 'jump', [224.293, 112.024]], 0]], ['rejection mid sweep', [[1, 1, [37, 154], [[0.4, 1.3], [-0.5, -0.6], [-0.8, 1.4]], 82]], [[[25.747, 94.301], 'jump', [83.474, 72.542]], 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 |
|---|---|---|---|
| smooth arc | [[[0.0, 90.0], [5.997, 89.427], [12.456, 87.708]], 0] | [[[0.0, 90.0], [5.997, 89.427], [12.456, 87.708]], 0] | Passed |
| seam crossing | [['jump', 'jump', 'jump'], 0] | [['jump', 'jump', 'jump'], 0] | Passed |
| multi-turn start | [[[360.0, 90.0], [354.576, 89.427]], 0] | [[[360.0, 90.0], [354.576, 89.427]], 0] | Passed |
| dropout in middle | [[[0.0, 90.0], 'hold', [5.997, 89.427]], 0] | [[[0.0, 90.0], 'hold', [5.997, 89.427]], 0] | Passed |
| dropout then branch change | [[[-3.716, 63.577], 'hold', [-51.862, 62.613]], 0] | [[[-3.716, 63.577], 'hold', [-51.862, 62.613]], 0] | Passed |
| large jump rejected | [[[0.0, 90.0], 'jump', [-5.738, 95.451]], 0] | [[[0.0, 90.0], 'jump', [-5.738, 95.451]], 0] | Passed |
| dropout across branch change | [[[-24.867, 31.788], 'hold', [-132.051, -14.07]], 1] | [[[-24.867, 31.788], 'hold', [-132.051, -14.07]], 1] | Passed |
SHA-256 / 0d31775f491bb73e426b3499143a6d9837a2a5619ef734a6e749a347c27e19c9
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.257743+00:00.
Case digest / 3ba038332d80cae89632e0fb54adb4398ebb38267ce0986d88a3f687c7e1526d