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