2"""Two arms facing each other across a table each pick and place a cube with RNEA computed
3torque; Python counterpart of ex_rnea_pick_place.cpp. Headless it exits 1 unless both cubes
4are placed, both elbows stay up, the free objects stay put, the arms never touch and RNEA
7from __future__
import annotations
11from dataclasses
import dataclass
15import mj_kdl_wrapper
as mjk
17HOME = [0.0, 0.2618, 3.1416, -2.2689, 0.0, 0.9599, 1.5708]
23KP = [100.0, 200.0, 100.0, 200.0, 100.0, 200.0, 100.0]
24KD = [20.0, 28.0, 20.0, 28.0, 20.0, 28.0, 20.0]
27MIN_ELBOW_HEIGHT = 0.45
31BASES = [(-0.70, -0.12, 0.0), (0.70, 0.12, math.pi)]
34 (
"red_box", mjk.Shape.BOX, 0.0, 0.42, 0.03, (1.0, 0.2, 0.2)),
35 (
"green_box", mjk.Shape.BOX, 0.0, -0.42, 0.03, (0.2, 1.0, 0.2)),
36 (
"yellow_box", mjk.Shape.BOX, -0.45, 0.42, 0.04, (1.0, 0.85, 0.1)),
37 (
"orange_sphere", mjk.Shape.SPHERE, 0.45, -0.42, 0.035, (1.0, 0.55, 0.0)),
38 (
"purple_sphere", mjk.Shape.SPHERE, 0.0, 0.0, 0.025, (0.7, 0.0, 0.9)),
42 (
"overview", [0.0, -1.3, 1.9], [0.410747, 0.0, 0.0, 0.911749], 55.0),
43 (
"side", [0.0, 1.5, 1.15], [0.0, 0.633989, 0.773342, 0.0], 50.0),
48 """Raised after a simulate UI reset, to restart the sequence."""
51@dataclass(frozen=
True)
62 x, y, yaw = BASES[arm]
63 return kdl.Frame(kdl.Rotation.RotZ(yaw), kdl.Vector(x, y, SURFACE_Z))
67 """A cube's centre resting on the table at xy in the arm's base frame, in the world frame."""
68 return world_T_base(arm) * kdl.Vector(xy[0], xy[1], CUBE_HS)
71def cube_object(name: str, spot: kdl.Vector) -> mjk.SceneObject:
72 cube = mjk.SceneObject()
74 cube.shape = mjk.Shape.BOX
75 cube.size = [CUBE_HS, CUBE_HS, CUBE_HS]
76 cube.pos = [spot.x(), spot.y(), spot.z()]
77 cube.rgba = [0.1, 0.35, 1.0, 1.0]
79 cube.condim = mjk.Condim.Torsional
80 cube.friction = [0.8, 0.02, 0.001]
84def free_object(name, shape, x, y, half, rgb) -> mjk.SceneObject:
85 obj = mjk.SceneObject()
88 box = shape == mjk.Shape.BOX
89 obj.size = [half, half
if box
else 0.0, half
if box
else 0.0]
90 obj.pos = [x, y, SURFACE_Z + half]
91 obj.rgba = [*rgb, 1.0]
93 obj.friction = [1.0, 0.005, 0.0001]
97def camera(name, pos, quat, fovy) -> mjk.CameraSpec:
98 cam = mjk.CameraSpec()
107 table = mjk.SceneObject()
109 table.mjcf_path = str(mjk.ASSETS_DIR /
"table.xml")
110 table.pos = [0.0, 0.0, SURFACE_Z]
112 gripper = mjk.AttachmentSpec()
113 gripper.mjcf_path = str(mjk.ASSETS_DIR /
"robotiq_2f85/2f85.xml")
114 gripper.attach_to = mjk.AttachTarget(mjk.AttachKind.Site,
"pinch_site")
115 gripper.prefix =
"g_"
117 for (x, y, yaw), prefix
in zip(BASES, PREFIXES):
118 robot_spec = mjk.RobotSpec()
119 robot_spec.path = str(mjk.ASSETS_DIR /
"kinova_gen3/gen3.xml")
120 robot_spec.prefix = prefix
121 robot_spec.pos = [x, y, SURFACE_Z]
122 robot_spec.quat = [0.0, 0.0, math.sin(yaw / 2.0), math.cos(yaw / 2.0)]
123 robot_spec.attachments = [gripper]
124 robot_specs.append(robot_spec)
126 spec = mjk.SceneSpec()
127 spec.timestep = 0.002
128 spec.add_floor =
True
129 spec.add_skybox =
True
130 spec.robots = robot_specs
136 spec.cameras = [
camera(*c)
for c
in CAMERAS]
137 env = mjk.Env.build(spec)
138 tool = mjk.ToolFrameSpec()
139 tool.tool_body =
"g_base_mount"
140 tool.tcp_site =
"g_pinch"
141 robots = [env.create_robot(
"base_link",
"bracelet_link", p, tool=tool)
for p
in PREFIXES]
145def jnt(values) -> kdl.JntArray:
146 q = kdl.JntArray(len(values))
147 for i, value
in enumerate(values):
152def gravity(dyn: kdl.ChainDynParam, q_values) -> list[float]:
153 g = kdl.JntArray(len(q_values))
154 dyn.JntToGravity(
jnt(q_values), g)
155 return [g[i]
for i
in range(g.rows())]
158def solve_near_seed(chain, limits, seed: list[float], target: kdl.Frame) -> list[float]:
159 """Damped-least-squares IK stepped from seed, so the answer stays on the seed's branch."""
160 fk = kdl.ChainFkSolverPos_recursive(chain)
161 ik = kdl.ChainIkSolverVel_wdls(chain, 1e-5, 150)
164 dq = kdl.JntArray(q.rows())
166 current = kdl.Frame()
167 fk.JntToCart(q, current)
168 dx = kdl.diff(current, target)
169 if dx.vel.Norm() <= 2e-3
and dx.rot.Norm() <= 2e-2:
171 if dx.vel.Norm() > 0.05:
172 dx.vel = dx.vel * (0.05 / dx.vel.Norm())
173 if dx.rot.Norm() > 0.20:
174 dx.rot = dx.rot * (0.20 / dx.rot.Norm())
175 if ik.CartToJnt(q, dx, dq) < 0:
176 raise RuntimeError(
"PyKDL IK velocity step failed")
178 for i
in range(q.rows()):
179 q[i] = min(limits[i][1], max(limits[i][0], q[i] + dq[i]))
181 current = kdl.Frame()
182 fk.JntToCart(q, current)
183 dx = kdl.diff(current, target)
184 if dx.vel.Norm() > 2e-3
or dx.rot.Norm() > 2e-2:
185 raise RuntimeError(
"PyKDL IK did not converge")
186 return [q[i]
for i
in range(q.rows())]
190 """Joint waypoints in the arm's base frame, so they serve every arm placed like this one."""
191 chain = robot.kdl_chain()
195 def solve(xy, z: float) -> list[float]:
197 target = kdl.Frame(robot.tip_T_tcp.M, kdl.Vector(xy[0], xy[1], z))
201 pick_above = solve(PICK, CUBE_HS + 0.20)
202 pick = solve(PICK, CUBE_HS)
203 lift = solve(PICK, CUBE_HS + 0.30)
204 place_above = solve(PLACE, CUBE_HS + 0.20)
205 place = solve(PLACE, CUBE_HS)
206 closed = GRIPPER_CLOSED
208 Phase(
"HOME", HOME, 1.0, 2.5, 0.08, 0.0),
209 Phase(
"PICK_ABOVE", pick_above, 5.0, 7.0, 0.08, 0.0),
210 Phase(
"PICK", pick, 5.0, 8.0, 0.03, 0.0),
211 Phase(
"CLOSE", pick, 1.5, 2.5, -1.0, closed),
212 Phase(
"LIFT", lift, 3.0, 5.0, 0.08, closed),
213 Phase(
"PLACE_ABOVE", place_above, 3.0, 5.0, 0.08, closed),
214 Phase(
"PLACE", place, 5.0, 8.0, 0.03, closed),
215 Phase(
"OPEN", place, 1.0, 2.0, -1.0, 0.0),
216 Phase(
"RETREAT", place_above, 2.0, 4.0, 0.08, 0.0),
217 Phase(
"HOLD", place_above, 1.0, 1.0, -1.0, 0.0),
223 q, qd = robot.jnt_pos_msr, robot.jnt_vel_msr
224 qdd = kdl.JntArray(n)
226 qdd[i] = KP[i] * (q_des[i] - q[i]) - KD[i] * qd[i]
227 tau = kdl.JntArray(n)
228 wrenches = [kdl.Wrench.Zero()
for _
in range(n_segments)]
229 if rnea.CartToJnt(
jnt(q),
jnt(qd), qdd, wrenches, tau) < 0:
230 raise RuntimeError(
"PyKDL RNEA failed")
231 return [tau[i]
for i
in range(n)]
235 """What a viewer sees: the elbows, the free objects and any arm-to-arm contact."""
237 for a, prefix
in enumerate(PREFIXES):
238 z = env.body_frame(prefix +
"forearm_link").p.z()
239 m[
"elbow_min"][a] = min(m[
"elbow_min"][a], z)
240 for name, rest
in state[
"rest"].items():
241 m[
"disturb"] = max(m[
"disturb"], (env.body_frame(name).p - rest).Norm())
242 model, data, roots = env.model, env.data, state[
"roots"]
243 for c
in range(data.ncon):
244 r0, r1 = (model.body_rootid[model.geom_bodyid[g]]
for g
in data.contact[c].geom)
245 if {r0, r1} == roots:
246 m[
"arm_contacts"] += 1
249def run_phase(env, robots, fingers, solvers, phase: Phase, state: dict) -> bool:
250 """One update() per step for both arms: it reads the state and applies the previous
252 print(f
"State: {phase.name}")
253 starts = [r.jnt_pos_msr[:]
for r
in robots]
257 elapsed = env.data.time - t0
258 alpha = min(1.0, max(0.0, elapsed / phase.duration))
if phase.duration > 0.0
else 1.0
260 for robot, finger, (rnea, n_seg), start
in zip(robots, fingers, solvers, starts):
261 q_des = [s + alpha * (t - s)
for s, t
in zip(start, phase.target)]
262 robot.jnt_trq_cmd =
rnea_torque(robot, rnea, n_seg, q_des)
263 finger.ctrl[0] = phase.gripper
264 err = max(err, *(abs(t - m)
for t, m
in zip(phase.target, robot.jnt_pos_msr)))
265 done_pose = phase.settle_tol < 0.0
or err <= phase.settle_tol
266 if (elapsed >= phase.duration
and done_pose)
or elapsed >= phase.timeout:
271 state[
"reset"] =
False
278 return {
"elbow_min": [math.inf, math.inf],
"disturb": 0.0,
"arm_contacts": 0}
282 parser = argparse.ArgumentParser()
283 parser.add_argument(
"--gui", action=
"store_true")
284 args = parser.parse_args()
288 print(f
"cameras: {' '.join(env.model.camera(i).name for i in range(env.model.ncam))}")
289 fingers = [env.data.actuator(p +
"g_fingers_actuator")
for p
in PREFIXES]
290 gravity_vec = kdl.Vector(0.0, 0.0, env.spec.gravity_z)
291 solvers, dyns = [], []
293 robot.set_control_mode(mjk.CtrlMode.TORQUE)
294 chain = robot.kdl_chain()
295 solvers.append((kdl.ChainIdSolver_RNE(chain, gravity_vec), chain.getNrOfSegments()))
296 dyns.append(kdl.ChainDynParam(chain, gravity_vec))
302 "rest": {name: kdl.Vector(x, y, SURFACE_Z + h)
for name, _, x, y, h, _
in FREE_OBJECTS},
303 "roots": {env.model.body_rootid[env.model.body(p +
"base_link").id]
for p
in PREFIXES},
307 for a, (robot, finger, dyn)
in enumerate(zip(robots, fingers, dyns)):
308 robot.set_joint_pos(HOME)
310 env.set_body_pose(PREFIXES[a] +
"cube", [spot.x(), spot.y(), spot.z()])
312 robot.jnt_trq_cmd =
gravity(dyn, HOME)
314 state[
"reset"] =
True
316 env.on_reset = on_reset
318 state[
"reset"] =
False
320 env.open_viewer(
"ex_rnea_pick_place.py")
325 completed = all(
run_phase(env, robots, fingers, solvers, p, state)
for p
in phases)
327 except ResetRequested:
333 for a, prefix
in enumerate(PREFIXES):
334 cube = env.body_frame(prefix +
"cube").p
336 place_err = math.hypot(cube.x() - spot.x(), cube.y() - spot.y())
337 on_table = abs(cube.z() - spot.z()) < 0.002
338 elbow_h = m[
"elbow_min"][a] - SURFACE_Z
340 f
"arm {a + 1}: cube at [{cube.x():.4f}, {cube.y():.4f}, {cube.z():.4f}] "
341 f
"place error {place_err * 1000:.4f} mm (limit {MAX_PLACE_ERR * 1000} mm)"
342 + (
"" if on_table
else ", not on the table")
343 + f
"; lowest elbow above the table {elbow_h * 1000:.4f} mm "
344 f
"(limit {MIN_ELBOW_HEIGHT * 1000} mm)"
346 ok = ok
and on_table
and place_err <= MAX_PLACE_ERR
and elbow_h >= MIN_ELBOW_HEIGHT
348 f
"largest free-object displacement: {m['disturb'] * 1000:.4f} mm "
349 f
"(limit {MAX_DISTURB * 1000} mm)"
351 print(f
"arm-to-arm contacts: {m['arm_contacts']} (limit 0)")
352 ok = ok
and m[
"disturb"] <= MAX_DISTURB
and m[
"arm_contacts"] == 0
357 print(f
"{'PASS' if ok else 'FAIL'}: both arms placed their cubes and left the rest alone")
358 return 0
if ok
else 1
361if __name__ ==
"__main__":
362 raise SystemExit(
main())
bool run_phase(env, robots, fingers, solvers, Phase phase, dict state)
kdl.Frame world_T_base(int arm)
list[float] solve_near_seed(chain, limits, list[float] seed, kdl.Frame target)
mjk.SceneObject cube_object(str name, kdl.Vector spot)
mjk.SceneObject free_object(name, shape, x, y, half, rgb)
tuple[mjk.Env, list[mjk.Robot]] build_env()
None measure(env, dict state)
list[Phase] pick_place_phases(mjk.Robot robot)
mjk.CameraSpec camera(name, pos, quat, fovy)
list[float] gravity(kdl.ChainDynParam dyn, q_values)
list[float] rnea_torque(robot, rnea, n_segments, q_des)
kdl.Vector cube_spot(int arm, xy)