- name
- mujoco-spec-editing
- description
- MuJoCo procedural model authoring, editing, and tuning with MjSpec (`mujoco.MjSpec` in Python and `mjSpec` in C++): bodies, geoms, joints, sites, cameras, sub-model attachment with prefix namespaces, actuators (position servos, motors, velocity drives, trntype), baseline option/default overrides (implicitfast, elliptic cones, solref, solimp, armature, damping), collision bitmasks (contype, conaffinity), friction (condim), default classes (MjsDefault), procedural meshes, deletion, spec recompilation (spec.recompile). Use only for authoring or tuning MuJoCo MJCF/MjSpec models, adding bodies/geoms/actuators, and attaching sub-models. Don't use for designing custom non-MuJoCo XML/scene-graph APIs when MuJoCo `MjSpec` is not used, stepping simulations (mujoco-python), or rendering (mujoco-rendering).
# MuJoCo Spec Editing: Building & Modifying Models
> [!TIP]
>
> **Official MuJoCo documentation (`doc/*.rst`):** Read short chapters like
> [`../../programming/modeledit.rst`](../../programming/modeledit.rst)
> (`.. _meOverview:`) and [`../../overview.rst`](../../overview.rst) directly
> when in a MuJoCo checkout; for large reference files like
> [`../../modeling.rst`](../../modeling.rst) (`.. _ProceduralModeling:`,
> `.. _Mechanisms:`, `.. _CTuning:`),
> [`../../XMLreference.rst`](../../XMLreference.rst),
> [`../../python.rst`](../../python.rst) (`.. _PyModelEdit:`), and
> [`../../APIreference/functions.rst`](../../APIreference/functions.rst), grep
> the literal `.. _Label:` anchor. Outside a checkout, the same `doc/*.rst` tree
> ships offline in `pip install mujoco` at
> `pathlib.Path(mujoco.__file__).parent / 'doc'`; otherwise fetch
> `https://mujoco.readthedocs.io/en/latest/<path>.html` (or `/en/stable/` for
> released pages; `functions.rst` renders inside
> `APIreference/APIfunctions.html`, and Sphinx anchors lowercase the label,
> e.g. `.. _meOverview:` → `#meoverview`) and browse repo paths (`../../../`)
> under `https://github.com/google-deepmind/mujoco/tree/main/`.
>
> **Related skills:**
>
> - [mujoco-python](../python/SKILL.md) — Simulation lifecycle, stepping, and
> diagnostics.
> - [mujoco-rendering](../rendering/SKILL.md) — Offscreen image rendering and
> camera setup.
> - [mujoco-studio](../studio/SKILL.md) — Interactive viewers and Studio
> plugins.
## 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.
```python
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++:
```cpp
#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
```python
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` (see [modeling.rst](../../modeling.rst),
`.. _COrientation:`, and [XMLreference.rst](../../XMLreference.rst),
`.. _body-geom:`).
## 3. Sub-model Composition & Attachments
Compose models modularly by attaching sub-specs with prefix namespaces (see
[programming/modeledit.rst](../../programming/modeledit.rst),
`.. _meAttachment:` and `.. _meAttributeMerging:`):
```python
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:
```python
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:
```python
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:
```python
# 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`):
```python
# 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, dampratio=1.0) # Pair with implicitfast (kv defaults to 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). See
> [modeling.rst](../../modeling.rst) (`.. _CActuators:`, `.. _CActShortcuts:`)
> and [XMLreference.rst](../../XMLreference.rst) (`.. _actuator:`).
## 6. Default Classes (Inheritance & Styling)
Default classes (`MjsDefault`) provide cascaded styling and properties (see
[modeling.rst](../../modeling.rst), `.. _CDefault:`, and
[programming/modeledit.rst](../../programming/modeledit.rst), `.. _meDefault:`):
```python
# 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
```
### Baseline Tuning Overrides (`<option>` & Root `<default>`)
Several internal defaults (`Euler`, `pyramidal` cones with `impratio=1`, soft
`solref="0.02 1"`, zero joint `armature`/`damping`) are kept for backwards
compatibility rather than new robotics models. Override them at the top of a
project (full rationale and priority-ordered parameter guide in
[modeling.rst](../../modeling.rst), `.. _CTuning:`, `.. _CTuningDefaults:`,
`.. _CTuningGlobal:`, `.. _CTuningModel:`):
```python
spec.option.integrator = mujoco.mjtIntegrator.mjINT_IMPLICITFAST # DISCRETE for flex elasticity
spec.option.cone = mujoco.mjtCone.mjCONE_ELLIPTIC
spec.option.impratio = 10.0
spec.default.geom.solref = [0.005, 1.0] # Keep solref[0] >= 2 * timestep
spec.default.joint.armature = 0.01 # Reflected rotor inertia (I_rotor * N^2)
spec.default.joint.damping = 0.1 # Implicit in M + h*D at zero solver cost
```
View on GitHub