2"""F/T admittance around an RNEA task-space inner loop; Python counterpart of ex_admittance_ft.cpp.
4Headless, it runs the self-check and exits 1 when a metric is out of its limit; with --gui the
5same sequence runs once, the mouse (ctrl + right-drag) pushing the tool after the helix.
8from __future__
import annotations
14import mj_kdl_wrapper
as mjk
16HOME = [0.0, 0.2618, 3.1416, -2.2689, 0.0, 0.9599, 1.5708]
20KP_LIN, KD_LIN = 2500.0, 100.0
21KP_ROT, KD_ROT = 2500.0, 100.0
22BETA_LIN_MAX, BETA_ROT_MAX = 300.0, 300.0
23KP_NULL, KD_NULL = 100.0, 20.0
26M_ADM, D_ADM, K_ADM = 8.0, 80.0, 0.0
31GRIPPER_ACTUATOR =
"g_fingers_actuator"
32FT_SITE =
"wrist_ft_site"
35HANDOFF_TARE_TIME = 1.0
37SELFCHECK_PUSH = (8.0, 12.0, 6.0)
38ELBOW_BODY =
"forearm_link"
39MIN_ELBOW_HEIGHT = 0.64
48def jnt(values: list[float]) -> kdl.JntArray:
49 out = kdl.JntArray(len(values))
50 for i, value
in enumerate(values):
55def clamp(value: float, low: float, high: float) -> float:
56 return max(low, min(high, value))
59def vadd(a: list[float], b: list[float]) -> list[float]:
60 return [a[i] + b[i]
for i
in range(3)]
63def vscale(a: list[float], s: float) -> list[float]:
64 return [s * a[i]
for i
in range(3)]
67def vclamp(a: list[float], limit: float) -> list[float]:
68 return [
clamp(x, -limit, limit)
for x
in a]
71def vnorm(a: list[float]) -> float:
72 return math.sqrt(sum(x * x
for x
in a))
75def xyz(v: kdl.Vector) -> list[float]:
76 return [v.x(), v.y(), v.z()]
79def frame_point(frame: kdl.Frame, point: kdl.Vector) -> list[float]:
80 return xyz(frame * point)
84 spec = mjk.AttachmentSpec()
85 spec.mjcf_path = str(mjk.ASSETS_DIR /
"ft_sensor.xml")
86 spec.attach_to = mjk.AttachTarget(mjk.AttachKind.Site,
"pinch_site")
91 spec = mjk.AttachmentSpec()
92 spec.mjcf_path = str(mjk.ASSETS_DIR /
"robotiq_2f85/2f85.xml")
93 spec.attach_to = mjk.AttachTarget(mjk.AttachKind.Site,
"wrist_ft_site")
99 table = mjk.SceneObject()
101 table.mjcf_path = str(mjk.ASSETS_DIR /
"table.xml")
102 table.pos = [0.0, 0.0, TABLE_Z]
109 spec = mjk.SceneSpec()
110 spec.timestep = 0.002
111 spec.add_floor =
True
112 spec.add_skybox =
True
113 spec.objects = [table]
115 robot_spec = mjk.RobotSpec()
116 robot_spec.path = str(mjk.ASSETS_DIR /
"kinova_gen3/gen3.xml")
117 robot_spec.attach_to = mjk.AttachTarget(mjk.AttachKind.Site,
"table_top")
119 spec.robots = [robot_spec]
121 env = mjk.Env.build(spec)
123 ft = mjk.ForceTorqueSensorSpec()
125 ft.frame_site = FT_SITE
127 tool = mjk.ToolFrameSpec()
128 tool.tool_body =
"g_base_mount"
129 tool.tcp_site =
"g_pinch"
130 tool.ft_sensors = [ft]
132 robot = env.create_robot(
"base_link",
"bracelet_link", tool=tool)
136def tcp_frame(robot: mjk.Robot, state: dict) -> kdl.Frame:
138 state[
"fk"].JntToCart(
jnt(robot.jnt_pos_msr), frame)
143 return [sum(jac[row, col] * qdot[col]
for col
in range(qdot.rows()))
for row
in range(6)]
146def rnea_track(env: mjk.Env, robot: mjk.Robot, state: dict, target: kdl.Frame) ->
None:
147 """Task-space computed torque: Cartesian PD -> qddot + null-space posture -> RNEA torque."""
148 q =
jnt(robot.jnt_pos_msr)
149 qdot =
jnt(robot.jnt_vel_msr)
151 err = kdl.diff(
tcp_frame(robot, state), target)
152 jac = kdl.Jacobian(robot.n_joints)
153 state[
"jac_solver"].JntToJac(q, jac)
156 qddot = kdl.JntArray(robot.n_joints)
158 beta.vel = kdl.Vector(
159 clamp(KP_LIN * err.vel.x() - KD_LIN * tcp_vel[0], -BETA_LIN_MAX, BETA_LIN_MAX),
160 clamp(KP_LIN * err.vel.y() - KD_LIN * tcp_vel[1], -BETA_LIN_MAX, BETA_LIN_MAX),
161 clamp(KP_LIN * err.vel.z() - KD_LIN * tcp_vel[2], -BETA_LIN_MAX, BETA_LIN_MAX),
163 beta.rot = kdl.Vector(
164 clamp(KP_ROT * err.rot.x() - KD_ROT * tcp_vel[3], -BETA_ROT_MAX, BETA_ROT_MAX),
165 clamp(KP_ROT * err.rot.y() - KD_ROT * tcp_vel[4], -BETA_ROT_MAX, BETA_ROT_MAX),
166 clamp(KP_ROT * err.rot.z() - KD_ROT * tcp_vel[5], -BETA_ROT_MAX, BETA_ROT_MAX),
169 z =
jnt([KP_NULL * (HOME[i] - q[i]) - KD_NULL * qdot[i]
for i
in range(robot.n_joints)])
171 beta = beta - kdl.Twist(kdl.Vector(*jz[:3]), kdl.Vector(*jz[3:]))
172 if state[
"acc_ik"].CartToJnt(q, beta, qddot) < 0:
173 raise RuntimeError(
"RNEA task acceleration solve failed")
174 for i
in range(robot.n_joints):
177 tau = kdl.JntArray(robot.n_joints)
178 wrenches = [kdl.Wrench.Zero()
for _
in range(state[
"n_seg"])]
179 if state[
"id_solver"].CartToJnt(q, qdot, qddot, wrenches, tau) < 0:
180 raise RuntimeError(
"RNEA inverse dynamics failed")
181 robot.jnt_trq_cmd = [tau[i]
for i
in range(robot.n_joints)]
185 env.data.actuator(GRIPPER_ACTUATOR).ctrl[0] = GRIPPER_CLOSED
189 """Hold home while the gripper closes, then tare: its ~10 N load only appears once closed."""
192 for _
in range(SETTLE_STEPS):
204 """External force on the tool in the world frame: the tared reading, deadbanded."""
205 wrench = robot.ft_sensor(
"wrist_ft")
206 f_world =
xyz(env.site_frame(FT_SITE).M * wrench.force)
208 f_ext = [f_world[i] - bias[i]
for i
in range(3)]
209 force_norm =
vnorm(f_ext)
210 if force_norm < FORCE_DEADBAND:
211 return [0.0, 0.0, 0.0]
215def tare_force(env: mjk.Env, robot: mjk.Robot) -> list[float]:
216 return xyz(env.site_frame(FT_SITE).M * robot.ft_sensor(
"wrist_ft").force)
221 if force == [0.0, 0.0, 0.0]:
222 state[
"vel"] = [0.0, 0.0, 0.0]
225 (force[i] - D_ADM * state[
"vel"][i] - K_ADM * state[
"offset"][i]) / M_ADM
229 state[
"offset"] =
vclamp(
vadd(state[
"offset"],
vscale(state[
"vel"], dt)), MAX_OFFSET)
233 """D_ADM times a helix's velocity: the mass-damper admittance (v = F / D) traces the helix."""
234 if t < 0.0
or t > TEACH_TIME:
235 return [0.0, 0.0, 0.0]
236 theta = 2.0 * math.pi * TEACH_TURNS * t / TEACH_TIME
237 theta_dot = 2.0 * math.pi * TEACH_TURNS / TEACH_TIME
238 vx = -TEACH_RADIUS * theta_dot * math.sin(theta)
239 vy = TEACH_RADIUS * theta_dot * math.cos(theta)
240 vz = TEACH_RISE / TEACH_TIME
241 return [D_ADM * vx, D_ADM * vy, D_ADM * vz]
245 """Force -> offset (outer loop) -> RNEA-tracked TCP target, which it returns."""
247 target = kdl.Frame(nominal.M, nominal.p + kdl.Vector(*state[
"offset"]))
252def run_gui(env: mjk.Env, robot: mjk.Robot, nominal: kdl.Frame, state: dict) ->
None:
253 """The same sequence with the viewer; after the helix the mouse pushes the tool."""
254 env.open_viewer(
"ex_admittance_ft.py")
256 viewer.set_free_camera(1.55, 145.0, -24.0, (0.05, 0.0, TABLE_Z + 0.35))
257 start = env.data.time
258 handoff_tared =
False
259 target_prev: list[float] |
None =
None
260 tcp_prev: list[float] |
None =
None
263 while env.data.time - start < TEACH_TIME + HANDOFF_TARE_TIME + GUIDE_TIME:
266 state[
"reset"] =
False
267 start = env.data.time
268 handoff_tared =
False
269 target_prev = tcp_prev =
None
271 t = env.data.time - start
277 elif t < TEACH_TIME + HANDOFF_TARE_TIME:
278 force = [0.0, 0.0, 0.0]
280 if not handoff_tared:
288 world_base = env.body_frame(
"base_link")
291 if target_prev
and trace_step % 5 == 0:
292 viewer.add_trace_segment(target_prev, target_xyz, (1.0, 0.95, 0.0, 1.0))
293 if tcp_prev
and trace_step % 5 == 0:
294 viewer.add_trace_segment(tcp_prev, tcp_xyz, (0.0, 1.0, 0.2, 1.0))
295 target_prev = target_xyz
302 env.data.body(TOOL_BODY).xfrc_applied[:] = 0.0
306 return env.body_frame(ELBOW_BODY).p.z() - TABLE_Z
309def run_selfcheck(env: mjk.Env, robot: mjk.Robot, nominal: kdl.Frame, state: dict) -> dict:
310 """The helix, the tare, then a scripted push on the tool sensed by the F/T; returns metrics."""
314 helix_track_err = 0.0
315 while env.data.time - t0 < TEACH_TIME:
316 t = env.data.time - t0
321 err = [tcp.p[i] - target.p[i]
for i
in range(3)]
322 helix_react = max(helix_react,
vnorm(state[
"offset"]))
323 helix_track_err = max(helix_track_err,
vnorm(err))
330 t_handoff = env.data.time
331 while env.data.time - t_handoff < HANDOFF_TARE_TIME:
336 err = [tcp.p[i] - target.p[i]
for i
in range(3)]
337 helix_track_err = max(helix_track_err,
vnorm(err))
348 handoff_force = max(handoff_force,
vnorm(force))
355 helix_settle_err = 0.0
356 t_settle = env.data.time
357 while env.data.time - t_settle < 0.5:
362 err = [tcp.p[i] - target.p[i]
for i
in range(3)]
363 helix_settle_err = max(helix_settle_err,
vnorm(err))
369 pre_push = state[
"offset"][:]
371 settled: list[float] |
None =
None
372 push_recovery_err: float |
None =
None
373 while env.data.time - t1 < 4.0:
374 t = env.data.time - t1
375 push = SELFCHECK_PUSH
if t < 1.0
else (0.0, 0.0, 0.0)
376 env.data.body(TOOL_BODY).xfrc_applied[:] = [*push, 0.0, 0.0, 0.0]
381 err = [tcp.p[i] - target.p[i]
for i
in range(3)]
382 if push_recovery_err
is None and t >= 2.0:
383 push_recovery_err =
vnorm(err)
385 if settled
is None and t >= 2.5:
386 settled = state[
"offset"][:]
391 env.data.body(TOOL_BODY).xfrc_applied[:] = 0.0
393 "helix_react": helix_react,
394 "helix_track_err": helix_track_err,
395 "helix_settle_err": helix_settle_err,
396 "handoff_force": handoff_force,
397 "push_response":
vnorm([(settled
or pre_push)[i] - pre_push[i]
for i
in range(3)]),
398 "push_dy": (settled
or pre_push)[1] - pre_push[1],
399 "push_recovery_err": push_recovery_err
or 0.0,
400 "hold_drift":
vnorm([state[
"offset"][i] - (settled
or pre_push)[i]
for i
in range(3)]),
401 "elbow_start": elbow_start,
402 "elbow_min": elbow_min,
407 parser = argparse.ArgumentParser()
408 parser.add_argument(
"--gui", action=
"store_true")
409 args = parser.parse_args()
413 chain = robot.kdl_chain()
414 acc_ik = kdl.ChainIkSolverVel_wdls(chain)
415 acc_ik.setLambda(0.10)
416 robot.set_control_mode(mjk.CtrlMode.TORQUE)
419 "bias": [0.0, 0.0, 0.0],
420 "offset": [0.0, 0.0, 0.0],
421 "vel": [0.0, 0.0, 0.0],
422 "fk": kdl.ChainFkSolverPos_recursive(chain),
423 "jac_solver": kdl.ChainJntToJacSolver(chain),
425 "id_solver": kdl.ChainIdSolver_RNE(chain, kdl.Vector(0.0, 0.0, -9.81)),
426 "n_seg": chain.getNrOfSegments(),
431 robot.set_joint_pos(HOME)
432 state[
"offset"] = [0.0, 0.0, 0.0]
433 state[
"vel"] = [0.0, 0.0, 0.0]
434 env.data.body(TOOL_BODY).xfrc_applied[:] = 0.0
435 state[
"reset"] =
True
437 env.on_reset = on_reset
439 state[
"reset"] =
False
444 print(f
"FT bias: [{bias[0]:.3f}, {bias[1]:.3f}, {bias[2]:.3f}] N")
446 run_gui(env, robot, nominal, state)
449 f
"[{state['offset'][0]:.4f}, {state['offset'][1]:.4f}, {state['offset'][2]:.4f}] m"
453 print(f
"helix force response (max offset): {m['helix_react']:.4f} m")
454 print(f
"helix TCP tracking error: {m['helix_track_err']:.4f} m")
455 print(f
"helix settle error: {m['helix_settle_err']:.4f} m")
456 print(f
"FT handoff residual force: {m['handoff_force']:.4f} N")
457 print(f
"FT push response (offset norm): {m['push_response']:.4f} m")
458 print(f
"FT push response (offset dY): {m['push_dy']:.4f} m")
459 print(f
"push release recovery error: {m['push_recovery_err']:.4f} m")
460 print(f
"hold drift after push released: {m['hold_drift']:.4f} m")
462 f
"elbow height start / lowest: {m['elbow_start']:.4f} / "
463 f
"{m['elbow_min']:.4f} m (limit {MIN_ELBOW_HEIGHT:.4f} m)"
466 m[
"helix_react"] > 0.10
467 and m[
"helix_track_err"] < 0.006
468 and m[
"helix_settle_err"] < 0.006
469 and m[
"handoff_force"] == 0.0
470 and m[
"push_response"] > 0.12
471 and m[
"push_recovery_err"] < 0.002
472 and m[
"hold_drift"] < 0.001
473 and m[
"elbow_min"] >= MIN_ELBOW_HEIGHT
475 goal =
"the admittance responded to the helix and the FT push and held on release"
476 print(f
"{'PASS' if ok else 'FAIL'}: {goal}")
477 return 0
if ok
else 1
483if __name__ ==
"__main__":
484 raise SystemExit(
main())
float elbow_height(mjk.Env env)
None run_gui(mjk.Env env, mjk.Robot robot, kdl.Frame nominal, dict state)
list[float] tare_force(mjk.Env env, mjk.Robot robot)
list[float] vscale(list[float] a, float s)
list[float] measured_force(mjk.Env env, mjk.Robot robot, dict state)
list[float] jacobian_twist(kdl.Jacobian jac, kdl.JntArray qdot)
list[float] vadd(list[float] a, list[float] b)
float clamp(float value, float low, float high)
kdl.JntArray jnt(list[float] values)
mjk.AttachmentSpec ft_attachment()
None rnea_track(mjk.Env env, mjk.Robot robot, dict state, kdl.Frame target)
list[float] settle_and_tare(mjk.Env env, mjk.Robot robot, dict state)
dict run_selfcheck(mjk.Env env, mjk.Robot robot, kdl.Frame nominal, dict state)
None close_gripper(mjk.Env env)
mjk.AttachmentSpec gripper_attachment()
list[float] frame_point(kdl.Frame frame, kdl.Vector point)
list[float] spiral_force(float t)
list[float] xyz(kdl.Vector v)
float vnorm(list[float] a)
admittance_step(env, robot, nominal, state, force)
kdl.Frame tcp_frame(mjk.Robot robot, dict state)
list[float] vclamp(list[float] a, float limit)
mjk.SceneObject table_object()
tuple[mjk.Env, mjk.Robot] build_env()
None admittance_update(dict state, list[float] force, float dt)