Mujoco KDL Wrapper  0.2.2
MuJoCo + KDL bridge for robot kinematics and dynamics
Loading...
Searching...
No Matches
ex_table_pour.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""Table pour example ported from src/examples/ex_table_pour.cpp."""
3
4from __future__ import annotations
5
6import argparse
7import math
8from dataclasses import dataclass
9
10import PyKDL as kdl
11import mj_kdl_wrapper as mjk
12
13HOME = [0.0, 0.2618, 3.1416, -2.2689, 0.0, 0.9599, 1.5708]
14TABLE_Z = 0.70
15ROBOT_BACK_X = -0.26
16JUG_X = 0.30
17JUG_Y = 0.14
18RETREAT_X = JUG_X - 0.08
19RETREAT_Y = JUG_Y - 0.08
20BALL_RADIUS = 0.007
21NUM_BALLS = 36
22POUR_TILT_RAD = 1.95
23IK_TOL = 3e-3
24KP = [120.0, 220.0, 120.0, 220.0, 110.0, 190.0, 90.0]
25KD = [12.0, 22.0, 12.0, 22.0, 11.0, 18.0, 9.0]
26
27
28@dataclass(frozen=True)
29class Phase:
30 name: str
31 target: list[float]
32 duration: float
33 timeout: float
34 settle_tol: float
35 gripper: float
36
37
38class ResetRequested(Exception):
39 """Raised when the simulate UI reset is detected, to restart the sequence."""
40
41
42def gripper_attachment(gripper_path: str) -> mjk.AttachmentSpec:
43 attach = mjk.AttachmentSpec()
44 attach.mjcf_path = gripper_path
45 attach.attach_to = mjk.AttachTarget(mjk.AttachKind.Site, "pinch_site")
46 attach.prefix = "g_"
47 return attach
48
49
50def bottle_attachment(bottle_path: str) -> mjk.AttachmentSpec:
51 attach = mjk.AttachmentSpec()
52 attach.mjcf_path = bottle_path
53 attach.attach_to = mjk.AttachTarget(mjk.AttachKind.Body, "g_base")
54 attach.prefix = "pour_"
55 return attach
56
57
58def table_object(table_path: str) -> mjk.SceneObject:
59 table = mjk.SceneObject()
60 table.name = "table"
61 table.mjcf_path = table_path
62 table.pos = [0.0, 0.0, TABLE_Z]
63 table.fixed = True
64 return table
65
66
67def receiver_object(receiver_path: str) -> mjk.SceneObject:
68 recv = mjk.SceneObject()
69 recv.name = "recv"
70 recv.mjcf_path = receiver_path
71 recv.pos = [JUG_X, JUG_Y, TABLE_Z]
72 return recv
73
74
75def ball_object(index: int) -> mjk.SceneObject:
76 ball = mjk.SceneObject()
77 ball.name = f"grain_{index:02d}"
78 ball.shape = mjk.Shape.SPHERE
79 ball.size = [BALL_RADIUS, 0.0, 0.0]
80 ball.pos = [0.0, 0.0, TABLE_Z + 0.40 + index * 2.0 * BALL_RADIUS]
81 ball.rgba = [1.0, 0.84, 0.30, 1.0]
82 ball.mass = 0.006
83 ball.condim = mjk.Condim.Torsional
84 ball.friction = [0.5, 0.02, 0.001]
85 return ball
86
87
88def build_env() -> tuple[mjk.Env, mjk.Robot]:
89 spec = mjk.SceneSpec()
90 spec.timestep = 0.002
91 spec.add_floor = True
92 spec.add_skybox = True
93 spec.objects = [
94 table_object(mjk.menagerie.asset_path("table.xml", env_var="MJ_KDL_TABLE")),
95 *[ball_object(i) for i in range(NUM_BALLS)],
96 receiver_object(mjk.menagerie.asset_path("mug_table.xml", env_var="MJ_KDL_RECEIVER")),
97 ]
98
99 robot_spec = mjk.RobotSpec()
100 robot_spec.path = mjk.menagerie.model_path("kinova_gen3", env_var="MJ_KDL_MODEL")
101 robot_spec.pos = [ROBOT_BACK_X, 0.0, TABLE_Z]
102 robot_spec.attachments = [
104 mjk.menagerie.asset_path("robotiq_2f85/2f85.xml", env_var="MJ_KDL_GRIPPER")
105 ),
106 bottle_attachment(mjk.menagerie.asset_path("mug.xml", env_var="MJ_KDL_BOTTLE")),
107 ]
108 spec.robots = [robot_spec]
109
110 env = mjk.Env.build(spec)
111 tool = mjk.ToolFrameSpec()
112 tool.tool_body = "g_base"
113 tool.tcp_site = "g_pinch"
114 robot = env.create_robot("base_link", "bracelet_link", tool=tool)
115 return env, robot
116
117
118def jnt(values: list[float]) -> kdl.JntArray:
119 q = kdl.JntArray(len(values))
120 for i, value in enumerate(values):
121 q[i] = value
122 return q
123
124
125def as_list(q: kdl.JntArray) -> list[float]:
126 return [q[i] for i in range(q.rows())]
127
128
129def clamp(value: float, low: float, high: float) -> float:
130 return max(low, min(high, value))
131
132
133def clamp_joint(value: float, limit: tuple[float, float]) -> float:
134 low, high = limit
135 if math.isfinite(low) and math.isfinite(high) and high > low:
136 return clamp(value, low, high)
137 return value
138
139
140def joint_limit_arrays(robot: mjk.Robot) -> tuple[kdl.JntArray, kdl.JntArray]:
141 q_min = kdl.JntArray(robot.n_joints)
142 q_max = kdl.JntArray(robot.n_joints)
143 for i, (low, high) in enumerate(robot.joint_limits):
144 if math.isfinite(low) and math.isfinite(high) and high > low:
145 q_min[i], q_max[i] = low, high
146 else:
147 q_min[i], q_max[i] = -2.0 * math.pi, 2.0 * math.pi
148 return q_min, q_max
149
150
151def build_waypoints(env: mjk.Env, robot: mjk.Robot) -> dict[str, list[float]]:
152 chain = robot.kdl_chain()
153 n = robot.n_joints
154 fk = kdl.ChainFkSolverPos_recursive(chain)
155 q_min, q_max = joint_limit_arrays(robot)
156 ik_vel = kdl.ChainIkSolverVel_pinv(chain)
157 ik_nr = kdl.ChainIkSolverPos_NR_JL(chain, q_min, q_max, fk, ik_vel, 2000, 1e-5)
158 ik_lma = kdl.ChainIkSolverPos_LMA(chain, 1e-5, 2000)
159
160 q_home = jnt(HOME)
161 home_fk = kdl.Frame()
162 fk.JntToCart(q_home, home_fk)
163 # Carry the bottle at the home orientation, tilted slightly forward.
164 carry_tcp = home_fk.M * kdl.Rotation.RotY(-0.05)
165
166 world_T_base = kdl.Frame(kdl.Rotation.Identity(), kdl.Vector(ROBOT_BACK_X, 0.0, TABLE_Z))
167 base_T_world = world_T_base.Inverse()
168
169 # Constant TCP->outlet offset, measured at the live home configuration.
170 robot.set_joint_pos(HOME, call_forward=False)
171 world_T_outlet = env.site_frame("pour_outlet")
172 world_T_tcp = env.site_frame("g_pinch")
173 tcp_outlet = world_T_tcp.Inverse() * world_T_outlet.p
174
175 def outlet_target_to_tcp_target(tcp_rot: kdl.Rotation, outlet_pos: kdl.Vector) -> kdl.Frame:
176 return kdl.Frame(tcp_rot, outlet_pos - tcp_rot * tcp_outlet)
177
178 def solve(name: str, seed_values: list[float], outlet_pos: kdl.Vector) -> list[float]:
179 target = base_T_world * outlet_target_to_tcp_target(carry_tcp, outlet_pos)
180 seed = jnt(seed_values)
181 out = kdl.JntArray(n)
182 ok = ik_nr.CartToJnt(seed, target, out) >= 0
183 if not ok:
184 ok = ik_lma.CartToJnt(seed, target, out) >= 0
185 if not ok:
186 raise RuntimeError(f"IK failed for {name}")
187 fk_out = kdl.Frame()
188 fk.JntToCart(out, fk_out)
189 if (target.p - fk_out.p).Norm() > IK_TOL:
190 raise RuntimeError(f"IK pose error for {name}")
191 return as_list(out)
192
193 q_pre_pour = solve("pre-pour", HOME, kdl.Vector(JUG_X, JUG_Y, TABLE_Z + 0.27))
194 q_pour = solve("pour", q_pre_pour, kdl.Vector(JUG_X, JUG_Y, TABLE_Z + 0.20))
195 q_retreat = solve("retreat", q_pour, kdl.Vector(RETREAT_X, RETREAT_Y, TABLE_Z + 0.27))
196 q_tilt = q_pour[:]
197 q_tilt[-1] = clamp_joint(q_tilt[-1] + POUR_TILT_RAD, robot.joint_limits[-1])
198 return {
199 "home": HOME[:],
200 "pre_pour": q_pre_pour,
201 "pour": q_pour,
202 "tilt": q_tilt,
203 "retreat": q_retreat,
204 }
205
206
207def apply_pd_gravity(robot: mjk.Robot, target: list[float]) -> None:
208 robot.update()
209 gravity = robot.gravity_torques(-9.81)
210 robot.jnt_trq_cmd = [
211 KP[i] * (target[i] - robot.jnt_pos_msr[i]) - KD[i] * robot.jnt_vel_msr[i] + gravity[i]
212 for i in range(robot.n_joints)
213 ]
214
215
216def max_abs_joint_err(robot: mjk.Robot, target: list[float]) -> float:
217 return max(abs(target[i] - robot.jnt_pos_msr[i]) for i in range(robot.n_joints))
218
219
220def lerp(start: list[float], target: list[float], alpha: float) -> list[float]:
221 return [a + alpha * (b - a) for a, b in zip(start, target)]
222
223
224def place_balls_in_bottle(env: mjk.Env, robot: mjk.Robot) -> None:
225 robot.set_joint_pos(HOME, call_forward=True)
226 center = env.site_frame("pour_center")
227 spacing = 2.0 * BALL_RADIUS
228 for i in range(NUM_BALLS):
229 layer = i // 9
230 slot = i % 9
231 ix = float(slot % 3) - 1.0
232 iy = float(slot // 3) - 1.0
233 local = kdl.Vector(ix * spacing, iy * spacing, -0.026 + layer * spacing)
234 world = center * local
235 env.set_body_pose(f"grain_{i:02d}", [world.x(), world.y(), world.z()])
236
237
238def balls_in_receiver(env: mjk.Env) -> tuple[int, list[float]]:
239 count = 0
240 centroid = [0.0, 0.0, 0.0]
241 for i in range(NUM_BALLS):
242 frame = env.body_frame(f"grain_{i:02d}")
243 pos = [frame.p.x(), frame.p.y(), frame.p.z()]
244 centroid = [a + b for a, b in zip(centroid, pos)]
245 if (
246 abs(pos[0] - JUG_X) < 0.040
247 and abs(pos[1] - JUG_Y) < 0.040
248 and TABLE_Z + 0.004 < pos[2] < TABLE_Z + 0.13
249 ):
250 count += 1
251 return count, [value / NUM_BALLS for value in centroid]
252
253
254def step_once(robot: mjk.Robot, viewer: mjk.SimulateViewer | None) -> bool:
255 if viewer is not None:
256 return viewer.step()
257 return robot.step()
258
259
261 env: mjk.Env,
262 robot: mjk.Robot,
263 phase: Phase,
264 viewer: mjk.SimulateViewer | None,
265 recorder: mjk.VideoRecorder | None,
266 record_every: int,
267 step_counter: list[int],
268 state: dict,
269) -> bool:
270 print(f"State: {phase.name}")
271 robot.update()
272 start = robot.jnt_pos_msr[:]
273 t0 = env.time()
274 while True:
275 elapsed = env.time() - t0
276 alpha = clamp(elapsed / phase.duration, 0.0, 1.0) if phase.duration > 0.0 else 1.0
277 apply_pd_gravity(robot, lerp(start, phase.target, alpha))
278 if env.has_actuator("g_fingers_actuator"):
279 env.set_actuator_ctrl("g_fingers_actuator", phase.gripper)
280
281 done_time = elapsed >= phase.duration
282 done_pose = phase.settle_tol < 0.0 or max_abs_joint_err(robot, phase.target) <= phase.settle_tol
283 done_timeout = phase.timeout > 0.0 and elapsed >= phase.timeout
284 if (done_time and done_pose) or done_timeout:
285 return True
286 if viewer is not None and not viewer.is_running():
287 return False
288 if not step_once(robot, viewer):
289 return False
290 if viewer is not None and env.time() < state["prev"] - 1e-6: # UI reset pressed
291 env.reset()
292 state["prev"] = env.time()
293 raise ResetRequested()
294 state["prev"] = env.time()
295 step_counter[0] += 1
296 if recorder is not None and step_counter[0] % record_every == 0:
297 recorder.record_frame()
298
299
300def main() -> int:
301 parser = argparse.ArgumentParser()
302 parser.add_argument("--gui", action="store_true")
303 parser.add_argument("--record", nargs="?", const="table_pour.mp4")
304 parser.add_argument("--headless", action="store_true", help=argparse.SUPPRESS)
305 args = parser.parse_args()
306
307 env, robot = build_env()
308 recorder = None
309 try:
310 robot.ctrl_mode = mjk.CtrlMode.TORQUE
311
312 def on_reset(ctx):
313 place_balls_in_bottle(env, robot) # also re-homes the arm
314 if env.has_actuator("g_fingers_actuator"):
315 env.set_actuator_ctrl("g_fingers_actuator", 255.0)
316
317 env.on_reset = on_reset
318 env.reset()
319
320 waypoints = build_waypoints(env, robot)
321 phases = [
322 Phase("HOME", waypoints["home"], 0.8, 2.0, 0.08, 255.0),
323 Phase("PRE_POUR", waypoints["pre_pour"], 2.0, 4.0, 0.08, 255.0),
324 Phase("POUR", waypoints["pour"], 1.8, 4.0, 0.07, 255.0),
325 Phase("TILT", waypoints["tilt"], 3.0, 5.0, 0.07, 255.0),
326 Phase("POUR_HOLD", waypoints["tilt"], 2.5 if not args.gui else 10.0, 0.0, -1.0, 255.0),
327 Phase("RETREAT", waypoints["retreat"], 1.6, 3.0, 0.08, 255.0),
328 Phase("HOLD", waypoints["retreat"], 1.0 if not args.gui else 10.0, 0.0, -1.0, 255.0),
329 ]
330
331 fps = 60
332 record_every = max(1, int(1.0 / (fps * env.timestep())))
333 if args.record:
334 recorder = mjk.VideoRecorder.open_preset(
335 env, args.record, mjk.VideoResolution.R1080p, fps
336 )
337 step_counter = [0]
338 state = {"prev": env.time()}
339 if args.gui:
340 viewer = mjk.SimulateViewer.open(robot, "ex_table_pour.py")
341 try:
342 while viewer.is_running():
343 try:
344 for phase in phases:
345 if not run_phase(
346 env, robot, phase, viewer, recorder, record_every, step_counter, state
347 ):
348 raise StopIteration
349 break
350 except ResetRequested:
351 continue
352 except StopIteration:
353 break
354 finally:
355 viewer.close()
356 else:
357 for phase in phases:
358 if not run_phase(
359 env, robot, phase, None, recorder, record_every, step_counter, state
360 ):
361 break
362 in_receiver, centroid = balls_in_receiver(env)
363 print(f"balls in transparent receiver: {in_receiver}/{NUM_BALLS}")
364 print(f"grain centroid: {[round(v, 3) for v in centroid]} receiver center={[JUG_X, JUG_Y]}")
365 if recorder is not None:
366 print(f"recorded: {args.record}")
367 finally:
368 if recorder is not None:
369 recorder.close()
370 env.close()
371 return 0
372
373
374if __name__ == "__main__":
375 raise SystemExit(main())
tuple[mjk.Env, mjk.Robot] build_env()
bool step_once(mjk.Robot robot, mjk.SimulateViewer|None viewer)
tuple[int, list[float]] balls_in_receiver(mjk.Env env)
float clamp_joint(float value, tuple[float, float] limit)
list[float] lerp(list[float] start, list[float] target, float alpha)
mjk.AttachmentSpec bottle_attachment(str bottle_path)
mjk.AttachmentSpec gripper_attachment(str gripper_path)
mjk.SceneObject ball_object(int index)
bool run_phase(mjk.Env env, mjk.Robot robot, Phase phase, mjk.SimulateViewer|None viewer, mjk.VideoRecorder|None recorder, int record_every, list[int] step_counter, dict state)
dict[str, list[float]] build_waypoints(mjk.Env env, mjk.Robot robot)
float clamp(float value, float low, float high)
kdl.JntArray jnt(list[float] values)
None place_balls_in_bottle(mjk.Env env, mjk.Robot robot)
mjk.SceneObject receiver_object(str receiver_path)
mjk.SceneObject table_object(str table_path)
tuple[kdl.JntArray, kdl.JntArray] joint_limit_arrays(mjk.Robot robot)
float max_abs_joint_err(mjk.Robot robot, list[float] target)
list[float] as_list(kdl.JntArray q)
None apply_pd_gravity(mjk.Robot robot, list[float] target)