SuperAPI AI Directory

mujoco-spec-editing

Coding & development

View repository

Updated 10-15 21:53

About this skill

MuJoCo Spec Editing: Building & Modifying Models

[!TIP]

Related skills:

1. Procedural Modeling with MjSpec

MjSpec is a mutable C++ and Python data structure that mirrors the MJCF XML schema, allowing models to be constructed and modified programmatically.

import mujoco

spec = mujoco.MjSpec()

# Add a body with a freejoint and sphere geom
body = spec.worldbody.add_body(name='ball', pos=[0, 0, 1.0])
body.add_freejoint()
body.add_geom(type=mujoco.mjtGeom.mjGEOM_SPHERE, size=[0.1], rgba=[1, 0, 0, 1])

# Compile to MjModel
model = spec.compile()

In C++:

#include <mujoco/mujoco.h>

mjSpec* spec = mj_makeSpec();
mjsBody* body = mjs_addBody(mjs_findBody(spec, "world"), nullptr);
body->pos[2] = 1.0;
mjs_addFreeJoint(body);
mjsGeom* geom = mjs_addGeom(body, nullptr);
geom->type = mjGEOM_SPHERE;
geom->size[0] = 0.1;

mjModel* model = mj_compile(spec, nullptr);
if (!model) printf("%s\n", mjs_getError(spec));
mj_deleteSpec(spec);

2. Building Hierarchies: Bodies, Geoms, Joints & Sites

Adding Elements

body = spec.worldbody.add_body(name='arm', pos=[0, 0, 0.5])

# Geom with capsule shape
geom = body.add_geom(
    name='arm_geom',
    type=mujoco.mjtGeom.mjGEOM_CAPSULE,
    size=[0.05, 0.25],
    rgba=[0.2, 0.6, 1.0, 1.0],
)

# Hinge joint
joint = body.add_joint(
    name='shoulder',
    type=mujoco.mjtJoint.mjJNT_HINGE,
    axis=[0, 1, 0],
    range=[-1.57, 1.57],
)

# Sensor site and camera
site = body.add_site(name='grip_site', pos=[0, 0, 0.25])
cam = body.add_camera(name='tool_cam', pos=[0, -0.5, 0.25], xyaxes=[1, 0, 0, 0, 0, 1])

Geom Dimensions Reference

Geom Type size Parameter Semantics
mjGEOM_SPHERE [radius]
mjGEOM_CAPSULE [radius, half_length] (or [radius] if using fromto)
mjGEOM_CYLINDER [radius, half_length] (or [radius] if using fromto)
mjGEOM_BOX [half_x, half_y, half_z]
mjGEOM_ELLIPSOID [radius_x, radius_y, radius_z]
mjGEOM_PLANE [half_x, half_y, grid_spacing]

[!WARNING]

When fromto=[x1, y1, z1, x2, y2, z2] is specified for capsules or cylinders, size takes only one value ([radius]); length is calculated from endpoints.

Orientation Keywords

Only one orientation specification can be used at a time:

  • quat=[w, x, y, z] (native representation)
  • euler=[a1, a2, a3] (sequence set by spec.compiler.eulerseq, default 'xyz')
  • axisangle=[x, y, z, angle]
  • xyaxes=[x_x, x_y, x_z, y_x, y_y, y_z]
  • zaxis=[x, y, z]

Angles in euler and axisangle are in degrees unless spec.compiler.degree = False.

3. Sub-model Composition & Attachments

Compose models modularly by attaching sub-specs with prefix namespaces:

parent = mujoco.MjSpec()
child = mujoco.MjSpec()

child_body = child.worldbody.add_body(name='gripper')
child_body.add_geom(size=[0.05], type=mujoco.mjtGeom.mjGEOM_SPHERE)

# Attach child spec to a mount site on the parent
mount_site = parent.worldbody.add_site(name='mount', pos=[0, 0, 1.0])
parent.attach(child, site=mount_site, prefix='gripper_')
# Child body is renamed to 'gripper_gripper' in parent

Cross-Spec Equality Constraints

When linking bodies from two different child specs (e.g. dual-arm setups), attach both children to the common parent first, then declare constraints on the parent spec:

left_mount = parent.worldbody.add_frame(pos=[0, 0.3, 0])
right_mount = parent.worldbody.add_frame(pos=[0, -0.3, 0])
parent.attach(arm1_spec, frame=left_mount, prefix='left_')
parent.attach(arm2_spec, frame=right_mount, prefix='right_')

eq = parent.add_equality(
    type=mujoco.mjtEq.mjEQ_CONNECT,
    objtype=mujoco.mjtObj.mjOBJ_BODY,
    name1='left_hand',
    name2='right_hand',
)
eq.data[0:3] = [0, 0, 0]  # connect anchor, in name1's body frame

[!NOTE]

attach() requires exactly one of frame= or site=; omitting both raises ValueError.

4. Deleting & Dynamically Modifying Elements

Deleting Elements

Pass the element object to spec.delete(); deleting a body removes its subtree:

spec.delete(spec.body('unwanted_body'))
spec.delete(spec.geom('old_geom'))
spec.delete(spec.joint('redundant_joint'))
spec.delete(spec.actuator('motor'))

State-Preserving Recompilation

When changing model architecture at runtime, use spec.recompile to keep existing physics state:

# Add new tool to robot
spec.worldbody.add_geom(name='tool_tip', size=[0.02], type=mujoco.mjtGeom.mjGEOM_SPHERE)

# Recompile keeping positions/velocities intact
model, data = spec.recompile(model, data)

5. Actuators & Servos

Always explicitly set the transmission type (trntype):

# 1. Torque Motor
motor = spec.add_actuator(
    name='torque_actuator',
    target='shoulder',
    trntype=mujoco.mjtTrn.mjTRN_JOINT,
)
motor.set_to_motor()

# 2. Stiff Position Servo (Recommended for position control)
servo = spec.add_actuator(
    name='pos_servo',
    target='shoulder',
    trntype=mujoco.mjtTrn.mjTRN_JOINT,
)
servo.set_to_position(kp=200.0)
servo.forcelimited = True
servo.forcerange = [-50.0, 50.0]     # Peak torque in Nm
servo.ctrllimited = True
servo.ctrlrange = [-1.57, 1.57]      # Target angle in radians

# 3. Velocity Actuator
vel = spec.add_actuator(
    name='vel_driver',
    target='wheel_joint',
    trntype=mujoco.mjtTrn.mjTRN_JOINT,
)
vel.set_to_velocity(kv=10.0)

[!CAUTION]

The helper functions set_to_position() and set_to_motor() configure gains but do not set the transmission. You must always specify trntype=mujoco.mjtTrn.mjTRN_JOINT (or another valid transmission type).

6. Default Classes (Inheritance & Styling)

Default classes (MjsDefault) provide cascaded styling and properties:

# Create child class under root 'main' class
main_class = spec.default
rubber = spec.add_default('rubber', main_class)
rubber.geom.friction = [1.5, 0.01, 0.001]
rubber.geom.density = 1200.0

# Assign class at creation
body.add_geom(rubber, name='tire', type=mujoco.mjtGeom.mjGEOM_CYLINDER, size=[0.1, 0.05])

# Assign class post-creation (must pass MjsDefault object, NOT a string)
other_geom.classname = rubber

7. Collisions & Friction Tuning

Collision Filtering Bitmasks

Two geoms collide if and only if: (g1.contype & g2.conaffinity) || (g2.contype & g1.conaffinity)

# Visual-only geom: never collides
body.add_geom(size=[0.1], contype=0, conaffinity=0, group=1)

# Team A collides only with Team B
geom_a = body.add_geom(size=[0.1], contype=1, conaffinity=2)
geom_b = body.add_geom(size=[0.1], contype=2, conaffinity=1)

Contact Dimension (condim) & Friction

condim Active Friction Components Friction Index Used
1 Normal force only (frictionless) —
3 Tangential sliding friction friction[0]
4 Tangential + torsional (twisting) friction[0:2]
6 Tangential + torsional + rolling friction[0:3]
# High-friction contact for grasping
finger.add_geom(
    type=mujoco.mjtGeom.mjGEOM_CAPSULE,
    size=[0.01, 0.03],
    condim=4,
    friction=[1.2, 0.02, 0.0001],
)

8. Procedural Meshes, Materials & Sensors

Procedural Meshes

vertices = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, 1]], dtype=np.float32)
faces = np.array([[0, 2, 1], [0, 1, 3], [0, 3, 2], [1, 2, 3]], dtype=np.int32)
mesh = spec.add_mesh(name='tetra', uservert=vertices.flatten(), userface=faces.flatten())
body.add_geom(type=mujoco.mjtGeom.mjGEOM_MESH, meshname='tetra')

Procedural Sensors

# IMU Accelerometer and Gyroscope
spec.add_sensor(name='torso_acc', type=mujoco.mjtSensor.mjSENS_ACCELEROMETER,
                objtype=mujoco.mjtObj.mjOBJ_SITE, objname='imu_site')
spec.add_sensor(name='torso_gyro', type=mujoco.mjtSensor.mjSENS_GYRO,
                objtype=mujoco.mjtObj.mjOBJ_SITE, objname='imu_site')

9. Common Gotchas & Best Practices

  1. Non-zero geom sizes: Geoms with radius or half-extents of zero cause immediate compilation errors.
  2. classname takes an object: geom.classname = 'rubber' raises a TypeError. Pass the MjsDefault instance: geom.classname = rubber.
  3. Always assign recompile(): spec.recompile(m, d) produces newly allocated model and data instances. Reassign them: m, d = spec.recompile(m, d).

10. Key References

Documentation

Source Code Examples