FA-88256 / Inverse kinematics solvers / Open access
Trajectory output jumps by 360 at the seam · case 01
A continuous joint command steps from 179 to -179 instead of moving to 181.
ROOT CAUSE
The chosen solution is emitted in its principal range instead of unwrapped near the previous angle.
VERIFIED REPAIR
Unwrap both joints relative to the previous state.
Unsuccessful approach: Unwrapping only the shoulder leaves the elbow discontinuous.
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')
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=best[1]
t2=best[2]
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]], ['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]]], [['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]], ['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]], ['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]]], [['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]], ['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]], ['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]]], [['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]], ['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]]], [['multi-turn start', [[1, 1, [360, 90], [[1, 1], [1.1, 0.9]], 30]], [[[360.0, 90.0], [354.576, 89.427]], 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 | [[[0.0, 90.0], [-5.424, 89.427]], 0] | [[[360.0, 90.0], [354.576, 89.427]], 0] | Failed |
| 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 |
| down to up flip | [[[-4.734, -129.42], 'hold', [20.581, 126.157]], 1] | [[[-4.734, -129.42], 'hold', [20.581, -233.843]], 1] | Failed |
SHA-256 / 27915a48fbfdb6e490e90a985d0e64ccf1cd5f74eba4b8e22f9e596d708f4908
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')
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=best[2]
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]], ['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]]], [['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]], ['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]], ['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]]], [['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]], ['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]], ['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]]], [['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]], ['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]]], [['multi-turn start', [[1, 1, [360, 90], [[1, 1], [1.1, 0.9]], 30]], [[[360.0, 90.0], [354.576, 89.427]], 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 |
| down to up flip | [[[-4.734, -129.42], 'hold', [20.581, 126.157]], 1] | [[[-4.734, -129.42], 'hold', [20.581, -233.843]], 1] | Failed |
SHA-256 / 6223f58052b17589d4d18c74b811d4918546ee6e5e7d987a6966a9fb7362f7a8
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]], ['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]]], [['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]], ['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]], ['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]]], [['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]], ['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]], ['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]]], [['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]], ['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]]], [['multi-turn start', [[1, 1, [360, 90], [[1, 1], [1.1, 0.9]], 30]], [[[360.0, 90.0], [354.576, 89.427]], 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 |
| down to up flip | [[[-4.734, -129.42], 'hold', [20.581, -233.843]], 1] | [[[-4.734, -129.42], 'hold', [20.581, -233.843]], 1] | Passed |
SHA-256 / a791c4342f4c7be4280878b956cbed7d11d8809bde9a74db30fe4bd8ec726ff7
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.467877+00:00.
Case digest / a8faf70e9e2eeb682707bd44349b87cd9bb3ddb23455255c30829ef7e9deaaae