17HOME = [0.0, 0.2618, 3.1416, -2.2689, 0.0, 0.9599, 1.5708]
33KP = [120.0, 220.0, 120.0, 220.0, 110.0, 190.0, 90.0]
34KD = [12.0, 22.0, 12.0, 22.0, 11.0, 18.0, 9.0]
85 ball = mjk.SceneObject()
86 ball.name = f
"grain_{index:02d}"
87 ball.shape = mjk.Shape.SPHERE
88 ball.size = [BALL_RADIUS, 0.0, 0.0]
89 ball.pos = [0.0, 0.0, TABLE_Z + 0.40 + index * 2.0 * BALL_RADIUS]
90 ball.rgba = [1.0, 0.84, 0.30, 1.0]
92 ball.condim = mjk.Condim.Torsional
93 ball.friction = [0.5, 0.02, 0.001]
98 spec = mjk.SceneSpec()
100 spec.add_floor =
True
101 spec.add_skybox =
True
108 robot_spec = mjk.RobotSpec()
109 robot_spec.path = str(mjk.ASSETS_DIR /
"kinova_gen3/gen3.xml")
110 robot_spec.pos = [ROBOT_BACK_X, 0.0, TABLE_Z]
111 robot_spec.attachments = [
115 spec.robots = [robot_spec]
117 env = mjk.Env.build(spec)
118 tool = mjk.ToolFrameSpec()
119 tool.tool_body =
"g_base_mount"
120 tool.tcp_site =
"g_pinch"
121 robot = env.create_robot(
"base_link",
"bracelet_link", tool=tool)
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
147 q_min[i], q_max[i] = -2.0 * math.pi, 2.0 * math.pi
152 chain = robot.kdl_chain()
154 fk = kdl.ChainFkSolverPos_recursive(chain)
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)
161 home_fk = kdl.Frame()
162 fk.JntToCart(q_home, home_fk)
164 carry_tcp = home_fk.M * kdl.Rotation.RotY(-0.05)
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()
170 robot.set_joint_pos(HOME)
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
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)
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
184 ok = ik_lma.CartToJnt(seed, target, out) >= 0
186 raise RuntimeError(f
"IK failed for {name}")
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}")
194 def tilted(q_pour: list[float]) -> list[float]:
196 q_tilt[-1] += POUR_TILT_RAD
199 q_pre_pour = solve(
"pre-pour", HOME, kdl.Vector(JUG_X, JUG_Y, TABLE_Z + 0.27))
200 pour_pos = kdl.Vector(JUG_X, JUG_Y, TABLE_Z + 0.20)
201 q_pour = solve(
"pour", q_pre_pour, pour_pos)
202 q_tilt = tilted(q_pour)
205 robot.set_joint_pos(q_tilt)
206 outlet = env.site_frame(
"pour_outlet").p
207 err = kdl.Vector(JUG_X, JUG_Y, TILT_OUTLET_Z) - outlet
208 if err.Norm() < 5e-3:
210 pour_pos = pour_pos + err
211 q_pour = solve(
"pour", q_pre_pour, pour_pos)
212 q_tilt = tilted(q_pour)
213 q_retreat = solve(
"retreat", q_pour, kdl.Vector(RETREAT_X, RETREAT_Y, TABLE_Z + 0.27))
216 "pre_pour": q_pre_pour,
219 "retreat": q_retreat,
246 robot.set_joint_pos(HOME)
247 center = env.site_frame(
"pour_center")
248 spacing = 2.0 * BALL_RADIUS
249 for i
in range(NUM_BALLS):
252 ix = float(slot % 3) - 1.0
253 iy = float(slot // 3) - 1.0
254 local = kdl.Vector(ix * spacing, iy * spacing, -0.026 + layer * spacing)
255 world = center * local
256 env.set_body_pose(f
"grain_{i:02d}", [world.x(), world.y(), world.z()])
261 centroid = [0.0, 0.0, 0.0]
262 for i
in range(NUM_BALLS):
263 frame = env.body_frame(f
"grain_{i:02d}")
264 pos = [frame.p.x(), frame.p.y(), frame.p.z()]
265 centroid = [a + b
for a, b
in zip(centroid, pos)]
267 abs(pos[0] - JUG_X) < JUG_RADIUS - 0.012
268 and abs(pos[1] - JUG_Y) < JUG_RADIUS - 0.012
269 and TABLE_Z + 0.006 < pos[2] < TABLE_Z + JUG_HEIGHT + 0.04
272 return count, [value / NUM_BALLS
for value
in centroid]
275def run_phase(env, robot, fingers, dyn, phase: Phase, state: dict) -> bool:
276 """One update() per step: it reads the state and applies the previous cycle's command."""
277 print(f
"State: {phase.name}")
278 start = robot.jnt_pos_msr[:]
282 elapsed = env.data.time - t0
283 alpha =
clamp(elapsed / phase.duration, 0.0, 1.0)
if phase.duration > 0.0
else 1.0
285 fingers.ctrl[0] = phase.gripper
288 done_pose = phase.settle_tol < 0.0
or err <= phase.settle_tol
289 if (elapsed >= phase.duration
and done_pose)
or elapsed >= phase.timeout:
294 state[
"reset"] =
False
298 recorder = state[
"recorder"]
299 if recorder
is not None and state[
"steps"] % state[
"record_every"] == 0:
300 if not recorder.record_frame():
301 raise RuntimeError(f
"record_frame() failed at step {state['steps']}")
305 parser = argparse.ArgumentParser()
306 parser.add_argument(
"--gui", action=
"store_true")
307 parser.add_argument(
"--record", nargs=
"?", const=
"table_pour.mp4")
308 args = parser.parse_args()
313 robot.set_control_mode(mjk.CtrlMode.TORQUE)
314 fingers = env.data.actuator(
"g_fingers_actuator")
315 chain = robot.kdl_chain()
316 dyn = kdl.ChainDynParam(chain, kdl.Vector(0.0, 0.0, env.spec.gravity_z))
318 state = {
"reset":
False,
"steps": 0,
"recorder":
None}
319 state[
"record_every"] = max(1, int(1.0 / (fps * env.model.opt.timestep)))
323 fingers.ctrl[0] = GRIPPER_CLOSED
324 robot.jnt_trq_cmd =
gravity(dyn, HOME)
325 state[
"reset"] =
True
327 env.on_reset = on_reset
331 state[
"reset"] =
False
334 Phase(
"HOME", waypoints[
"home"], 1.0, 2.5, 0.08, g),
335 Phase(
"PRE_POUR", waypoints[
"pre_pour"], 4.0, 6.5, 0.08, g),
336 Phase(
"POUR", waypoints[
"pour"], 3.5, 5.5, 0.07, g),
337 Phase(
"TILT", waypoints[
"tilt"], 7.0, 10.0, 0.07, g),
338 Phase(
"POUR_HOLD", waypoints[
"tilt"], 9.0, 9.0, -1.0, g),
339 Phase(
"RETREAT", waypoints[
"retreat"], 2.0, 4.0, 0.08, g),
340 Phase(
"HOLD", waypoints[
"retreat"], 1.0, 1.0, -1.0, g),
344 recorder = mjk.VideoRecorder.open_preset(
345 env, args.record, mjk.VideoResolution.R1080p, fps
347 state[
"recorder"] = recorder
349 env.open_viewer(
"ex_table_pour.py")
353 completed = all(
run_phase(env, robot, fingers, dyn, p, state)
for p
in phases)
355 except ResetRequested:
360 print(f
"balls in transparent receiver: {in_receiver}/{NUM_BALLS} (min {MIN_IN_RECEIVER})")
361 print(f
"grain centroid: {[round(v, 3) for v in centroid]} receiver center={[JUG_X, JUG_Y]}")
362 if recorder
is not None:
363 print(f
"recorded: {args.record}")
365 if recorder
is not None:
370 ok = completed
and in_receiver >= MIN_IN_RECEIVER
371 print(f
"{'PASS' if ok else 'FAIL'}: the balls were poured into the receiver")
372 return 0
if ok
else 1