Mujoco KDL Wrapper  0.5.1
MuJoCo + KDL bridge for robot kinematics and dynamics
Loading...
Searching...
No Matches
ex_achd_pick_place.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""Table pick-place with ACHD + RNEA; Python counterpart of src/examples/ex_achd_pick_place.cpp."""
3
4from __future__ import annotations
5
6import argparse
7import math
8
9import PyKDL as kdl
10
11import mj_kdl_wrapper as mjk
12
13HOME = [0.0, 0.2618, 3.1416, -2.2689, 0.0, 0.9599, 1.5708]
14SURFACE_Z = 0.70
15CUBE_HS = 0.02
16PICK = (0.40, 0.00)
17PLACE = (0.40, 0.24)
18CUBE_START = [PICK[0], PICK[1], SURFACE_Z + CUBE_HS]
19GRIPPER_CLOSED = 0.82 # [rad] top of the 2F-85's ctrlrange (its driver joint stops at 0.8)
20MAX_PLACE_ERR = 0.005 # [m] in the table plane
21MAX_ELBOW_DROP = 0.10 # [m] below the support reference
22
23# With alpha = I_6 these are the desired TCP linear/angular accelerations.
24KP_LIN, KI_LIN, KD_LIN = 200.0, 100.0, 40.0
25KP_ROT, KI_ROT, KD_ROT = 120.0, 50.0, 80.0
26BETA_LIN_MAX, BETA_ROT_MAX = 120.0, 80.0
27INTEGRAL_MAX = 0.5
28
29# An ACHD-only upward wrench on this link keeps the elbow from dropping in every phase.
30SUPPORT_LINK = "half_arm_2_link"
31SUPPORT_KP, SUPPORT_KD, SUPPORT_F_MAX, SUPPORT_LIFT = 800.0, 80.0, 45.0, 0.06
32
33
34class ResetRequested(Exception):
35 """Raised after a simulate UI reset, to restart the sequence."""
36
37
38def build_env() -> tuple[mjk.Env, mjk.Robot]:
39 table = mjk.SceneObject()
40 table.name = "table"
41 table.mjcf_path = str(mjk.ASSETS_DIR / "table.xml")
42 table.pos = [0.0, 0.0, SURFACE_Z]
43 table.fixed = True
44 cube = mjk.SceneObject()
45 cube.name = "cube"
46 cube.shape = mjk.Shape.BOX
47 cube.size = [CUBE_HS, CUBE_HS, CUBE_HS]
48 cube.pos = CUBE_START[:]
49 cube.rgba = [0.1, 0.35, 1.0, 1.0]
50 cube.mass = 0.1
51 cube.condim = mjk.Condim.Torsional
52 cube.friction = [0.8, 0.02, 0.001]
53 gripper = mjk.AttachmentSpec()
54 gripper.mjcf_path = str(mjk.ASSETS_DIR / "robotiq_2f85/2f85.xml")
55 gripper.attach_to = mjk.AttachTarget(mjk.AttachKind.Site, "pinch_site")
56 gripper.prefix = "g_"
57 robot_spec = mjk.RobotSpec()
58 robot_spec.path = str(mjk.ASSETS_DIR / "kinova_gen3/gen3.xml")
59 robot_spec.pos = [0.0, 0.0, SURFACE_Z]
60 robot_spec.attachments = [gripper]
61
62 spec = mjk.SceneSpec()
63 spec.timestep = 0.002
64 spec.add_floor = True
65 spec.add_skybox = True
66 spec.objects = [table, cube]
67 spec.robots = [robot_spec]
68 env = mjk.Env.build(spec)
69 tool = mjk.ToolFrameSpec()
70 tool.tool_body = "g_base_mount"
71 tool.tcp_site = "g_pinch"
72 return env, env.create_robot("base_link", "bracelet_link", tool=tool)
73
74
75def jnt(values) -> kdl.JntArray:
76 out = kdl.JntArray(len(values))
77 for i, v in enumerate(values):
78 out[i] = v
79 return out
80
81
82def clamp_abs(value: float, limit: float) -> float:
83 return max(-limit, min(limit, value))
84
85
86def smoothstep(value: float) -> float:
87 t = max(0.0, min(1.0, value))
88 return t * t * (3.0 - 2.0 * t)
89
90
91class Achd:
92 """ACHD qddot for a TCP target, priced by RNEA into the robot's torque command."""
93
94 def __init__(self, robot: mjk.Robot, gravity_z: float, dt: float):
95 self.robot, self.dt = robot, dt
96 # KDL solvers keep a reference to the chain, so it must outlive them.
97 self.chain = chain = robot.kdl_chain()
98 self.n, self.n_seg = robot.n_joints, chain.getNrOfSegments()
99 self.fk = kdl.ChainFkSolverPos_recursive(chain)
100 self.fk_vel = kdl.ChainFkSolverVel_recursive(chain)
101 root_acc = kdl.Twist(kdl.Vector(0.0, 0.0, -gravity_z), kdl.Vector.Zero())
102 self.achd = kdl.ChainHdSolver_Vereshchagin(chain, root_acc, 6)
103 self.rnea = kdl.ChainIdSolver_RNE(chain, kdl.Vector(0.0, 0.0, gravity_z))
104 self.alpha = kdl.Jacobian(6)
105 for i in range(6):
106 self.alpha[i, i] = 1.0
107 self.f_zero = [kdl.Wrench.Zero() for _ in range(self.n_seg)]
108 self.support = -1
109 for i in range(self.n_seg):
110 if chain.getSegment(i).getName() == SUPPORT_LINK:
111 self.support = i
112 if self.support < 0:
113 raise RuntimeError(f"support segment not found: {SUPPORT_LINK}")
114 self.new_phase()
115
116 def new_phase(self) -> None:
117 self.err_i = [0.0] * 6
118 self.err_prev = None
119
120 def measure(self) -> kdl.Frame:
121 self.q, self.qd = jnt(self.robot.jnt_pos_msr), jnt(self.robot.jnt_vel_msr)
122 self.tcp = kdl.Frame()
123 self.fk.JntToCart(self.q, self.tcp)
124 return self.tcp
125
126 def link_z(self) -> float:
127 frame = kdl.Frame()
128 self.fk.JntToCart(self.q, frame, self.support + 1)
129 return SURFACE_Z + frame.p.z()
130
131 def control(self, target: kdl.Frame, target_twist: kdl.Twist, f_ext: list) -> None:
132 tcp_vel = kdl.FrameVel()
133 self.fk_vel.JntToCart(kdl.JntArrayVel(self.q, self.qd), tcp_vel)
134 w = tcp_vel.deriv()
135 err = kdl.diff(self.tcp, target)
136 dt = self.dt
137 e = [err.vel.x(), err.vel.y(), err.vel.z(), err.rot.x(), err.rot.y(), err.rot.z()]
138 prev = self.err_prev or e
139 self.err_prev = e
140 # The rotation is damped against the measured rate: the target's own turn is no error.
141 d_rot = [
142 target_twist.rot.x() - w.rot.x(),
143 target_twist.rot.y() - w.rot.y(),
144 target_twist.rot.z() - w.rot.z(),
145 ]
146 beta = kdl.JntArray(6)
147 for i in range(6):
148 self.err_i[i] = clamp_abs(self.err_i[i] + e[i] * dt, INTEGRAL_MAX)
149 if i < 3:
150 accel = KP_LIN * e[i] + KI_LIN * self.err_i[i] + KD_LIN * (e[i] - prev[i]) / dt
151 beta[i] = clamp_abs(accel, BETA_LIN_MAX)
152 else:
153 accel = KP_ROT * e[i] + KI_ROT * self.err_i[i] + KD_ROT * d_rot[i - 3]
154 beta[i] = clamp_abs(accel, BETA_ROT_MAX)
155 qdd, ff, constraint_tau, tau = (kdl.JntArray(self.n) for _ in range(4))
156 q, qd, alpha = self.q, self.qd, self.alpha
157 if self.achd.CartToJnt(q, qd, qdd, alpha, beta, f_ext, ff, constraint_tau) < 0:
158 raise RuntimeError("PyKDL ACHD failed")
159 if self.rnea.CartToJnt(self.q, self.qd, qdd, self.f_zero, tau) < 0:
160 raise RuntimeError("PyKDL RNEA failed")
161 self.robot.jnt_trq_cmd = [tau[i] for i in range(self.n)]
162
163
164def run_phase(env, ctrl: Achd, fingers, phase: dict, state: dict) -> bool:
165 """One update() per step: it reads the state and applies the previous cycle's command."""
166 print(f"State: {phase['name']}")
167 t0 = env.data.time
168 phase_start = ctrl.measure()
169 support = state["support"]
170 if phase["name"] == "PLACE_ABOVE" or not support["valid"]:
171 support.update(z_ref=ctrl.link_z() + SUPPORT_LIFT, valid=True)
172 support["prev_z"] = support["z_ref"]
173 ctrl.new_phase()
174 prev_target = phase_start
175 while True:
176 env.update()
177 tcp = ctrl.measure()
178 elapsed = env.data.time - t0
179 s = smoothstep(elapsed / phase["duration"]) if phase["duration"] > 0.0 else 1.0
180 target = kdl.addDelta(phase_start, kdl.diff(phase_start, phase["target"]), s)
181 target_twist = kdl.diff(prev_target, target, ctrl.dt)
182 prev_target = target
183
184 f_ext = [kdl.Wrench.Zero() for _ in range(ctrl.n_seg)]
185 z = ctrl.link_z()
186 vz = (z - support["prev_z"]) / ctrl.dt
187 support["prev_z"] = z
188 support["drop"] = max(support["drop"], support["z_ref"] - SUPPORT_LIFT - z)
189 fz = min(SUPPORT_F_MAX, max(0.0, SUPPORT_KP * (support["z_ref"] - z) - SUPPORT_KD * vz))
190 f_ext[ctrl.support] = kdl.Wrench(kdl.Vector(0.0, 0.0, fz), kdl.Vector.Zero())
191 ctrl.control(target, target_twist, f_ext)
192 fingers.ctrl[0] = phase["gripper"]
193
194 err = kdl.diff(tcp, phase["target"])
195 settled = phase["pos_tol"] < 0.0 or (
196 err.vel.Norm() <= phase["pos_tol"] and err.rot.Norm() <= phase["rot_tol"]
197 )
198 if (elapsed >= phase["duration"] and settled) or elapsed >= phase["timeout"]:
199 return True
200 if not env.step():
201 return False
202 if state["reset"]:
203 state["reset"] = False
204 raise ResetRequested()
205 env.pace()
206
207
208def build_phases(robot: mjk.Robot, home_tcp: kdl.Frame) -> list[dict]:
209 grasp_rot = robot.tip_T_tcp.M
210 z_grasp = CUBE_HS
211 z_above = z_grasp + 0.20
212 z_lift = z_grasp + 0.30
213
214 def at(xy, z: float) -> kdl.Frame:
215 return kdl.Frame(grasp_rot, kdl.Vector(xy[0], xy[1], z))
216
217 def phase(name, target, duration, timeout, pos_tol, rot_tol, gripper) -> dict:
218 return dict(
219 name=name,
220 target=target,
221 duration=duration,
222 timeout=timeout,
223 pos_tol=pos_tol,
224 rot_tol=rot_tol,
225 gripper=gripper,
226 )
227
228 closed = GRIPPER_CLOSED
229 return [
230 phase("HOME", home_tcp, 1.0, 2.5, 0.03, 0.05, 0.0),
231 phase("PICK_ABOVE", at(PICK, z_above), 8.0, 14.0, 0.04, 0.03, 0.0),
232 phase("PICK", at(PICK, z_grasp), 5.0, 12.0, 0.02, 0.03, 0.0),
233 phase("CLOSE", at(PICK, z_grasp), 1.5, 2.5, -1.0, -1.0, closed),
234 phase("LIFT", at(PICK, z_lift), 3.0, 8.0, 0.04, 0.03, closed),
235 phase("PLACE_ABOVE", at(PLACE, z_above), 5.0, 12.0, 0.04, 0.03, closed),
236 phase("PLACE", at(PLACE, z_grasp), 5.0, 14.0, 0.02, 0.03, closed),
237 phase("OPEN", at(PLACE, z_grasp), 1.0, 2.0, -1.0, -1.0, 0.0),
238 phase("RETREAT", at(PLACE, z_above), 3.0, 6.0, 0.04, 0.08, 0.0),
239 phase("HOLD", at(PLACE, z_above), 4.0, 4.0, -1.0, -1.0, 0.0),
240 ]
241
242
243def main() -> int:
244 parser = argparse.ArgumentParser()
245 parser.add_argument("--gui", action="store_true")
246 args = parser.parse_args()
247
248 env, robot = build_env()
249 try:
250 robot.set_control_mode(mjk.CtrlMode.TORQUE)
251 fingers = env.data.actuator("g_fingers_actuator")
252 gravity_z = env.spec.gravity_z
253 ctrl = Achd(robot, gravity_z, env.model.opt.timestep)
254 dyn = kdl.ChainDynParam(ctrl.chain, kdl.Vector(0.0, 0.0, gravity_z))
255 home_tcp = kdl.Frame()
256 ctrl.fk.JntToCart(jnt(HOME), home_tcp)
257 phases = build_phases(robot, home_tcp)
258 state = {"reset": False}
259
260 def on_reset(ctx):
261 robot.set_joint_pos(HOME)
262 env.set_body_pose("cube", CUBE_START)
263 fingers.ctrl[0] = 0.0
264 g = kdl.JntArray(robot.n_joints)
265 dyn.JntToGravity(jnt(HOME), g)
266 robot.jnt_trq_cmd = [g[i] for i in range(robot.n_joints)]
267 state["reset"] = True
268
269 env.on_reset = on_reset
270 env.reset()
271 if args.gui:
272 env.open_viewer("ex_achd_pick_place.py")
273
274 completed = False
275 while not completed:
276 state.update(
277 reset=False, support={"valid": False, "z_ref": 0.0, "prev_z": 0.0, "drop": 0.0}
278 )
279 try:
280 completed = all(run_phase(env, ctrl, fingers, p, state) for p in phases)
281 break
282 except ResetRequested:
283 continue
284 env.update()
285
286 cube = env.body_frame("cube").p
287 place_err = math.hypot(cube.x() - PLACE[0], cube.y() - PLACE[1])
288 on_table = abs(cube.z() - (SURFACE_Z + CUBE_HS)) < 0.002
289 print(
290 f"cube final position: [{cube.x():.4f}, {cube.y():.4f}, {cube.z():.4f}] "
291 f"place error {place_err * 1000:.4f} mm (limit {MAX_PLACE_ERR * 1000} mm)"
292 + ("" if on_table else ", not on the table")
293 )
294 drop = state["support"]["drop"]
295 print(f"elbow drop: {drop * 1000:.4f} mm (limit {MAX_ELBOW_DROP * 1000} mm)")
296 finally:
297 env.close()
298 if args.gui:
299 return 0
300 ok = completed and on_table and place_err <= MAX_PLACE_ERR and drop <= MAX_ELBOW_DROP
301 print(f"{'PASS' if ok else 'FAIL'}: the cube was placed with the elbow held up")
302 return 0 if ok else 1
303
304
305if __name__ == "__main__":
306 raise SystemExit(main())
None control(self, kdl.Frame target, kdl.Twist target_twist, list f_ext)
__init__(self, mjk.Robot robot, float gravity_z, float dt)
list[dict] build_phases(mjk.Robot robot, kdl.Frame home_tcp)
float smoothstep(float value)
float clamp_abs(float value, float limit)
kdl.JntArray jnt(values)
tuple[mjk.Env, mjk.Robot] build_env()
bool run_phase(env, Achd ctrl, fingers, dict phase, dict state)