Attribute views

The MuJoCo library stores data about joints, bodies, and other elements in contiguous arrays. These can be challenging to work with, particularly when the array’s length varies between elements. For example, different types of joints may have a different number of degrees of freedom. MuJoCo’s Python bindings solve this issue by providing views to specific ranges in the corresponding arrays.

Like MuJoCo’s Python bindings, MuJoCo-rs also provides views. Specifically, we provide views for attributes of MjData and MjModel.

Views borrow data and cannot be preserved across simulation steps, as that would violate Rust’s borrow checker rules. Re-looking up names each step would also be expensive. To overcome this, “info” structs exist, which cache the resolved index ranges and the model signature for fast view creation after each step.

Warning

Cached info structs are tied to the model signature they were created from. Calling view() / view_mut() with data from an incompatible model will panic with a model-signature mismatch. Use try_view() / try_view_mut() if you want to handle mismatches as Result values instead of panicking.

Reading

For example, let’s say we want to read the position of a free joint. We will first create an info struct by calling MjData::joint like so:

use mujoco_rs::prelude::*;

fn main() {
    let model = MjModel::from_xml("model.xml").expect("could not load the model");
    let mut data = MjData::new(&model);
    let joint_info = data.joint("football-ball").expect("name not found");
    loop {
        data.step();
        // ...
    }
}

To actually view the data, we will now call MjJointDataInfo::view and pass it a reference to MjData, like so:

use mujoco_rs::prelude::*;

fn main() {
    let model = MjModel::from_xml("model.xml").expect("could not load the model");
    let mut data = MjData::new(&model);
    let joint_info = data.joint("football-ball").expect("name not found");
    loop {
        data.step();
        println!("{:?}", &joint_info.view(&data).qpos[..3]);  // print x, y and z coordinates.
    }
}

If a signature mismatch is possible in your workflow, use MjJointDataInfo::try_view and handle the returned Result instead of panicking.

View attributes like MjJointDataView::qpos use mujoco_rs::util::PointerView (or Option<PointerView> for optional fields). PointerView implements the Deref trait and on deref acts like a slice. While some fields might be scalars, we still treat those as arrays for implementation simplicity reasons.

The same view / view_mut pattern applies to every named element type — bodies, geoms, sites, actuators, sensors, and so on — each looked up through its corresponding finder (e.g. MjData::body or MjData::geom) and info struct.

Writing

The above example shows a read-only view. For mutability, MjJointDataInfo::view_mut must be called and passed a mutable reference to MjData, like so:

use mujoco_rs::prelude::*;

fn main() {
    let model = MjModel::from_xml("model.xml").expect("could not load the model");
    let mut data = MjData::new(&model);
    let joint_info = data.joint("football-ball").expect("name not found");
    loop {
        data.step();
        joint_info.view_mut(&mut data).qpos[0] = 0.5;
    }
}

For fallible mutable access, use MjJointDataInfo::try_view_mut to get a Result instead of panicking on signature mismatch.

In mutable views, regular writable fields use mujoco_rs::util::PointerViewMut. Fields marked as read-only in the generated view still support mutation, but only through mujoco_rs::util::PointerViewUnsafeMut and explicit unsafe:

use mujoco_rs::prelude::*;

fn main() {
    let mut model = MjModel::from_xml("model.xml").expect("could not load the model");
    let actuator_info = model.actuator("slider").unwrap();
    let mut view = actuator_info.view_mut(&mut model);

    unsafe {
        view.dyntype.as_mut_slice()[0] = MjtDyn::mjDYN_NONE;
        view.actadr.as_mut_slice()[0] = -1;
    }
}