Mujoco KDL Wrapper  0.5.1
MuJoCo + KDL bridge for robot kinematics and dynamics
Loading...
Searching...
No Matches
ex_table_pick_place.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""Table pick-place with joint impedance, pushed while carrying the cube; Python counterpart
3of ex_table_pick_place.cpp. Headless it exits 1 unless the cube is placed, the push deflection
4matches J K^-1 J^T F, the arm springs back, the cube stays in hand and the elbow stays up."""
5
6from __future__ import annotations
7
8import argparse
9import math
10from dataclasses import dataclass
11
12import PyKDL as kdl
13
14import mj_kdl_wrapper as mjk
15
16HOME = [0.0, 0.2618, 3.1416, -2.2689, 0.0, 0.9599, 1.5708]
17SURFACE_Z = 0.70
18CUBE_HS = 0.02
19PICK = (0.40, 0.00)
20PLACE = (0.40, 0.24)
21KP = [100.0, 200.0, 100.0, 200.0, 100.0, 200.0, 100.0]
22KD = [10.0, 20.0, 10.0, 20.0, 10.0, 20.0, 10.0]
23GRIPPER_CLOSED = 0.82 # [rad] top of the 2F-85's ctrlrange (its driver joint stops at 0.8)
24CUBE_START = [PICK[0], PICK[1], SURFACE_Z + CUBE_HS]
25MAX_PLACE_ERR = 0.005 # [m] in the table plane
26
27PUSH_BODY = "bracelet_link"
28PUSH_PHASE = "PLACE_ABOVE"
29PUSH_FORCE = 20.0 # [N] along world +x, across the carry along +y
30PUSH_DIR = kdl.Vector(1.0, 0.0, 0.0)
31PUSH_ON, PUSH_OFF, PUSH_RAMP = 0.8, 2.0, 0.2 # [s] into the phase; rise and fall
32SETTLE = 0.5 # [s] after the push, when the residual is taken
33
34MIN_DEFLECTION = 0.010 # [m]
35DEFLECTION_RATIO = (0.85, 1.15) # measured / predicted
36MAX_RESIDUAL = 0.004 # [m]
37MAX_CUBE_SLIP = 0.010 # [m] cube centre from the TCP while pushed
38MIN_ELBOW_HEIGHT = 0.45 # [m] forearm_link origin above the table
39
40
41class ResetRequested(Exception):
42 """Raised after a simulate UI reset, to restart the sequence."""
43
44
45@dataclass(frozen=True)
46class Phase:
47 name: str
48 target: list[float]
49 duration: float
50 timeout: float
51 settle_tol: float
52 gripper: float
53
54
55def build_env() -> tuple[mjk.Env, mjk.Robot]:
56 table = mjk.SceneObject()
57 table.name = "table"
58 table.mjcf_path = str(mjk.ASSETS_DIR / "table.xml")
59 table.pos = [0.0, 0.0, SURFACE_Z]
60 table.fixed = True
61 cube = mjk.SceneObject()
62 cube.name = "cube"
63 cube.shape = mjk.Shape.BOX
64 cube.size = [CUBE_HS, CUBE_HS, CUBE_HS]
65 cube.pos = CUBE_START[:]
66 cube.rgba = [0.1, 0.35, 1.0, 1.0]
67 cube.mass = 0.1
68 cube.condim = mjk.Condim.Torsional
69 cube.friction = [0.8, 0.02, 0.001]
70 gripper = mjk.AttachmentSpec()
71 gripper.mjcf_path = str(mjk.ASSETS_DIR / "robotiq_2f85/2f85.xml")
72 gripper.attach_to = mjk.AttachTarget(mjk.AttachKind.Site, "pinch_site")
73 gripper.prefix = "g_"
74 robot_spec = mjk.RobotSpec()
75 robot_spec.path = str(mjk.ASSETS_DIR / "kinova_gen3/gen3.xml")
76 robot_spec.pos = [0.0, 0.0, SURFACE_Z]
77 robot_spec.attachments = [gripper]
78
79 spec = mjk.SceneSpec()
80 spec.timestep = 0.002
81 spec.add_floor = True
82 spec.add_skybox = True
83 spec.objects = [table, cube]
84 spec.robots = [robot_spec]
85 env = mjk.Env.build(spec)
86 tool = mjk.ToolFrameSpec()
87 tool.tool_body = "g_base_mount"
88 tool.tcp_site = "g_pinch"
89 return env, env.create_robot("base_link", "bracelet_link", tool=tool)
90
91
92def jnt(values) -> kdl.JntArray:
93 q = kdl.JntArray(len(values))
94 for i, value in enumerate(values):
95 q[i] = value
96 return q
97
98
99def gravity(dyn: kdl.ChainDynParam, q_values) -> list[float]:
100 g = kdl.JntArray(len(q_values))
101 dyn.JntToGravity(jnt(q_values), g)
102 return [g[i] for i in range(g.rows())]
103
104
105def solve_near_seed(chain, limits, seed: list[float], target: kdl.Frame) -> list[float]:
106 """Damped-least-squares IK stepped from seed, so the answer stays on the seed's branch."""
107 fk = kdl.ChainFkSolverPos_recursive(chain)
108 ik = kdl.ChainIkSolverVel_wdls(chain, 1e-5, 150)
109 ik.setLambda(0.05)
110 q = jnt(seed)
111 dq = kdl.JntArray(q.rows())
112 for _ in range(300):
113 current = kdl.Frame()
114 fk.JntToCart(q, current)
115 dx = kdl.diff(current, target)
116 if dx.vel.Norm() <= 2e-3 and dx.rot.Norm() <= 2e-2:
117 break
118 if dx.vel.Norm() > 0.05:
119 dx.vel = dx.vel * (0.05 / dx.vel.Norm())
120 if dx.rot.Norm() > 0.20:
121 dx.rot = dx.rot * (0.20 / dx.rot.Norm())
122 if ik.CartToJnt(q, dx, dq) < 0:
123 raise RuntimeError("PyKDL IK velocity step failed")
124 # joint_limits are +-inf for an unlimited joint, so the clamp leaves it alone.
125 for i in range(q.rows()):
126 q[i] = min(limits[i][1], max(limits[i][0], q[i] + dq[i]))
127 else:
128 current = kdl.Frame()
129 fk.JntToCart(q, current)
130 dx = kdl.diff(current, target)
131 if dx.vel.Norm() > 2e-3 or dx.rot.Norm() > 2e-2:
132 raise RuntimeError("PyKDL IK did not converge")
133 return [q[i] for i in range(q.rows())]
134
135
136def pick_place_phases(robot: mjk.Robot) -> list[Phase]:
137 chain = robot.kdl_chain()
138 seed = HOME[:]
139
140 # Base-frame heights: the TCP at the cube centre, then above it.
141 def solve(xy, z: float) -> list[float]:
142 nonlocal seed
143 target = kdl.Frame(robot.tip_T_tcp.M, kdl.Vector(xy[0], xy[1], z))
144 seed = solve_near_seed(chain, robot.joint_limits, seed, target)
145 return seed[:]
146
147 pick_above = solve(PICK, CUBE_HS + 0.20)
148 pick = solve(PICK, CUBE_HS)
149 lift = solve(PICK, CUBE_HS + 0.30)
150 place_above = solve(PLACE, CUBE_HS + 0.20)
151 place = solve(PLACE, CUBE_HS)
152 closed = GRIPPER_CLOSED
153 return [
154 Phase("HOME", HOME, 1.0, 2.5, 0.08, 0.0),
155 Phase("PICK_ABOVE", pick_above, 5.0, 7.0, 0.08, 0.0),
156 Phase("PICK", pick, 5.0, 8.0, 0.03, 0.0),
157 Phase("CLOSE", pick, 1.5, 2.5, -1.0, closed),
158 Phase("LIFT", lift, 3.0, 5.0, 0.08, closed),
159 Phase("PLACE_ABOVE", place_above, 3.0, 5.0, 0.08, closed),
160 Phase("PLACE", place, 5.0, 8.0, 0.03, closed),
161 Phase("OPEN", place, 1.0, 2.0, -1.0, 0.0),
162 Phase("RETREAT", place_above, 2.0, 4.0, 0.08, 0.0),
163 Phase("HOLD", place_above, 1.0, 1.0, -1.0, 0.0),
164 ]
165
166
167def new_metrics() -> dict:
168 return {
169 "lag0": kdl.Vector(), # TCP minus its reference just before the push
170 "peak": 0.0, # [m] along the push
171 "predicted": 0.0, # [m] J K^-1 J^T F at the peak
172 "residual": -1.0, # [m] along the push, SETTLE after it; < 0 until taken
173 "cube_slip": 0.0,
174 "elbow_min": math.inf, # [m] world z
175 }
176
177
178def push_scale(t_rel: float) -> float:
179 rise = (t_rel - PUSH_ON) / PUSH_RAMP
180 fall = (PUSH_OFF - t_rel) / PUSH_RAMP
181 return min(1.0, max(0.0, min(rise, fall)))
182
183
184def measure_push(env, robot, state: dict, q_ref, cube, t_rel: float, force) -> None:
185 """Deflection against the reference of the same cycle, and what the stiffness predicts."""
186 m, fk, jac_solver = state["metrics"], state["fk"], state["jac"]
187 base = kdl.Vector(0.0, 0.0, SURFACE_Z)
188 tcp, ref = kdl.Frame(), kdl.Frame()
189 fk.JntToCart(jnt(robot.jnt_pos_msr), tcp)
190 fk.JntToCart(jnt(q_ref), ref)
191 m["cube_slip"] = max(m["cube_slip"], (cube - base - tcp.p).Norm())
192 lag = tcp.p - ref.p
193 if t_rel < PUSH_ON:
194 m["lag0"] = lag
195 deflection = kdl.dot(lag - m["lag0"], PUSH_DIR)
196 if t_rel >= PUSH_OFF + SETTLE and m["residual"] < 0.0:
197 m["residual"] = abs(deflection)
198 # Only at full force: on the ramps the arm lags the changing force.
199 full = PUSH_ON + PUSH_RAMP <= t_rel <= PUSH_OFF - PUSH_RAMP
200 if not full or deflection <= m["peak"]:
201 return
202
203 # dq = K^-1 J_push^T F at the pushed body's centre of mass, seen at the TCP through J.
204 n = robot.n_joints
205 jac = kdl.Jacobian(n)
206 jac_solver.JntToJac(jnt(q_ref), jac)
207 jac_push = kdl.Jacobian(jac)
208 jac_push.changeRefPoint(kdl.Vector(*env.data.body(PUSH_BODY).xipos) - base - ref.p)
209 dx = kdl.Vector()
210 for i in range(n):
211 dx += jac.getColumn(i).vel * (kdl.dot(jac_push.getColumn(i).vel, force) / KP[i])
212 m["peak"] = deflection
213 m["predicted"] = kdl.dot(dx, PUSH_DIR)
214
215
216def run_phase(env, robot, fingers, dyn, phase: Phase, state: dict) -> bool:
217 """One update() per step: it reads the state and applies the previous cycle's command."""
218 print(f"State: {phase.name}")
219 start = robot.jnt_pos_msr[:]
220 t0 = env.data.time
221 push = env.data.body(PUSH_BODY)
222 pushing = phase.name == PUSH_PHASE
223 while True:
224 env.update()
225 elapsed = env.data.time - t0
226 alpha = min(1.0, max(0.0, elapsed / phase.duration)) if phase.duration > 0.0 else 1.0
227 q, qd = robot.jnt_pos_msr, robot.jnt_vel_msr
228 q_ref = [start[i] + alpha * (phase.target[i] - start[i]) for i in range(len(q))]
229 g = gravity(dyn, q)
230 robot.jnt_trq_cmd = [
231 g[i] + KP[i] * (q_ref[i] - q[i]) - KD[i] * qd[i] for i in range(len(q))
232 ]
233 fingers.ctrl[0] = phase.gripper
234 cube = env.body_frame("cube").p if pushing else None
235 err = max(abs(t - m) for t, m in zip(phase.target, q))
236 done_pose = phase.settle_tol < 0.0 or err <= phase.settle_tol
237 if (elapsed >= phase.duration and done_pose) or elapsed >= phase.timeout:
238 return True
239 if not env.step():
240 return False
241 if state["reset"]:
242 state["reset"] = False
243 raise ResetRequested()
244 env.pace()
245 metrics = state["metrics"]
246 metrics["elbow_min"] = min(metrics["elbow_min"], env.body_frame("forearm_link").p.z())
247 force = PUSH_DIR * (PUSH_FORCE * push_scale(elapsed) if pushing else 0.0)
248 push.xfrc_applied[:] = [force.x(), force.y(), force.z(), 0.0, 0.0, 0.0]
249 if pushing:
250 measure_push(env, robot, state, q_ref, cube, elapsed, force)
251
252
253def main() -> int:
254 parser = argparse.ArgumentParser()
255 parser.add_argument("--gui", action="store_true")
256 args = parser.parse_args()
257
258 env, robot = build_env()
259 try:
260 robot.set_control_mode(mjk.CtrlMode.TORQUE)
261 fingers = env.data.actuator("g_fingers_actuator")
262 chain = robot.kdl_chain()
263 dyn = kdl.ChainDynParam(chain, kdl.Vector(0.0, 0.0, env.spec.gravity_z))
264 phases = pick_place_phases(robot)
265 state = {
266 "reset": False,
267 "metrics": new_metrics(),
268 "fk": kdl.ChainFkSolverPos_recursive(chain),
269 "jac": kdl.ChainJntToJacSolver(chain),
270 }
271
272 def on_reset(ctx):
273 robot.set_joint_pos(HOME)
274 env.set_body_pose("cube", CUBE_START)
275 fingers.ctrl[0] = 0.0
276 robot.jnt_trq_cmd = gravity(dyn, HOME)
277 state["metrics"] = new_metrics()
278 state["reset"] = True
279
280 env.on_reset = on_reset
281 env.reset()
282 state["reset"] = False
283 if args.gui:
284 env.open_viewer("ex_table_pick_place.py")
285
286 completed = False
287 while not completed:
288 try:
289 completed = all(run_phase(env, robot, fingers, dyn, p, state) for p in phases)
290 break
291 except ResetRequested:
292 continue
293 env.update()
294
295 cube = env.body_frame("cube").p
296 place_err = math.hypot(cube.x() - PLACE[0], cube.y() - PLACE[1])
297 on_table = abs(cube.z() - (SURFACE_Z + CUBE_HS)) < 0.002
298 m = state["metrics"]
299 ratio = m["peak"] / m["predicted"] if m["predicted"] > 0.0 else 0.0
300 elbow_h = m["elbow_min"] - SURFACE_Z
301 print(
302 f"cube final position: [{cube.x():.4f}, {cube.y():.4f}, {cube.z():.4f}] "
303 f"place error {place_err * 1000:.4f} mm (limit {MAX_PLACE_ERR * 1000} mm)"
304 + ("" if on_table else ", not on the table")
305 )
306 print(
307 f"push deflection: {m['peak'] * 1000:.4f} mm (at least {MIN_DEFLECTION * 1000} mm), "
308 f"stiffness predicts {m['predicted'] * 1000:.4f} mm, ratio {ratio:.4f} "
309 f"(limits {DEFLECTION_RATIO[0]}..{DEFLECTION_RATIO[1]})"
310 )
311 print(
312 f"deflection {SETTLE} s after the push: {m['residual'] * 1000:.4f} mm "
313 f"(limit {MAX_RESIDUAL * 1000} mm)"
314 )
315 print(
316 f"cube from the TCP while pushed: {m['cube_slip'] * 1000:.4f} mm "
317 f"(limit {MAX_CUBE_SLIP * 1000} mm)"
318 )
319 print(
320 f"lowest elbow above the table: {elbow_h * 1000:.4f} mm "
321 f"(limit {MIN_ELBOW_HEIGHT * 1000} mm)"
322 )
323 finally:
324 env.close()
325 if args.gui:
326 return 0
327 ok = (
328 completed
329 and on_table
330 and place_err <= MAX_PLACE_ERR
331 and m["peak"] >= MIN_DEFLECTION
332 and DEFLECTION_RATIO[0] <= ratio <= DEFLECTION_RATIO[1]
333 and 0.0 <= m["residual"] <= MAX_RESIDUAL
334 and m["cube_slip"] <= MAX_CUBE_SLIP
335 and elbow_h >= MIN_ELBOW_HEIGHT
336 )
337 print(f"{'PASS' if ok else 'FAIL'}: the cube was placed through the push")
338 return 0 if ok else 1
339
340
341if __name__ == "__main__":
342 raise SystemExit(main())
float push_scale(float t_rel)
list[float] solve_near_seed(chain, limits, list[float] seed, kdl.Frame target)
bool run_phase(env, robot, fingers, dyn, Phase phase, dict state)
tuple[mjk.Env, mjk.Robot] build_env()
None measure_push(env, robot, dict state, q_ref, cube, float t_rel, force)
list[float] gravity(kdl.ChainDynParam dyn, q_values)
kdl.JntArray jnt(values)
list[Phase] pick_place_phases(mjk.Robot robot)