Mujoco KDL Wrapper  0.2.2
MuJoCo + KDL bridge for robot kinematics and dynamics
Loading...
Searching...
No Matches
ex_admittance_ft_achd.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""Admittance control with an ACHD (Vereshchagin) task-space inner loop, FT-driven.
3
4Same outer admittance loop as ex_admittance_ft.py / ex_admittance_ft_rnea.py,
5but a different inner loop. Admittance is an outer force->position loop wrapped
6around an inner motion controller. The siblings use an ideal POSITION loop and a
7joint-space RNEA computed-torque loop; here the inner loop is TASK SPACE:
8
9 beta = Cartesian PD on the TCP pose error (desired Cartesian acceleration)
10 qddot = ACHD(q, qdot, alpha, beta) (constrained hybrid dynamics, KDL
11 ChainHdSolver_Vereshchagin)
12 tau = RNEA(q, qdot, qddot) (inverse dynamics for the torque)
13 apply tau in TORQUE mode
14
15The acceleration-constrained hybrid-dynamics (ACHD) solver consumes the desired
16Cartesian acceleration directly, so the admittance's Cartesian target feeds it
17without an IK step (alpha = identity constrains all 6 TCP DOF). It resolves the
18joint accelerations through the full arm dynamics, so tracking bandwidth is
19uniform across directions (a plain PD+gravity law lags/inverts the soft axis).
20
21Outer admittance law per Cartesian axis (no position stiffness):
22
23 M * a = F_ext - D * v
24 v += a * dt (clamped to MAX_VEL)
25 offset += v * dt (clamped to MAX_OFFSET)
26
27The logical FT sensor sits between the Kinova wrist and the Robotiq gripper.
28After closing the gripper and letting the wrist load settle, the controller
29tares it (the gripper's ~10 N static load only appears once it has closed).
30
31The run has two sources of external force, both handled by the same law:
32 - Intro: a scripted force whose direction sweeps a helix (spiral_force) drives
33 the admittance, so the TCP traces a helix.
34 - After the helix: the scripted force stops; the controller stays in
35 admittance and responds to the FT-measured force, so in the GUI you can
36 ctrl + right-drag the gripper. With K = 0 there is no equilibrium to spring
37 back to: when force stops, damping bleeds v -> 0 and the pose holds.
38"""
39
40from __future__ import annotations
41
42import argparse
43import math
44
45import PyKDL as kdl
46import mj_kdl_wrapper as mjk
47
48HOME = [0.0, 0.2618, 3.1416, -2.2689, 0.0, 0.9599, 1.5708]
49TABLE_Z = 0.70
50
51# Task-space inner-loop gains: Cartesian PD -> desired acceleration (beta), with
52# per-component acceleration limits and a joint-torque clamp.
53KP_LIN, KD_LIN = 5760.0, 378.0
54KP_ROT, KD_ROT = 3600.0, 441.0
55BETA_LIN_MAX, BETA_ROT_MAX, TAU_MAX = 3600.0, 2520.0, 212.4
56
57# Admittance outer loop: virtual mass, damping, stiffness (isotropic).
58# K_ADM = 0 -> pure hand-guiding: holds pose on release. Set > 0 to self-center.
59M_ADM, D_ADM, K_ADM = 8.0, 80.0, 0.0
60FORCE_DEADBAND = 2.5 # N; rejects sensor noise and settling transients
61MAX_OFFSET = 0.20 # m; reachable workspace half-extent around home
62MAX_VEL = 0.25 # m/s
63TOOL_BODY = "g_base" # rigid gripper base; where the headless self-check pushes
64GRIPPER_ACTUATOR = "g_fingers_actuator"
65SETTLE_STEPS = 300 # ~0.6 s at dt=0.002 to close the gripper before taring
66HANDOFF_TARE_TIME = 1.0 # s; let scripted-motion transients settle before FT hand-guiding
67SELFCHECK_PUSH = (8.0, 12.0, 6.0)
68
69# Intro helical force: amplitude/shape and how long it is applied.
70TEACH_TIME = 16.0
71TEACH_RADIUS = 0.04
72TEACH_RISE = 0.10
73TEACH_TURNS = 5.0
74
75
76def jnt(values: list[float]) -> kdl.JntArray:
77 out = kdl.JntArray(len(values))
78 for i, value in enumerate(values):
79 out[i] = value
80 return out
81
82
83def clamp(value: float, low: float, high: float) -> float:
84 return max(low, min(high, value))
85
86
87def vadd(a: list[float], b: list[float]) -> list[float]:
88 return [a[i] + b[i] for i in range(3)]
89
90
91def vscale(a: list[float], s: float) -> list[float]:
92 return [s * a[i] for i in range(3)]
93
94
95def vclamp(a: list[float], limit: float) -> list[float]:
96 return [clamp(x, -limit, limit) for x in a]
97
98
99def vnorm(a: list[float]) -> float:
100 return math.sqrt(sum(x * x for x in a))
101
102
103def xyz(v: kdl.Vector) -> list[float]:
104 return [v.x(), v.y(), v.z()]
105
106
107def frame_point(frame: kdl.Frame, point: kdl.Vector) -> list[float]:
108 return xyz(frame * point)
109
110
111def alpha_identity() -> kdl.Jacobian:
112 """Constraint matrix: all 6 TCP DOF are controlled (one beta per row)."""
113 alpha = kdl.Jacobian(6)
114 for i in range(6):
115 alpha[i, i] = 1.0
116 return alpha
117
118
119def ft_attachment() -> mjk.AttachmentSpec:
120 spec = mjk.AttachmentSpec()
121 spec.mjcf_path = mjk.menagerie.asset_path("ft_sensor.xml", env_var="MJ_KDL_FT_SENSOR")
122 spec.attach_to = mjk.AttachTarget(mjk.AttachKind.Site, "pinch_site")
123 return spec
124
125
126def gripper_attachment() -> mjk.AttachmentSpec:
127 spec = mjk.AttachmentSpec()
128 spec.mjcf_path = mjk.menagerie.asset_path("robotiq_2f85/2f85.xml", env_var="MJ_KDL_GRIPPER")
129 spec.attach_to = mjk.AttachTarget(mjk.AttachKind.Site, "wrist_ft_site")
130 spec.prefix = "g_"
131 return spec
132
133
134def table_object() -> mjk.SceneObject:
135 table = mjk.SceneObject()
136 table.name = "table"
137 table.mjcf_path = mjk.menagerie.asset_path("table.xml", env_var="MJ_KDL_TABLE")
138 table.pos = [0.0, 0.0, TABLE_Z]
139 table.fixed = True
140 return table
141
142
143def build_env() -> tuple[mjk.Env, mjk.Robot]:
144 table = table_object()
145 spec = mjk.SceneSpec()
146 spec.timestep = 0.002
147 spec.add_floor = True
148 spec.add_skybox = True
149 spec.objects = [table]
150
151 robot_spec = mjk.RobotSpec()
152 robot_spec.path = mjk.menagerie.model_path("kinova_gen3", env_var="MJ_KDL_MODEL")
153 robot_spec.attach_to = mjk.AttachTarget(
154 mjk.AttachKind.Site, mjk.scene_object_site_name(table, "table_top")
155 )
156 robot_spec.attachments = [ft_attachment(), gripper_attachment()]
157 spec.robots = [robot_spec]
158
159 env = mjk.Env.build(spec)
160
161 ft = mjk.ForceTorqueSensorSpec()
162 ft.name = "wrist_ft"
163 ft.frame_site = "wrist_ft_site"
164
165 tool = mjk.ToolFrameSpec()
166 tool.tool_body = "g_base"
167 tool.tcp_site = "g_pinch"
168 tool.ft_sensors = [ft]
169
170 robot = env.create_robot("base_link", "bracelet_link", tool=tool)
171 return env, robot
172
173
174def achd_track(robot: mjk.Robot, state: dict, target: kdl.Frame) -> None:
175 """Inner loop: task-space control via ACHD constrained hybrid dynamics.
176
177 A Cartesian PD on the TCP pose error is the desired acceleration (beta) for
178 all 6 constrained DOF (alpha = identity); ACHD resolves it into joint
179 accelerations through the arm dynamics, then RNEA maps those to torques.
180 No IK is needed -- the Cartesian target feeds the solver directly.
181 robot.update() must have refreshed jnt_pos_msr/jnt_vel_msr this step.
182 """
183 n = robot.n_joints
184 q = jnt(robot.jnt_pos_msr)
185 qd = jnt(robot.jnt_vel_msr)
186
187 err = kdl.diff(robot.fk_frame(), target)
188 e = [err.vel.x(), err.vel.y(), err.vel.z(), err.rot.x(), err.rot.y(), err.rot.z()]
189 if state["first_pid"]:
190 state["err_prev"] = e[:]
191 state["first_pid"] = False
192 de = [(e[i] - state["err_prev"][i]) / state["dt"] for i in range(6)]
193 state["err_prev"] = e[:]
194
195 beta = kdl.JntArray(6)
196 for i in range(3):
197 beta[i] = clamp(KP_LIN * e[i] + KD_LIN * de[i], -BETA_LIN_MAX, BETA_LIN_MAX)
198 for i in range(3, 6):
199 beta[i] = clamp(KP_ROT * e[i] + KD_ROT * de[i], -BETA_ROT_MAX, BETA_ROT_MAX)
200
201 qdd = kdl.JntArray(n)
202 ff = kdl.JntArray(n)
203 constraint_tau = kdl.JntArray(n)
204 f_ext = [kdl.Wrench.Zero() for _ in range(state["n_seg"])]
205 if state["achd"].CartToJnt(q, qd, qdd, state["alpha"], beta, f_ext, ff, constraint_tau) < 0:
206 raise RuntimeError("ACHD hybrid dynamics failed")
207
208 tau = kdl.JntArray(n)
209 if state["rnea"].CartToJnt(q, qd, qdd, f_ext, tau) < 0:
210 raise RuntimeError("RNEA inverse dynamics failed")
211 robot.jnt_trq_cmd = [clamp(tau[i], -TAU_MAX, TAU_MAX) for i in range(n)]
212
213
214def close_gripper(env: mjk.Env) -> None:
215 if env.has_actuator(GRIPPER_ACTUATOR):
216 env.set_actuator_ctrl(GRIPPER_ACTUATOR, 255.0)
217
218
219def settle_and_tare(env: mjk.Env, robot: mjk.Robot, state: dict) -> list[float]:
220 """Close the gripper, hold the home pose until the wrist load settles, tare.
221
222 The gripper's static load shows up at the FT site only once it has closed
223 and settled (~10 N here). Taring before that (right after reset, gripper
224 open) leaves a large constant bias error that an integrating (K=0)
225 admittance turns into permanent drift. So we hold the closed-gripper home
226 pose for a moment first, then capture the bias.
227 """
228 robot.update()
229 home = robot.fk_frame()
230 for _ in range(SETTLE_STEPS):
231 robot.update()
232 close_gripper(env)
233 achd_track(robot, state, home)
234 if not robot.step():
235 break
236 robot.update()
237 return xyz(robot.ft_sensor_frame("wrist_ft").M * robot.ft_sensor("wrist_ft").force)
238
239
240def measured_force(robot: mjk.Robot, state: dict) -> list[float]:
241 """External force on the tool in world frame, gravity-tared, deadbanded.
242
243 The MuJoCo force sensor reports the reaction wrench at the site, so the
244 external push the user applies is the negated, bias-removed reading. The
245 bias is the gripper's static gravity load captured after the gripper closes
246 and the wrist load settles (see settle_and_tare); expressed in the world
247 frame this is just the distal weight (mg, downward) and is invariant to the
248 arm configuration, so a single tare stays valid as the TCP translates around
249 home. Sub-deadband residue (noise, settling transients) is rejected to zero.
250 """
251 wrench = robot.ft_sensor("wrist_ft")
252 f_world = xyz(robot.ft_sensor_frame("wrist_ft").M * wrench.force)
253 bias = state["bias"]
254 f_ext = [bias[i] - f_world[i] for i in range(3)]
255 if vnorm(f_ext) < FORCE_DEADBAND:
256 return [0.0, 0.0, 0.0]
257 return f_ext
258
259
260def tare_force(robot: mjk.Robot) -> list[float]:
261 return xyz(robot.ft_sensor_frame("wrist_ft").M * robot.ft_sensor("wrist_ft").force)
262
263
264def admittance_update(state: dict, force: list[float], dt: float) -> None:
265 # offset = integral of velocity, so with K = 0 it is a pure integrator: the
266 # moment the push stops (force deadbanded to zero) we kill the velocity so
267 # motion stops dead and the offset (pose) is held exactly where it was left.
268 if force == [0.0, 0.0, 0.0]:
269 state["vel"] = [0.0, 0.0, 0.0]
270 return
271 acc = [
272 (force[i] - D_ADM * state["vel"][i] - K_ADM * state["offset"][i]) / M_ADM
273 for i in range(3)
274 ]
275 state["vel"] = vclamp(vadd(state["vel"], vscale(acc, dt)), MAX_VEL)
276 state["offset"] = vclamp(vadd(state["offset"], vscale(state["vel"], dt)), MAX_OFFSET)
277
278
279def spiral_force(t: float) -> list[float]:
280 """Scripted external force whose direction sweeps a helix over TEACH_TIME.
281
282 The force is D_ADM times the velocity of a helical path, so a mass-damper
283 admittance (steady state v = F / D) turns it into helical motion. Fed into
284 the admittance, this drives the intro helix.
285 """
286 if t < 0.0 or t > TEACH_TIME:
287 return [0.0, 0.0, 0.0]
288 theta = 2.0 * math.pi * TEACH_TURNS * t / TEACH_TIME
289 theta_dot = 2.0 * math.pi * TEACH_TURNS / TEACH_TIME
290 vx = -TEACH_RADIUS * theta_dot * math.sin(theta)
291 vy = TEACH_RADIUS * theta_dot * math.cos(theta)
292 vz = TEACH_RISE / TEACH_TIME
293 return [D_ADM * vx, D_ADM * vy, D_ADM * vz]
294
295
296def admittance_step(env, robot, nominal, state, force):
297 """One admittance tick: force -> offset (outer loop) -> ACHD-tracked TCP.
298
299 robot.update() must have run this step so the FT read behind `force` is
300 current. Returns the commanded target frame (for tracing).
301 """
302 admittance_update(state, force, env.timestep())
303 target = kdl.Frame(nominal.M, nominal.p + kdl.Vector(*state["offset"]))
304 achd_track(robot, state, target)
305 return target
306
307
308def run_gui(env: mjk.Env, robot: mjk.Robot, nominal: kdl.Frame, state: dict) -> None:
309 """Admittance control for the whole run (ACHD task-space inner loop).
310
311 For the first TEACH_TIME seconds a scripted helical force drives the
312 admittance, so the TCP traces a helix. After that the scripted force stops
313 and you can ctrl + right-drag the gripper to apply your own force, which the
314 FT senses; the same admittance responds and holds on release.
315 """
316 viewer = mjk.SimulateViewer.open(robot, "ex_admittance_ft_achd.py")
317 viewer.set_free_camera(1.55, 145.0, -24.0, (0.05, 0.0, TABLE_Z + 0.35))
318 prev = env.time()
319 start = env.time()
320 handoff_tared = False
321 target_prev: list[float] | None = None
322 tcp_prev: list[float] | None = None
323 trace_step = 0
324 try:
325 while viewer.is_running():
326 if env.time() < prev - 1e-6:
327 env.reset()
328 start = env.time()
329 handoff_tared = False
330 state["offset"] = [0.0, 0.0, 0.0]
331 state["vel"] = [0.0, 0.0, 0.0]
332 state["err_prev"] = [0.0] * 6
333 state["first_pid"] = True
334 target_prev = tcp_prev = None
335 prev = env.time()
336 t = env.time() - start
337 robot.update()
338 close_gripper(env)
339 # Intro: the scripted helical force IS the external force the demo
340 # applies (fed straight in -- also shoving the body would double-
341 # actuate it). After: the FT-measured force, so a hand-drag is sensed.
342 if t < TEACH_TIME:
343 force = spiral_force(t)
344 elif t < TEACH_TIME + HANDOFF_TARE_TIME:
345 force = [0.0, 0.0, 0.0]
346 else:
347 if not handoff_tared:
348 state["bias"] = tare_force(robot)
349 handoff_tared = True
350 force = measured_force(robot, state)
351 target = admittance_step(env, robot, nominal, state, force)
352
353 # Draw the commanded (yellow) and actual measured TCP (green) paths.
354 # Both poses are in the base_link frame, so map them through the base
355 # body's world pose before tracing or the trail lands at the wrong
356 # place (down by the base) and looks skewed.
357 trace_step += 1
358 world_base = env.body_frame("base_link")
359 target_xyz = frame_point(world_base, target.p)
360 tcp_xyz = frame_point(world_base, robot.fk_frame().p)
361 if target_prev and trace_step % 5 == 0:
362 viewer.add_trace_segment(target_prev, target_xyz, (1.0, 0.95, 0.0, 1.0))
363 if tcp_prev and trace_step % 5 == 0:
364 viewer.add_trace_segment(tcp_prev, tcp_xyz, (0.0, 1.0, 0.2, 1.0))
365 target_prev = target_xyz
366 tcp_prev = tcp_xyz
367
368 if not viewer.step():
369 break
370 finally:
371 env.set_body_wrench(TOOL_BODY, (0.0, 0.0, 0.0))
372 viewer.close()
373
374
375def run_selfcheck(env: mjk.Env, robot: mjk.Robot, nominal: kdl.Frame, state: dict) -> dict:
376 """Headless exercise of the same admittance law the GUI uses. Returns metrics.
377
378 Phase A: the scripted helical force drives the admittance (intro behaviour).
379 Phase B: a physical +Y wrench is sensed by the FT and yielded to, then
380 released. Verifies the admittance reacts to both force sources and holds
381 when force stops.
382 """
383 t0 = env.time()
384 helix_react = 0.0
385 helix_track_err = 0.0
386 while env.time() - t0 < TEACH_TIME:
387 t = env.time() - t0
388 robot.update()
389 close_gripper(env)
390 target = admittance_step(env, robot, nominal, state, spiral_force(t))
391 tcp = robot.fk_frame()
392 err = [tcp.p[i] - target.p[i] for i in range(3)]
393 helix_react = max(helix_react, vnorm(state["offset"]))
394 helix_track_err = max(helix_track_err, vnorm(err))
395 if not robot.step():
396 break
397
398 handoff_force = 0.0
399 t_handoff = env.time()
400 while env.time() - t_handoff < HANDOFF_TARE_TIME:
401 robot.update()
402 close_gripper(env)
403 target = admittance_step(env, robot, nominal, state, [0.0, 0.0, 0.0])
404 tcp = robot.fk_frame()
405 err = [tcp.p[i] - target.p[i] for i in range(3)]
406 helix_track_err = max(helix_track_err, vnorm(err))
407 if not robot.step():
408 break
409 robot.update()
410 state["bias"] = tare_force(robot)
411 for _ in range(100):
412 robot.update()
413 close_gripper(env)
414 force = measured_force(robot, state)
415 handoff_force = max(handoff_force, vnorm(force))
416 admittance_step(env, robot, nominal, state, force)
417 if not robot.step():
418 break
419
420 helix_settle_err = 0.0
421 t_settle = env.time()
422 while env.time() - t_settle < 0.5:
423 robot.update()
424 close_gripper(env)
425 target = admittance_step(env, robot, nominal, state, [0.0, 0.0, 0.0])
426 tcp = robot.fk_frame()
427 err = [tcp.p[i] - target.p[i] for i in range(3)]
428 helix_settle_err = max(helix_settle_err, vnorm(err))
429 if not robot.step():
430 break
431
432 pre_push = state["offset"][:]
433 t1 = env.time()
434 settled: list[float] | None = None
435 push_recovery_err: float | None = None
436 while env.time() - t1 < 4.0:
437 t = env.time() - t1
438 env.set_body_wrench(TOOL_BODY, SELFCHECK_PUSH if t < 1.0 else (0.0, 0.0, 0.0))
439 robot.update()
440 close_gripper(env)
441 target = admittance_step(env, robot, nominal, state, measured_force(robot, state))
442 tcp = robot.fk_frame()
443 err = [tcp.p[i] - target.p[i] for i in range(3)]
444 if push_recovery_err is None and t >= 2.0:
445 push_recovery_err = vnorm(err)
446 # Sample once the torque loop's settle transient has died (it can ring
447 # for ~1.5 s after release); hold drift is then the steady drift.
448 if settled is None and t >= 2.5:
449 settled = state["offset"][:]
450 if not robot.step():
451 break
452 env.set_body_wrench(TOOL_BODY, (0.0, 0.0, 0.0))
453 return {
454 "helix_react": helix_react,
455 "helix_track_err": helix_track_err,
456 "helix_settle_err": helix_settle_err,
457 "handoff_force": handoff_force,
458 "push_response": vnorm([(settled or pre_push)[i] - pre_push[i] for i in range(3)]),
459 "push_dy": (settled or pre_push)[1] - pre_push[1],
460 "push_recovery_err": push_recovery_err or 0.0,
461 "hold_drift": vnorm([state["offset"][i] - (settled or pre_push)[i] for i in range(3)]),
462 }
463
464
465def main() -> int:
466 parser = argparse.ArgumentParser()
467 parser.add_argument("--gui", action="store_true")
468 args = parser.parse_args()
469
470 env, robot = build_env()
471 try:
472 chain = robot.kdl_chain()
473 robot.ctrl_mode = mjk.CtrlMode.TORQUE # ACHD + RNEA computed-torque inner loop
474
475 state = {
476 "bias": [0.0, 0.0, 0.0],
477 "offset": [0.0, 0.0, 0.0],
478 "vel": [0.0, 0.0, 0.0],
479 # Vereshchagin root acceleration carries gravity as +z (its sign
480 # convention); RNEA below uses the usual -z gravity.
481 "achd": kdl.ChainHdSolver_Vereshchagin_Fixed_Joint(
482 chain, kdl.Twist(kdl.Vector(0.0, 0.0, 9.81), kdl.Vector.Zero()), 6
483 ),
484 "rnea": kdl.ChainIdSolver_RNE(chain, kdl.Vector(0.0, 0.0, -9.81)),
485 "alpha": alpha_identity(),
486 "n_seg": chain.getNrOfSegments(),
487 "err_prev": [0.0] * 6,
488 "first_pid": True,
489 "dt": env.timestep(),
490 }
491
492 def on_reset(ctx):
493 robot.set_joint_pos(HOME, call_forward=False)
494 state["offset"] = [0.0, 0.0, 0.0]
495 state["vel"] = [0.0, 0.0, 0.0]
496 state["err_prev"] = [0.0] * 6
497 state["first_pid"] = True
498 env.set_body_wrench(TOOL_BODY, (0.0, 0.0, 0.0))
499
500 env.on_reset = on_reset
501 env.reset()
502 # Single tare after settling. Orientation is held during hand-guiding so
503 # the world-frame gravity bias stays ~constant; a slow auto-tare would be
504 # needed only if drift exceeded the deadband during large reorientations.
505 state["bias"] = settle_and_tare(env, robot, state)
506 nominal = robot.fk_frame()
507
508 print(f"FT bias: [{state['bias'][0]:.3f}, {state['bias'][1]:.3f}, {state['bias'][2]:.3f}] N")
509 if args.gui:
510 run_gui(env, robot, nominal, state)
511 print(
512 "final offset: "
513 f"[{state['offset'][0]:.4f}, {state['offset'][1]:.4f}, {state['offset'][2]:.4f}] m"
514 )
515 else:
516 m = run_selfcheck(env, robot, nominal, state)
517 print(f"helix force response (max offset): {m['helix_react']:.4f} m")
518 print(f"helix TCP tracking error: {m['helix_track_err']:.4f} m")
519 print(f"helix settle error: {m['helix_settle_err']:.4f} m")
520 print(f"FT handoff residual force: {m['handoff_force']:.4f} N")
521 print(f"FT push response (offset norm): {m['push_response']:.4f} m")
522 print(f"FT push response (offset dY): {m['push_dy']:.4f} m")
523 print(f"push release recovery error: {m['push_recovery_err']:.4f} m")
524 print(f"hold drift after push released: {m['hold_drift']:.4f} m")
525 assert m["helix_react"] > 0.05, "admittance did not respond to the helical force"
526 assert m["helix_track_err"] < 0.006, "TCP did not track the commanded helix"
527 assert m["helix_settle_err"] < 0.004, "TCP did not settle cleanly after the helix"
528 assert m["handoff_force"] == 0.0, "FT handoff produced a false external force"
529 assert m["push_response"] > 0.05, "admittance did not yield to the FT-sensed push"
530 assert m["push_recovery_err"] < 0.006, "TCP did not recover quickly after the push"
531 assert m["hold_drift"] < 0.01, "pose did not hold after the push stopped"
532 print("OK: admittance responded to helix + FT push and held on release")
533 finally:
534 env.close()
535 return 0
536
537
538if __name__ == "__main__":
539 raise SystemExit(main())
None admittance_update(dict state, list[float] force, float dt)
list[float] xyz(kdl.Vector v)
kdl.JntArray jnt(list[float] values)
admittance_step(env, robot, nominal, state, force)
tuple[mjk.Env, mjk.Robot] build_env()
float clamp(float value, float low, float high)
None achd_track(mjk.Robot robot, dict state, kdl.Frame target)
None close_gripper(mjk.Env env)
float vnorm(list[float] a)
list[float] settle_and_tare(mjk.Env env, mjk.Robot robot, dict state)
list[float] spiral_force(float t)
dict run_selfcheck(mjk.Env env, mjk.Robot robot, kdl.Frame nominal, dict state)
list[float] frame_point(kdl.Frame frame, kdl.Vector point)
list[float] vadd(list[float] a, list[float] b)
list[float] vscale(list[float] a, float s)
mjk.AttachmentSpec gripper_attachment()
None run_gui(mjk.Env env, mjk.Robot robot, kdl.Frame nominal, dict state)
list[float] measured_force(mjk.Robot robot, dict state)
list[float] vclamp(list[float] a, float limit)
mjk.AttachmentSpec ft_attachment()
list[float] tare_force(mjk.Robot robot)