|
Mujoco KDL Wrapper
0.5.1
MuJoCo + KDL bridge for robot kinematics and dynamics
|
This page collects the Python wrapper usage notes that are too detailed for the README. For complete function signatures, see the generated stubs in python/mj_kdl_wrapper/*.pyi.
Coming from 0.4? The models are in mjk.ASSETS_DIR; see Migrating to 0.5. Coming from 0.2.x? Placement orientation moved from .euler to .quat [x, y, z, w]; see Migrating from 0.2.x. Units, frames and what persists between calls: Conventions.
A call that fails on its input raises RuntimeError whose message says why (the C++ Status::error text); a wrong-sized joint vector raises ValueError, and a spec field of the wrong length (a 5-value quat, say) raises TypeError.
The Python package exposes the same scene, robot, reset, viewer, and recorder concepts as the C++ wrapper. As in C++, one Env owns the MuJoCo mjModel/mjData, its robots and its viewer; KDL values come back through the upstream PyKDL module.
The wheel bundles PyKDL as a top-level extension module built from the same Orocos KDL fork as the wrapper. import PyKDL works after installation, but PyKDL does not appear in pip list / uv pip list because it is not installed as a separate Python distribution with its own .dist-info.
mj_kdl_wrapper.ASSETS_DIR is a pathlib.Path to the models installed with the package (Kinova Gen3, Robotiq 2F-85, table, mug, cabinet, cube, F/T sensor, door latch); the examples and tests load them as str(mjk.ASSETS_DIR / "kinova_gen3/gen3.xml"). For other MJCF sources (e.g. a MuJoCo Menagerie checkout), assign RobotSpec.path or mjcf_path directly; a model that brings its own floor (Menagerie's scene.xml) needs add_floor = False, or the two groundplane textures collide.
SceneSpec has no defaults for timestep, add_floor, or add_skybox. Those are explicit scene choices; Env.build() raises if one is unset or timestep <= 0. spec.robots may be empty; object-only scenes are valid.
For an object-only scene, put MJCF or primitive objects in spec.objects and leave spec.robots empty:
Env owns the compiled MuJoCo model/data. Call close(), or use it as a context manager, to release native resources deterministically. step() advances it whether or not it has robots; env.data.time and env.model.opt.timestep report where it is. build(), step(), pace(), reset(), add_object(), remove_object() and a recorder's record_frame() and render_rgb() release the GIL while they run. env.spec is a read-only copy of the scene the Env runs, objects added or removed since build() included; changing the copy changes nothing.
The log level is a threshold: messages at that level and above print. INFO (the default) prints everything, WARN warnings and errors, ERROR errors only, NONE nothing:
mjk.__version__ is the Python package version. mjk.__mujoco_version__ is the MuJoCo version the extension was built against.
create_robot() registers the robot with the Env, whose update() reads and commands every registered robot. After init, joint_names, joint_limits, and all joint port vectors are in KDL chain order, and the command ports hold the current pose.
The ports (jnt_pos_msr, jnt_vel_msr, jnt_trq_msr, jnt_pos_cmd, jnt_vel_cmd, jnt_trq_cmd, and the boolean jnt_saturated) read as read-only numpy copies. Write a port by assigning the whole vector; writing one element raises, so a lost write cannot go unnoticed:
A held copy stays what it was: it does not follow later update() or reset() calls. An assignment must have n_joints values (ValueError otherwise), and a robot of a closed Env raises RuntimeError("robot is closed").
When a tool or gripper is attached, pass ToolFrameSpec so the KDL chain uses the TCP site and includes the tool's inertia. tool_body is the tool's root body; for the 2F-85 that is g_base_mount, which carries mass too. robot.tip_T_tcp is the transform from the chain tip to the TCP:
For a wrist force-torque sensor, attach the sensor MJCF first, then attach the gripper to a site exported by that sensor asset:
Then register the logical force-torque sensor on the same tool spec. MuJoCo models one FT sensor as separate <force> and <torque> sensors; the wrapper returns a PyKDL.Wrench.
When the chain comes from the same description that produced the MJCF, register it as given; joint_names are the MuJoCo joints in chain order, and no tool inertia is lumped onto it:
AttachTarget is a tagged pair of AttachKind and an element name. The Kinova GEN3 MJCF exports pinch_site on the bracelet, so a Robotiq gripper can attach without manual pose offsets:
Optional pos and quat on the attachment spec are composed with the parent site pose, so small calibration offsets can stay local to the attachment. quat is [x, y, z, w] and defaults to identity [0, 0, 0, 1]. Assign the whole list – these properties return a copy, so gripper.pos[2] = 0.005 is silently discarded:
Chains are supported by appending multiple AttachmentSpec objects in order. Each later attachment may reference a body, site, or frame added by an earlier attachment.
Use prefixes to keep MuJoCo names distinct when loading the same MJCF more than once. create_robot()'s prefix is prepended to every name it resolves (base and tip bodies, tool body, TCP site, F/T sensor names), so create_robot("base_link", "bracelet_link", "r2_", tool) and the same call with r2_-prefixed names and no prefix build the same robot.
SceneObject and RobotSpec share the same attach_to field, so robots and objects can be mounted to sites or bodies created earlier in the scene spec. Build order is decorations, objects in declaration order, robots, then cameras.
For primitive objects, shape, size, rgba and friction are required, and mass unless the object is fixed. For MJCF-backed objects, mjcf_path takes precedence and primitive geometry fields are ignored at runtime.
MuJoCo restricts free joints to top-level bodies, so a non-fixed primitive with a free joint must stay world-anchored.
Add fixed world cameras through SceneSpec.cameras. pos and fovy are required; quat is [x, y, z, w] and defaults to identity [0, 0, 0, 1].
The viewer's and VideoRecorder's use_camera(name) switch to a fixed camera. Pass "" to return to the free camera.
Use body_frame() and site_frame() to read world poses as PyKDL.Frame; the kinematics are recomputed only when the state changed since they were last computed. Env.set_body_pose() teleports a free body and zeroes its velocity; its quaternion is [x, y, z, w] like every quaternion in the API.
For named actuators that are not part of a Robot joint mapping, write env.data directly (see Direct MuJoCo access); env.model.actuator(name) raises KeyError when the actuator is missing:
The binding layer constructs standard PyKDL objects for KDL return types. Downstream code can use the regular PyKDL solvers:
Robot.set_joint_pos() accepts either a Python sequence or a PyKDL.JntArray. Env.body_frame() and Env.site_frame() return PyKDL.Frame.
Gravity torques come from ChainDynParam on the same chain and JntArray:
env.update() reads MuJoCo state into every registered robot's jnt_pos_msr, jnt_vel_msr, and jnt_trq_msr, then applies their command ports: the active mode's command goes to that mode's actuator controls (jnt_pos_cmd, jnt_vel_cmd or jnt_trq_cmd), clamped to ctrlrange and flagged in jnt_saturated. robot.joint_force_limits() returns each joint's torque limit in the active mode. env.step() advances one timestep, after which joint state and frames describe the same instant; it returns False only when the viewer window has been closed, so a headless loop needs its own end condition.
Use set_joint_pos(q) to seed joint state directly in KDL order:
Env.reset() resets everything the Env holds and keeps registered robots synchronized across resets and runtime scene rebuilds.
Env.reset() restores MuJoCo state, re-seeds every registered robot's ports and F/T readings from the reset state (so the first command holds the pose), then calls the optional reset hook, then reads the measurements back. A command the hook primes is kept; a POSITION robot the hook moves needs its jnt_pos_cmd set there too. The Simulate UI's reset button does the same. An exception the hook raises comes out of reset() (or of the step() that ran a UI reset) after the measurements have been read back; env.on_reset reads None until set, and close() drops it. ResetContext.options and ResetContext.info are a copy of the reset request and its result, safe to keep after the hook returns. ResetOptions.use_keyframe = False forces a default MuJoCo reset instead of loading a keyframe.
Env also carries the runtime-state helpers:
Env.add_object() and Env.remove_object() rebuild the native MuJoCo model/data and rebind existing Python Robot handles, the viewer and open recorders. remove_object() of a name the scene does not hold raises RuntimeError naming it. Calling Env.close() invalidates dependent robot handles; using one raises RuntimeError("robot is closed").
env.model and env.data are the live mujoco.MjModel and mujoco.MjData the Env runs on, so the mujoco package reads and writes them in place. add_object() and remove_object() replace both: re-read them after either call rather than holding the old objects. The viewer thread reads env.data too; call mujoco functions on it only while the viewer is closed or paused.
Use VideoRecorder.open(env, path, width, height, fps) for explicit frame sizes, or open_preset() for VideoResolution presets. record_frame() captures the current state; step the Env before each call. A recorder is also a context manager.
For frames in memory instead of a file, open an offscreen renderer; render_rgb() returns a (height, width, 3) uint8 array, top row first, on any recorder:
A recorder's use_camera(name) takes any fixed camera of the model (robot MJCF cameras and SceneSpec.cameras); "" returns to the free camera.
Recording is offscreen-only: a VideoRecorder renders on its own EGL context, so the interactive window never pays for it. To record the view a fresh window opens with, leave the recorder on its default free camera. The Simulate UI's Recorder panel also offers its Current and Tracking views, all rendered offscreen.
env.open_viewer() starts the custom MuJoCo Simulate UI on the Env, with or without robots; env.step() then also honours its pause, perturbation and recording, and env.close() closes the window:
env.viewer.key_pressed(key) reports a held GLFW key code, and env.viewer.capture_key(key) withholds a key from the UI's own bindings (arrows, space, escape) so a controller can use it.
The UI exposes the same wrapper panels as the C++ viewer: Frames, Trace, Perturb, Recorder, and RTF. Trace overlays, camera selection and the real-time factor (1.0 is real time, 0.0 runs as fast as the loop allows) go through the Env's viewer; see the stubs in python/mj_kdl_wrapper/*.pyi for the exact method names.