Model editing¶
The most general way to create an MjModel instance is by loading an XML file via MjModel::from_xml. Due to MjModel only allowing (some) changes to parameters and not to the actual geometry, MuJoCo introduced Model Editing.
In MuJoCo-rs, we created a high-level wrapper around MuJoCo’s C API, which provides safe wrappers around C structs, as well as methods. Aside from that, we try to stay faithful to MuJoCo’s implementation.
A procedurally generated, not yet compiled, model is represented by its specification (MjSpec). A specification can be created empty with MjSpec::new or pre-filled from XML with:
MjSpec::from_xml (loads XML from a file),
MjSpec::from_xml_vfs (loads XML from a file on a virtual file system),
MjSpec::from_xml_string (loads XML from a model defined in a string in memory).
After creation, we can use MjSpec to add items to the model, such as joints, geoms, actuators, etc. Non-structured items can be added through MjSpec itself (e.g., actuators, sensors, meshes, etc.). Structured items can be added through MjsBody (e.g., bodies, geoms, joints, etc.).
After procedurally creating a specification with MjSpec, compile it with MjSpec::compile. The compiled model runs in the simulation, and the compiled specification can also be saved to an XML file.
Attention
Model editing stays on one thread. MuJoCo’s C++ implementation shares unsynchronized state
between a specification and its elements, so MjSpec is neither Send nor Sync.
Examples¶
Complete, runnable examples on model editing are available in the repository’s examples:
Basic editing¶
Let’s lead with an example. We will create a model, where a ball falls onto a plane. We start by creating an MjSpec instance:
use mujoco_rs::prelude::*;
fn main() {
let mut spec = MjSpec::new();
}
Now we need to create a spherical body, which will be our ball.
This also includes adding a spherical geom and a free joint.
Since bodies are structured elements, we can’t add them to MjSpec.
Instead, we will add them to the world body (the worldbody element in a model’s XML).
To mutably access the specification’s world body, we can use the MjSpec::world_body_mut method.
use mujoco_rs::prelude::*;
fn main() {
let mut spec = MjSpec::new();
let world = spec.world_body_mut(); // or spec.body_mut("world").unwrap();
}
We can now add our ball’s body, geom and joint like so:
use mujoco_rs::prelude::*;
fn main() {
let mut spec = MjSpec::new();
let world = spec.world_body_mut(); // or spec.body_mut("world").unwrap();
// Add the ball
let ball_body = world.add_body()
.with_name("ball") // name
.with_pos([0.0, 0.0, 1.0]); // position
ball_body.add_geom()
.with_size([0.010, 0.0, 0.0]) // set the radius to 10 mm.
.with_type(MjtGeomBase::mjGEOM_SPHERE); // make this a spherical geom (default).
ball_body.add_joint()
.with_type(MjtJoint::mjJNT_FREE); // make the ball free to move anywhere.
}
Note
MjsBody::add_free_joint adds the same free joint in one call, with the exception of not inheriting any defaults (not even the set-by-default default).
Tip
In the above block, we used methods that have the with_ prefix.
These allow method chaining.
Many fields also carry a set_ method, but not all: an array field such as pos or
size offers with_pos and pos_mut only. A plain field setter
(e.g. set_group, set_mass) returns nothing, while a validated setter returns a
Result (set_name fails on a duplicate name, set_default on an unknown class, and
MjsNumeric::set_size on a negative size).
Setter (set_) methods are available for many common fields, including strings and
several vector/buffer fields. For nested or structured data, use getters that end with
the _mut suffix.
Finally, we can now add the base plane, like so:
use mujoco_rs::prelude::*;
fn main() {
let mut spec = MjSpec::new();
let world = spec.world_body_mut(); // or spec.body_mut("world").unwrap();
// Add the ball
let ball_body = world.add_body()
.with_name("ball") // name
.with_pos([0.0, 0.0, 1.0]); // position
ball_body.add_geom()
.with_size([0.010, 0.0, 0.0]) // set the radius to 10 mm.
.with_type(MjtGeomBase::mjGEOM_SPHERE); // make this a spherical geom (default).
ball_body.add_joint()
.with_type(MjtJoint::mjJNT_FREE); // make the ball free to move anywhere.
// Add the base plane
world.add_geom()
.with_type(MjtGeomBase::mjGEOM_PLANE)
.with_size([1.0, 1.0, 1.0]);
}
This concludes the specification’s definition.
We can now compile it to a model and save it to an MJB (binary) file.
The specification can also be saved to an MJCF (XML) file. By default (compiler option
savecompiled set), MuJoCo writes MJCF only from a compiled specification, thus save_xml and
save_xml_string return
SaveFailed when the
model was not compiled (compile()):
use mujoco_rs::prelude::*;
fn main() {
let mut spec = MjSpec::new();
let world = spec.world_body_mut(); // or spec.body_mut("world").unwrap();
// Add the ball
let ball_body = world.add_body()
.with_name("ball") // name
.with_pos([0.0, 0.0, 1.0]); // position
ball_body.add_geom()
.with_size([0.010, 0.0, 0.0]) // set the radius to 10 mm.
.with_type(MjtGeomBase::mjGEOM_SPHERE); // make this a spherical geom (default).
ball_body.add_joint()
.with_type(MjtJoint::mjJNT_FREE); // make the ball free to move anywhere.
// Add the base plane
world.add_geom()
.with_type(MjtGeomBase::mjGEOM_PLANE)
.with_size([1.0, 1.0, 1.0]);
// Compile and save
let model = spec.compile().expect("failed to compile");
spec.save_xml("model.xml").expect("failed to save"); // save XML to a file.
let xml_str = spec.save_xml_string(8192).expect("failed to save"); // save XML to a string.
model.save_to_file("model.mjb").expect("failed to save"); // save binary.
}
The model from the above example, generated by MjSpec::compile,
can be used exactly the same as if we were to directly load an XML/MJB model (see Basic simulation).
The compiled MjModel can also be swapped into a running simulation using
MjData::swap_model or
MjData::try_swap_model,
as described in the Basic simulation chapter. The swap keeps the existing MjData, so it accepts
only a compatible model, which can be checked by
MjModel::is_compatible_with_model.
A compatible model shares the same element tree, type of each element (joint, geom, site, etc.),
material of each element, and every size that fixes an MjData buffer, such as
nuserdata, na and nout. Most numeric attribute changes therefore keep compatibility,
while structural changes (e.g., an added or deleted element, a changed joint type, etc.) break compatibility.
Thread-safety and Send¶
Almost all of model-editing features, available in MuJoCo-rs, are wrappers of MuJoCo’s C++
model-editing code. That code is written with certain assumptions in place, such as single-thread
use of same spec. For this purpose, the model-editing elements and MjSpec itself do not implement
Send, which would allow MjSpec to be
sent to another thread directly.
However, using MjSpec is thread-safe in certain scenarios. Thread-safety is guaranteed, except under the following situations:
cloning a MjSpec that uses the
<model>tag inside<asset>in the MJCF XML definition;attaching procedurally via Attach::attach_by_reference;
attaching procedurally via Attach::attach_by_deep_copy;
holding a user value that is not
Send(see Storing user values).
Note that these attachments don’t include just direct attachments of MjSpec, but also any other model-editing element. Attaching inside a single MjSpec shares nothing. The restriction is a consequence of MuJoCo using non-atomic reference counting.
Sending a MjSpec to another thread does not compile:
use mujoco_rs::prelude::*;
use std::thread;
fn main() {
let mut spec = MjSpec::from_xml("model.xml").unwrap();
thread::spawn(move || {
spec.compile().unwrap();
});
}
error[E0277]: `NonNull<mjSpec_>` cannot be sent between threads safely
note: required because it appears within the type `mujoco_rs::wrappers::MjSpec`
When the conditions above hold, wrap the specification via MjSpec::into_sendable:
use mujoco_rs::prelude::*;
use std::thread;
fn main() {
let spec = MjSpec::from_xml("model.xml").unwrap();
// SAFETY: the spec was only parsed, never attached to and never cloned.
let sendable = unsafe { spec.into_sendable() };
thread::spawn(move || {
let mut spec = sendable.take();
spec.compile().unwrap();
}).join().unwrap();
}
Note
MjModel is Send, so you can also compile on the editing thread and move the compiled
model instead:
let model = spec.compile().unwrap(); // spec stays on this thread
thread::spawn(move || {
let mut data = MjData::new(&model);
data.step();
}).join().unwrap();
Attaching elements¶
In addition to adding elements to the specification and other elements directly, existing trees of elements can be attached directly onto a new tree.
Elements can be attached to another element via
Attach::attach_by_reference or Attach::attach_by_deep_copy,
available on types implementing Attach. Elements implementing Attach are MjsBody,
MjsFrame and MjsSite. To place the attached tree at a chosen position, first add a frame
to the parent with
MjsBody::add_frame
and attach to that frame. Here is the full mapping of what element type can be
attached to what other element type:
We do not permit attachments of a child MjsFrame to an MjsBody, because MuJoCo does not
actually deep-copy the frame fully.
Add a frame to the body with MjsBody::add_frame and attach to the newly added frame instead.
attach_by_deep_copy copies every element of the child into the parent, so both specifications
stay usable after the call. attach_by_reference makes the parent share the elements of the
child, which is MuJoCo’s default behavior. That sharing is why the method is unsafe.
See method docstrings for full conditions.
When attaching elements, a prefix and a suffix can be given. When no prefix/suffix is desired,
pass "" to Attach::attach_by_deep_copy / Attach::attach_by_reference at its
respective positions.
The example below attaches a one-body specification under a frame of another specification.
use mujoco_rs::prelude::*;
fn main() {
// Child specification.
let mut child = MjSpec::new();
child.world_body_mut().add_body().with_name("ball")
.add_geom().with_size([0.01, 0.0, 0.0]);
// Parent specification.
let mut parent = MjSpec::new();
// Add a frame onto which the child will be attached.
let frame = parent.world_body_mut().add_frame().with_pos([0.0, 0.0, 1.0]);
// Attach the child to the newly-created frame, giving all sub-elements
// of the child tree "robot_" as a prefix to all the names.
// This method is available by importing the `Attach` trait (already in the prelude).
frame.attach_by_deep_copy(&mut child, "robot_", "").unwrap();
// The child is now attached.
assert!(parent.body("robot_ball").is_some());
parent.compile().expect("failed to compile");
}
More on attachment is available in MuJoCo’s official documentation on attachment.
Danger
Regardless of whether the element is attached via MJCF XML (<model> inside <asset>),
by attach_by_reference or by attach_by_deep_copy, the specification becomes
thread-unsafe. Do not share it across threads.
Using attach_by_reference also permits creating shared references that aren’t aware they are
pointing to same data (one through parent and one through child), thus it is unsafe even in the same
thread. When using attach_by_reference, avoid using the original specification and its
existing references.
Deleting elements¶
Most elements can be removed from a specification with the unsafe
SpecObject::delete, called on the
handle of the element. A default class and a tendon wrap are no
SpecObject, so they carry no delete at all.
The world body does carry it, and it returns
UnsupportedOperation for it.
use mujoco_rs::prelude::*;
fn main() {
let mut spec = MjSpec::new();
spec.world_body_mut().add_body().with_name("ball");
unsafe { spec.body_mut("ball").unwrap().delete() }.expect("failed to delete the body");
}
delete is unsafe because MuJoCo cannot answer whether it already deleted an element. It
keeps a deleted element allocated until the specification drops, so a second deletion of the same
element frees it twice. The caller carries three obligations:
Delete each element at most once.
Do not delete an element that the deletion of a body or a frame already took out of the specification.
Do not use the handle of an element that the deletion of a body or a frame freed.
Deleting a body or a frame deletes its whole subtree, and it frees every actuator, sensor, tendon, equality, pair and exclude that refers to the subtree. The last two obligations follow from that: an iterator collected before such a deletion still hands out the handles it took.
The borrow checker covers the ordinary case, because a handle borrows the specification and a deletion needs it mutably. Look the next element up in each round, as the loop below does, and every handle the loop holds belongs to a live element.
use mujoco_rs::prelude::*;
fn delete_every_geom(spec: &mut MjSpec) {
// SAFETY: the walk starts again after each deletion, so it reaches a live geom only.
while let Some(geom) = spec.geom_iter_mut().next() {
unsafe { geom.delete() }.unwrap();
}
}
Class inheritance (defaults)¶
MuJoCo supports default classes, which allow shared attribute values to be set in one place and then inherited by multiple elements. In MuJoCo-rs, default classes can be created with MjSpec::add_default.
A new class is created by providing its class_name and optionally a parent_class_name
(for class inheritance). If no parent is given, the class inherits from the main default.
use mujoco_rs::prelude::*;
fn main() {
let mut spec = MjSpec::new();
// Create a top-level default class named "red".
// Use try_add_default() for a fallible variant that returns Result.
spec.add_default("red", None);
// Create a child class "small-red" that inherits from "red".
spec.add_default("small-red", Some("red"));
}
Elements reference a class in the model XML through their class attribute. A body and a frame
have childclass in-place of class, which causes their sub-elements to inherit the class.
In Rust code, class assignment is done either at creation of elements or post-creation.
They can be added at creation by calling SomeSpecElementType::add_geom_with_class,
which accepts a name of the class (e.g.,
MjsBody::add_geom_with_class):
use mujoco_rs::prelude::*;
fn main() {
let mut spec = MjSpec::new();
spec.add_default("red", None).geom_mut().set_margin(0.5);
// The geom takes the margin of the "red" class at creation.
let geom = spec.world_body_mut().add_geom_with_class("red").unwrap();
assert_eq!(geom.margin(), 0.5);
}
They can be added post-creation by calling
set_default or
with_default.
However, note that these two methods only set the class name on the item, without copying
the values of the class into it. MuJoCo copies the values at creation, as shown above. A saved XML
therefore holds something like <item class="some-cls" param1="0" param2="5">, where param1
and param2 keep the values that the item received at creation.
What the post-creation methods (set_default, with_default) do allow, is setting the
class of the children that the item receives any time after calling any of the two methods (MjsBody).
use mujoco_rs::prelude::*;
fn main() {
let mut spec = MjSpec::new();
spec.add_default("red", None).geom_mut().set_margin(0.5);
let body = spec.world_body_mut().add_body();
body.set_default("red").unwrap();
// The geom takes the margin of the class of the body at creation.
assert_eq!(body.add_geom().margin(), 0.5);
}
Iterators¶
Since MuJoCo-rs 1.5.0, it is possible to also iterate existing MjSpec items (geoms, joints, etc.). Iterators exist on MjSpec and MjsBody.
To iterate over MjSpec items, call
[item_type]_iter for immutable iteration or [item_type]_iter_mut for mutable iteration,
with [item_type] replaced by geom, body, etc.
A body is the exception: MjSpec carries body_iter alone. Every item that a mutable iterator
yields borrows the spec for the same lifetime, so all of them stay live together. A body owns a
subtree, so a flat mutable body iterator would hand out a body and that body’s own descendant at
once. Reach a body through
MjSpec::world_body_mut and walk down
the tree.
// ...
for body in spec.body_iter() { // spec is MjSpec.
println!("{}", body.name());
}
// ...
Iteration over MjsBody items can be used in a similar way.
The only difference is an additional boolean parameter, which enables recursive iteration when true.
The mutable body iterator
(MjsBody::body_iter_mut) takes no such
parameter and yields the direct children only, for the reason above.
// ...
// Iterate top level bodies of body.
for body in body.body_iter(false) { // body is MjsBody.
println!("{}", body.name());
}
// ...
// ...
// Iterate top level bodies of body + their sub-bodies recursively.
for body in body.body_iter(true) { // body is MjsBody.
println!("{}", body.name());
}
// ...
Finding elements¶
Existing elements can be looked up by name. MjSpec exposes a finder method (and a matching
_mut variant) per item type, for example
MjSpec::body /
MjSpec::body_mut,
MjSpec::geom, and
MjSpec::frame. Within a body,
a body of its subtree is found with
MjsBody::child /
MjsBody::child_mut.
The search is recursive, and it returns the body itself when its own name matches.
Every finder returns an Option that is None when no element with that name exists.
After compilation, an element’s numeric id in the resulting MjModel can be retrieved with
SpecItem::id
(None when the element has no id yet).
Storing user values¶
Every MjSpec element can carry arbitrary values under string keys, through the UserValued trait (exported by the prelude).
The user data stored through use of the methods in UserValued is not preserved after compiling
to MjModel. It can thus only be used during model editing.
The trait defines the following methods:
On the MuJoCo-rs’s API surface, the trait methods accept Box<dyn Any> and return &dyn Any
polymorphic type-erased objects. See
Any’s documentation
on how to downcast to the actual type
(e.g., via Any::downcast_ref).
use mujoco_rs::prelude::*;
fn main() {
let mut spec = MjSpec::new();
let geom = spec.world_body_mut().add_geom();
geom.set_user_value("foo", Box::new(String::from("user-value-1")));
geom.set_user_value("bar", Box::new(vec![0.0f64; 3]));
// Read and name the type again.
let serial = geom.user_value("foo").unwrap();
assert_eq!(serial.downcast_ref::<String>().unwrap(), "user-value-1");
// Wrong types give None.
assert!(serial.downcast_ref::<u32>().is_none());
// Modify in place.
geom.user_value_mut("bar").unwrap().downcast_mut::<Vec<f64>>().unwrap()[0] = 1.5;
geom.remove_user_value("foo");
assert!(geom.user_value("foo").is_none());
}
Keys have mujoco-rs: prefix added internally, which prevents clashes with user values
stored externally from MuJoCo-rs (e.g., in plugins).
Configuring actuators¶
An actuator added with MjSpec::add_actuator
can be configured into one of MuJoCo’s predefined actuator types with the set_to_* family of
methods on MjsActuator (motor, position, integrated velocity, velocity, damper, cylinder,
muscle, adhesion, DC motor, PID, and orientation), mirroring MuJoCo’s mjs_setToX C API.
A method whose parameters are all mandatory takes them positionally (for example
set_to_velocity(kv) or set_to_cylinder(timeconst, bias, area, diameter)). A method whose
underlying C function accepts nullable parameters instead takes a dedicated Default-able
config struct — PositionConfig,
IntVelocityConfig,
DcMotorConfig,
PidConfig, and
OrientationConfig — in which those nullable
parameters are Option fields. A config is built either with struct-update syntax or with the
chainable with_* builder methods (which take the inner value and wrap optional fields in
Some), so only the relevant fields need to be set; everything else stays at its MuJoCo default:
use mujoco_rs::prelude::*;
use mujoco_rs::wrappers::mj_editing::DcMotorConfig;
fn main() {
let mut spec = MjSpec::new();
let body = spec.world_body_mut().add_body();
body.add_geom().with_size([0.01, 0.0, 0.0]);
body.add_joint().with_name("hinge");
let actuator = spec.add_actuator().with_trntype(MjtTrn::mjTRN_JOINT);
actuator.set_target("hinge");
// Configure as a DC motor using the chainable builder. Struct-update syntax works too:
// DcMotorConfig { motorconst: Some([1.0, 1.0]), resistance: 1.0, ..Default::default() }
actuator.set_to_dc_motor(
DcMotorConfig::default().with_motorconst([1.0, 1.0]).with_resistance(1.0)
).expect("invalid DC motor parameters");
}
Methods that MuJoCo can reject (for example a negative gain, or mutually exclusive parameters)
return Result<(), MjEditError>, and the only error variant they ever produce is
MjEditError::InvalidParameter; the parameterless or
always-valid ones (set_to_motor, set_to_velocity, set_to_cylinder) return ().
Procedural flex generation¶
A flexcomp procedurally generates a deformable MjsFlex together with its supporting bodies, joints, and optional equality constraints — handy for cloth, ropes, and soft volumes. In MuJoCo-rs it is created with MjsBody::add_flexcomp (or the fallible MjsBody::try_add_flexcomp).
The generation is configured through a Default-able
MjFlexcompConfig, using the same with_*
builder pattern as the actuator configs above: only the fields you set deviate from MuJoCo’s
defaults, and unset optional fields fall back to the compiler’s own defaults. add_flexcomp
returns the created MjsFlex, which can be tweaked further (for example its self-collision mode).
use mujoco_rs::prelude::*;
fn main() {
let mut spec = MjSpec::new();
// A 5x5 cloth-like 2D grid, held together by edge equality constraints.
let config = MjFlexcompConfig::default()
.with_type("grid")
.with_dim(2)
.with_count([5, 5, 1])
.with_spacing([0.1, 0.1, 0.1])
.with_mass(1.0)
.with_equality(1); // 0 none, 1 edge, 2 vertex, 3 strain
spec.world_body_mut().add_flexcomp("cloth", &config);
spec.compile().expect("failed to compile");
}