Python Bindings
The mujoco_ros Python bindings expose a ROS-version-neutral wrapper around the hybrid C++ MujocoEnv API.
They build in both ROS 1 and ROS 2 workspaces and use py_binding_tools for ROS initialization support.
Basic Use
from pathlib import Path
from mujoco_ros import MujocoEnv
model_path = Path("/absolute/path/to/model.xml")
with MujocoEnv(model_path=model_path) as env:
env.pause()
env.step(100)
print(env.sim_info.model_valid)
Model and config path arguments accept strings and pathlib.Path objects.
load_model_from_string() uses the C++ loader, while load_from_path() and load_from_string() create Python-owned mujoco.MjModel and mujoco.MjData objects and pass their addresses to the C++ environment.
Loading A URDF/SRDF Description Bundle
There are two Python-facing ways to create a MujocoEnv from a robot
description bundle:
File-backed bundle: use
MujocoEnv.from_description(...)when you already have URDF/SRDF files on disk.Topic-backed bundle: construct
MujocoEnv(...)with the same ROS parameters the server launch files use. This is the path to use when the URDF and SRDF are published as latchedstd_msgs/Stringtopics such as/robot_descriptionand/robot_description_semantic.
File-backed URDF/SRDF
from pathlib import Path
from mujoco_ros import MujocoEnv
urdf_path = Path("/absolute/path/to/robot.urdf")
srdf_path = Path("/absolute/path/to/robot.srdf")
with MujocoEnv.from_description(
urdf_path=urdf_path,
srdf_path=srdf_path,
) as env:
env.pause()
env.step(1)
print(env.filename)
print(env.sim_info.model_valid)
MujocoEnv.from_description(...) compiles the URDF/SRDF bundle into a MuJoCo
model in Python, then loads that model into the wrapped C++ environment.
The SRDF path may be an empty string when no SRDF is needed.
Topic-backed URDF/SRDF
from mujoco_ros import MujocoEnv
parameters = {
"urdf.source": "topic",
"urdf.topic": "/robot_description",
"srdf.source": "topic",
"srdf.topic": "/robot_description_semantic",
"modelfile": "",
}
with MujocoEnv(parameters=parameters) as env:
env.pause()
env.step(1)
print(env.filename)
print(env.sim_info.model_valid)
This uses the same description-bundle parameter interface as the ROS launch
files. urdf.topic defaults to robot_description and srdf.topic
defaults to robot_description_semantic when omitted.
If your publisher also provides Extended Params or you want extra converter
options, add them to the same parameters mapping:
parameters = {
"urdf.source": "file",
"urdf.path": "/absolute/path/to/robot.urdf",
"srdf.source": "file",
"srdf.path": "/absolute/path/to/robot.srdf",
"description.generate_actuators": "true",
"description.attach_prefix": "robot1_",
"modelfile": "/absolute/path/to/world.xml",
}
with MujocoEnv(parameters=parameters) as env:
env.step(10)
Model With Plugin Example
The following example loads an installed model, injects a plugin configuration, changes runtime settings, steps the simulation, and prints simulation and plugin information.
from pathlib import Path
from mujoco_ros import MujocoEnv
def package_share(package_name):
try:
from ament_index_python.packages import get_package_share_directory
return Path(get_package_share_directory(package_name))
except ImportError:
import rospkg
return Path(rospkg.RosPack().get_path(package_name))
model_path = package_share("mujoco_ros") / "assets" / "pendulum_world.xml"
plugin_config = [
{
"name": "python_sensors",
"type": "mujoco_ros_sensors/MujocoRosSensorsPlugin",
}
]
with MujocoEnv(plugin_config=plugin_config) as env:
env.load_model_from_string(model_path)
env.pause()
env.settings.rt_factor = 0.5
env.settings.gravity = [0.0, 0.0, -3.71]
env.step(100)
info = env.sim_info
print(f"model: {info.model_path}")
print(f"valid: {info.model_valid}, loads: {info.load_count}")
print(f"rt setting: {info.rt_setting}")
print(f"gravity: {env.settings.gravity}")
for plugin in env.plugins:
print(f"plugin: {plugin.name} ({plugin.type})")
for stat in env.plugin_stats:
print(f"{stat.name}: load={stat.load_time:.6f}s reset={stat.reset_time:.6f}s")
Runtime Settings
env.settings is a runtime facade. Reads use a fresh read-only _EnvSettings copy, while supported writes call thread-safe C++ setters.
Writable fields are:
runningrt_factorbusywaitgravity
Use env.settings.snapshot() to retrieve the raw read-only settings value object for debugging or tests.
Runtime Options
env.runtime_options returns an immutable RuntimeOptionsSnapshot of the
active MuJoCo mjOption fields. env.apply_runtime_options(dict) applies a
partial patch through the same transaction path as ROS dynamic reconfigure and
ROS 2 parameters.
Field names match the configuration reference: integrator and solver settings,
scalars such as timestep and iterations, space-delimited arrays such as
gravity and solimp, disable flags ending in _disabled, and enable
flags such as energy and multiccd.
A rejected patch leaves the previous snapshot unchanged and does not advance the
Options Epoch. Validation failures raise ValueError with
field + ": " + message:
before = env.runtime_options
with self.assertRaisesRegex(ValueError, "solimp"):
env.apply_runtime_options({"timestep": 0.002, "solimp": "0.9 0.95"})
self.assertEqual(before, env.runtime_options)
During the Loading Window, reads and writes raise RuntimeError with
Runtime Options unavailable during Loading Window. Constructor keyword
runtime_options={...} applies startup-only patches before the first model
load; invalid startup values raise ValueError at construction time.
Plugins
Plugin configuration can be injected at construction time with plugin_config, config_files, and parameters. Launch-provided configuration continues to work unchanged.
env.sim_info is the preferred public status object.
It reports the model path, validity, load count, loading state, pause state, pending manual steps, and real-time factor values.
env.sim_state remains available as a low-level compatibility object for measured slowdown, model validity, and load count.
env.plugins returns generic bound plugin objects.
Each plugin exposes its name, type, load/reset timing, and callback timing EMAs.
env.plugin_names provides a convenience list of plugin names.
Each loaded plugin belongs to the current Plugin Generation — the complete
adapter set for one Simulation Model and runtime data pair. Reload replaces the
generation. Python plugin handles reacquire the host on every property or method
access and compare the handle’s generation to the active one. A handle retained
across reload raises RuntimeError("plugin handle belongs to an inactive Plugin
Generation") rather than returning stale backend state.
Downstream packages can provide plugin-specific wrappers by registering with mujoco_ros.plugins when imported, or through entry points in the mujoco_ros.plugins group.
Entry point names may match the plugin type suffix, the sanitized full plugin type, or the plugin instance name.
The loaded object can be a callable wrapper class, or expose bind(plugin) / from_mujoco_plugin(plugin).
entry_points={
"mujoco_ros.plugins": [
"MyPlugin = my_package.my_plugin:MyPlugin",
],
}
The same entry points are importable as dynamic submodules, for example import mujoco_ros.plugins.MyPlugin.
Rendering
When offscreen rendering is enabled, camera metadata and frame access are
available through mujoco_ros.rendering:
from mujoco_ros import MujocoEnv
from mujoco_ros.rendering import OffcamManager
with MujocoEnv(model_path="/absolute/path/to/camera_model.xml") as env:
with OffcamManager(env.binding, env.model) as cameras:
rgb, depth, segment = cameras.buffer(0)
The Python camera helper reads committed RenderCore leases directly. It
does not subscribe to ROS image topics and does not maintain a private ring.
Python demand is independent from ROS subscriber demand. A Python consumer can
force a capture while ROS publishers keep their configured cadence. When both
consumers select a frame, they use the same camera-specific capture identity.
buffer_size == 1 registers a one-shot consumer: idle managers do not render
until borrow_latest_* or copy_* requests a frame. buffer_size > 1
registers an explicit async cadence (one capture per simulation step,
independent from ROS) so step-then-read history fills without continuous idle
demand.
buffer(last_n=...) returns stable copied NumPy arrays. Borrowed access is
read-only and keeps its lease alive for the context-manager lifetime:
with cameras.camera(0).borrow_latest_rgb() as rgb:
assert not rgb.flags.writeable
print(rgb.capture_id, rgb.frame_generation)
Copies remain valid after later captures, resize operations, and reloads.
Use OffcamManager.close() or a with OffcamManager(...) block to release
Python render registrations explicitly. Manager destruction performs the same
cleanup.
Reload advances the frame generation. A borrowed view already acquired keeps
its old lease and remains readable. Acquire a new borrowed view after reload.
Camera wrappers rebind by camera name through OffscreenCameraConfig. Their
width, height, cam_name, fps, and plane metadata always
describe the active camera descriptor for that name. borrow_latest_* returns
a frame lease; copy_* copies the bytes from those leases. A separately held
native camera descriptor remains an old-layout snapshot, and an already
acquired borrowed view keeps its old shape and frame generation.
cam.buffer remains a stable compatibility object. It delegates legacy
native names such as getBufferHandles() and frame-count properties, while
also supporting copied aggregate snapshots through
cam.buffer(last_n=2). Legacy ring indices, lock methods, reset mutators,
and counter setters raise RuntimeError. An unconfigured plane’s read-only
frame count remains 0 for compatibility.
The aggregate buffer() result keeps its three-element shape. It uses
None for a plane that is not configured. The explicit
borrow_latest_*() and copy_*() accessors raise RuntimeError when
their requested plane is not configured. For a configured plane with no
committed frame yet, borrow_latest_*() raises RuntimeError while
copy_*() and aggregate buffer() snapshots may return None. The
removed native ring-buffer indices, lock methods, counter setters, and reset
mutators raise RuntimeError.
getBufferHandles() follows the same three-slot shape and returns None
for a missing plane.
Interactive Viewer
Import the viewer module from the package root:
from mujoco_ros import MujocoEnv, viewer
Blocking mode
viewer.launch(env) blocks the calling thread and owns GLFW on that thread
until the user closes the window. It returns only after the window closes.
with MujocoEnv(model_path="/path/to/model.xml") as env:
viewer.launch(env)
Passive mode
viewer.launch_passive(env, auto_sync=False) returns immediately with a
native lifetime handle. The viewer runs on a dedicated GUI thread. Only one live
viewer is allowed per environment; a second launch raises
RuntimeError("a viewer is already running for this MujocoEnv").
The handle supports close(), is_running(), sync(state_only=False),
lock() (reentrant context manager), and context-manager exit (which calls
close()). Stale handles from an earlier viewer generation are
generation-checked: close() on a stale or already-closed handle is an
idempotent no-op; is_running() returns False; sync() and lock()
raise RuntimeError("viewer is not running").
Closing a passive window (Exit button, handle.close(), or handle context
exit) stops only that viewer. The environment and physics loop stay available.
Automatic passive mode (auto_sync=True) synchronizes the viewer on each
rendered frame and around supported binding access while Python is idle.
Manual passive mode (the default) never auto-syncs; call handle.sync() after
binding changes, typically while holding handle.lock():
with MujocoEnv(model_path="/path/to/model.xml") as env:
with viewer.launch_passive(env, auto_sync=True) as handle:
env.unpause()
env.set_gravity([0.0, 0.0, -9.81])
with MujocoEnv(model_path="/path/to/model.xml") as env:
with viewer.launch_passive(env) as handle:
with handle.lock():
env.pause()
handle.sync()
Covered auto-sync operations
When auto_sync=True, these binding reads pull viewer-side state into Python
before returning:
gravity (
get_gravity())Runtime Options (
runtime_options,apply_runtime_options()reads)settings (
settings,settings.snapshot())simulation state/info (
sim_state,sim_info)model/data snapshots (
model,data)running state (
is_running,settings.running)
These binding writes push Python-side state to the viewer after succeeding:
load (
load_model_from_string(),load_from_path(),load_from_string())step, reset
pause/unpause (
pause(),unpause(),toggle_paused())real-time factor (
set_rt_factor(),settings.rt_factor)busywait (
settings.busywait)gravity (
set_gravity(),settings.gravity)Runtime Options (
apply_runtime_options())enable/disable flag helpers (
set_enableflag(),set_disableflag(),toggle_enableflag(),toggle_disableflag())
Raw writes through env.model or env.data are unsupported and do not sync
the viewer.
Build requirement and backend reporting
Interactive viewing requires a build configured with WITH_GUI=ON. Builds
with WITH_GUI=OFF raise
RuntimeError("mujoco_ros.viewer requires a build configured with WITH_GUI=ON").
Missing-display and GLFW initialization errors propagate; they are not swallowed.
Visible viewer GLFW and offscreen RenderCore are configured independently
(offscreen may still use a hidden GLFW context when ANY resolves that way):
WITH_GUIcontrols the visible GLFW viewer backend.OFFSCREEN_BACKENDcontrols RenderCore offscreen capture.pymujoco_ros.__viewer_backend__reports the first ("GLFW"or"NONE").pymujoco_ros.__render_backend__reports the second.
Deprecation
MujocoEnv.attach_viewer(active=True) is deprecated. It maps active=True
to blocking viewer.launch(self) and active=False to
viewer.launch_passive(self, auto_sync=True).
Known limitations
Passive GLFW runs on a dedicated non-process-main viewer thread. This follows MuJoCo’s passive-viewer shape but may be platform- or driver-sensitive on some systems.
Hybrid-NVIDIA laptop frozen-frame behavior is not guaranteed on all drivers.
Cooperative pump() and a separate-process GUI remain future options.
Render failures and capacity limits
Offscreen rendering uses the shared mujoco_ros_render_core library. The
offscreen backend is selected at build time through OFFSCREEN_BACKEND
(ANY, EGL, OSMesa, or DISABLE). Visible GLFW GUI is controlled
independently by WITH_GUI=ON or WITH_GUI=OFF. With WITH_GUI=ON,
ANY uses a hidden GLFW context for offscreen rendering (EGL only on a
detected hardware device) and OSMESA is rejected at configure time.
When configured camera history or byte demand exceeds the RenderCore budget,
model setup fails loudly with an explicit error such as configured frame slot
capacity ... exceeds RenderCore frame boundary budget .... The server does
not shrink history or return empty placeholders.
At runtime, capacity pressure surfaces as non-terminal FrameStatusCode values
such as kFrameSlotsExhausted and kGenerationCapacityExhausted. These are
distinct from backend integrity failures
(kTerminalError, kBackendFailure, kBackendUnavailable).
When the configured backend is disabled or fails to initialize, frame access returns terminal status rather than empty placeholders.
Python plane accessors raise RuntimeError for unconfigured planes and for
legacy ring-buffer APIs; they do not fabricate zero-filled frames.
borrow_latest_*() also raises RuntimeError when a configured plane has
no committed frame yet. copy_*() and aggregate buffer() snapshots may
return None for a configured plane that has not committed a frame yet.
Current Limitations
The core plugin packages currently provide specialized wrappers for sensors, laser, mocap, and control when their Python packages are importable.
The current port intentionally skips direct pixel rendering and MoveIt helpers.
Cooperative viewer pump() and a separate-process GUI are not implemented yet.
Passive GLFW runs on a dedicated non-process-main viewer thread; this
follows MuJoCo’s passive-viewer shape but may be platform- or driver-sensitive.
Hybrid-NVIDIA laptop frozen-frame behavior is not guaranteed on all drivers.