A model with bodies and joints falls over. A robot model also has the means to act on itself, to measure itself, and to couple its parts. MJCF gives each of these a section of the file, outside the body tree:
| Section | Elements | Role |
|---|---|---|
<actuator> |
general, motor, position, velocity, intvelocity, damper, cylinder, muscle, adhesion, pid, orientation, dcmotor, plugin |
turn data.ctrl into forces |
<sensor> |
40+ types, from jointpos to camprojection |
write measurements into data.sensordata |
<tendon> |
fixed, spatial |
lengths that combine joints, or paths through sites |
<equality> |
connect, weld, joint, tendon, flex, flexvert, flexstrain |
constraints that tie things together |
<keyframe> |
key |
named states to reset to |
The lists are the element names in the 3.14.0 XML reference. You will use a handful of each; the point of this lesson is to understand the one mechanism behind all actuators, so that any of them can be read as an instance of it.
The lab runs the gantry gripper: three position servos on slide joints, plus one position servo on a tendon that drives two fingers, which an equality constraint keeps symmetric. Move the sliders (they write data.ctrl) and watch the actuator forces. Close the gripper on the cube and watch the grip force saturate at its forcerange.
```lab simlab {“dock”: true, “title”: “Four actuators, one tendon, one equality”, “model”: “gantry_gripper”, “key”: 0, “height”: 250, “autoplay”: true, “camera”: {“azimuth”: -60, “elevation”: 20, “distance”: 0.9, “target”: [0.1, 0, 0.15]}, “toggles”: [“contacts”, “forces”, “joints”], “controls”: [ {“label”: “ctrl[0]: carriage x target”, “unit”: “m”, “set”: “data.ctrl[0]”, “min”: -0.3, “max”: 0.3, “step”: 0.005, “value”: 0.12}, {“label”: “ctrl[2]: carriage z target”, “unit”: “m”, “set”: “data.ctrl[2]”, “min”: -0.34, “max”: 0.05, “step”: 0.005, “value”: 0.0}, {“label”: “ctrl[3]: grip half-opening”, “unit”: “m”, “set”: “data.ctrl[3]”, “min”: 0, “max”: 0.04, “step”: 0.001, “value”: 0.04} ], “readouts”: [ {“label”: “z servo force (N)”, “expr”: “data.actuator_force[2]”, “digits”: 2}, {“label”: “grip force (N)”, “expr”: “data.actuator_force[3]”, “digits”: 2}, {“label”: “left finger (m)”, “expr”: “data.qpos[3]”, “digits”: 4}, {“label”: “right finger (m)”, “expr”: “data.qpos[4]”, “digits”: 4}, {“label”: “tendon length (m)”, “expr”: “data.ten_length[0]”, “digits”: 4}, {“label”: “cube z (m)”, “expr”: “data.qpos[7]”, “digits”: 4}], “plots”: [{“ylabel”: “actuator force (N)”, “window”: 6, “traces”: [ {“label”: “grip (N)”, “expr”: “data.actuator_force[3]”}, {“label”: “z servo (N)”, “expr”: “data.actuator_force[2]”, “dash”: true}]}], “note”: “Lower the carriage to about -0.33 m with the gripper open, close it to 0, then raise the carriage slowly. Raising it in one jump makes the servo accelerate the cube harder than friction can hold it: a preview of Level 10.1.”}
## The actuator model
> [!established] One force law for SISO actuators
> MuJoCo's documentation writes the force of a single-input single-output actuator as
> $$p = a\,u + b_0 + b_1\, l + b_2\, \dot l,$$
> where $u$ is the control (or the activation $w$, for actuators with internal state), $l$ and $\dot l$ are the actuator's length and velocity at its transmission, $a$ is `actuator_gainprm[0]` and $b_0, b_1, b_2$ are `actuator_biasprm[0:3]`. The force is mapped to joints through the transmission's moment arms: `qfrc_actuator` $= \sum_k \nabla l_k(q)\, p_k$. For a joint transmission with `gear` $g$, $l = g q$ and the joint torque is $g p$.
Every shortcut element is this law with particular parameters:
| Element | $a$ (gain) | $b_0, b_1, b_2$ (bias) | So the force is |
|---|---|---|---|
| `motor` | 1 | 0, 0, 0 | $p = u$ (a torque or force source) |
| `position kp kv` | $k_p$ | 0, $-k_p$, $-k_v$ | $p = k_p(u - l) - k_v \dot l$ (a PD servo to target $u$) |
| `velocity kv` | $k_v$ | 0, 0, $-k_v$ | $p = k_v(u - \dot l)$ (a P servo on velocity) |
| `general dyntype="filter" dynprm="τ" gainprm="a"` | $a$ | as set | $p = a w$, with $\dot w = (u - w)/\tau$ |
Then three clamps apply, in this order of meaning: `ctrlrange` clamps $u$ (if `ctrllimited`), `actrange` clamps the activation, `forcerange` clamps $p$. With the default `autolimits`, giving a range turns its clamp on.
```io
INPUT: a bench of four hinges, one per actuator type; the gantry gripper
PROCESS: set state and controls, `mj_forward`, compare `actuator_force` with the law; exceed the ranges; step a filter; close the gripper
OUTPUT: measured forces next to the formula, clamped values, activation versus the exact exponential, finger positions
```python file=examples/l2_3_actuators.py “"”Lesson 2.3: actuator force laws, clamping, activation dynamics, coupling.
INPUT an actuator test bench written in this file (one hinge per actuator), and gantry_gripper.xml for tendon and equality coupling PROCESS (1) put every joint at a known angle and velocity, set controls, run mj_forward, and compare actuator_force with the documented law p = au + b0 + b1l + b2ldot (with gear, l = gearq); (2) push controls past ctrlrange and forces past forcerange; (3) watch a filter actuator’s activation approach its control; (4) close the gripper and check both fingers move together OUTPUT tables of measured force next to the formula
Run: python examples/l2_3_actuators.py “””
import math
import mujoco import numpy as np
from mjcourse import model_path
BENCH = “””
”””
def force_laws() -> None: model = mujoco.MjModel.from_xml_string(BENCH) data = mujoco.MjData(model) q, qd = 0.3, -0.5 data.qpos[:] = q data.qvel[:] = qd data.ctrl[:] = [0.4, 0.5, 1.0, 0.8] mujoco.mj_forward(model, data) expected = { “motor”: 0.4, # gain 1: force = ctrl; the joint torque is gear * force “position”: 10 * (0.5 - q) - 1 * qd, # kp (ctrl - q) - kv qdot “velocity”: 3 * (1.0 - qd), # kv (ctrl - qdot) “filter”: 5 * data.act[0], # gain * activation (activation starts at 0) } print(f” {‘actuator’:<9}{‘gainprm[0]’:>11}{‘biasprm[0:3]’:>22}{‘actuator_force’:>16}{‘formula’:>10}{‘qfrc_actuator’:>15}”) for i in range(model.nu): name = model.actuator(i).name print(f” {name:<9}{model.actuator_gainprm[i, 0]:>11.3g}{np.array2string(model.actuator_biasprm[i, :3], precision=3):>22}” f”{data.actuator_force[i]:>16.4f}{expected[name]:>10.4f}{data.qfrc_actuator[i]:>15.4f}”) print(“ (the motor’s actuator_force is in actuator space, 0.4; the joint torque is gear * 0.4 = 0.8)”)
def clamping() -> None: model = mujoco.MjModel.from_xml_string(BENCH) data = mujoco.MjData(model) data.ctrl[0] = 5.0 # beyond ctrlrange [-1, 1] data.ctrl[1] = 2.0 # target 2 rad from q = 0: kp * 2 = 20 > forcerange 3 mujoco.mj_forward(model, data) print(f” motor ctrl 5.0 with ctrlrange [-1, 1] -> actuator_force {data.actuator_force[0]:.3f} “ f”(data.ctrl still reads {data.ctrl[0]})”) print(f” position target 2 rad, kp 10, forcerange [-3, 3] -> actuator_force {data.actuator_force[1]:.3f}”)
def activation() -> None: model = mujoco.MjModel.from_xml_string(BENCH) data = mujoco.MjData(model) data.ctrl[3] = 1.0 tau = model.actuator_dynprm[3, 0] for t_mark in (0.05, 0.1, 0.2, 0.5): while data.time < t_mark - 1e-9: mujoco.mj_step(model, data) print(f” t = {data.time:.2f} s: act = {data.act[0]:.4f}, 1 - exp(-t/tau) = {1 - math.exp(-data.time / tau):.4f}”)
def coupling() -> None: model = mujoco.MjModel.from_xml_path(str(model_path(“gantry_gripper”))) data = mujoco.MjData(model) mujoco.mj_resetDataKeyframe(model, data, 0) data.ctrl[model.actuator(“gripper/grip”).id] = 0.03 for _ in range(1000): mujoco.mj_step(model, data) left, right = data.joint(“gripper/finger_left”).qpos[0], data.joint(“gripper/finger_right”).qpos[0] print(f” grip command 0.03 m: finger_left {left:.5f} m, finger_right {right:.5f} m, “ f”tendon length {data.ten_length[0]:.5f} m”)
if name == “main”: print(“force laws at q = 0.3 rad, qdot = -0.5 rad/s:”) force_laws() print(“clamping:”) clamping() print(“filter activation, time constant 0.1 s, control stepped to 1 at t = 0:”) activation() print(“gripper coupling (tendon + joint equality):”) coupling()
Output:
```text
force laws at q = 0.3 rad, qdot = -0.5 rad/s:
actuator gainprm[0] biasprm[0:3] actuator_force formula qfrc_actuator
motor 1 [0. 0. 0.] 0.4000 0.4000 0.8000
position 10 [ 0. -10. -1.] 2.5000 2.5000 2.5000
velocity 3 [ 0. 0. -3.] 4.5000 4.5000 4.5000
filter 5 [0. 0. 0.] 0.0000 0.0000 0.0000
(the motor's actuator_force is in actuator space, 0.4; the joint torque is gear * 0.4 = 0.8)
clamping:
motor ctrl 5.0 with ctrlrange [-1, 1] -> actuator_force 1.000 (data.ctrl still reads 5.0)
position target 2 rad, kp 10, forcerange [-3, 3] -> actuator_force 3.000
filter activation, time constant 0.1 s, control stepped to 1 at t = 0:
t = 0.05 s: act = 0.3965, 1 - exp(-t/tau) = 0.3935
t = 0.10 s: act = 0.6358, 1 - exp(-t/tau) = 0.6321
t = 0.20 s: act = 0.8674, 1 - exp(-t/tau) = 0.8647
t = 0.50 s: act = 0.9936, 1 - exp(-t/tau) = 0.9933
gripper coupling (tendon + joint equality):
grip command 0.03 m: finger_left 0.03000 m, finger_right 0.03000 m, tendon length 0.03000 m
Three things in this output are worth stopping on.
actuator_force lives in actuator space. For the motor with gear="2", actuator_force is 0.4 and the joint torque in qfrc_actuator is 0.8. Code that logs “motor torque” from actuator_force is off by the gear ratio.
Clamping does not rewrite ctrl. The motor’s control is clamped to 1 inside the computation, but data.ctrl[0] still reads 5.0. If a policy’s actions are logged from ctrl, the log records what was asked, not what was applied. Clip actions yourself if the log must show the applied value.
The filter activation is integrated by Euler. It runs slightly ahead of the exact exponential (0.3965 against 0.3935 at 50 ms). The documentation provides dyntype="filterexact" for the exact update; it also warns that Euler-integrated filters diverge when the time constant is smaller than the timestep.
[!implementation] Actuators with more than one input Since 3.11, an actuator can take several controls: an
orientationservo commanded by a quaternion has 4 inputs and 3 force outputs, andpidanddcmotorcan take position, velocity and feedforward setpoints together. The model then distinguishesnactuator(actuators),nu(total controls, the length ofctrl) andnout(total force outputs). For the single-input actuators in this course all three are equal.
A sensor reads a quantity from mjData after the stage that computes it and writes it into data.sensordata, at offset model.sensor_adr[i] with model.sensor_dim[i] numbers. Named access is easier: data.sensor("tip_pos").data.
| Family | Examples | Computed in |
|---|---|---|
| joint and actuator | jointpos, jointvel, actuatorfrc, jointactuatorfrc, jointlimitfrc |
position, velocity or acceleration stage, depending on the quantity |
| frame | framepos, framequat, framelinvel, frameangvel, framelinacc |
position or velocity stage |
| inertial (site-mounted) | accelerometer, gyro, velocimeter, magnetometer |
acceleration or velocity stage |
| force and touch | force, torque (at a site, from the constraint forces on the subtree), touch, tactile |
acceleration stage |
| geometric | rangefinder, distance, normal, fromto, contact, insidesite, camprojection |
position stage |
| bookkeeping | clock, e_potential, e_kinetic, subtreecom |
various |
Remember Lesson 0.3: sensors are ideal. The noise attribute is stored, not applied. The nsample, delay and interval attributes, in contrast, are applied: they give a sensor a history buffer, a fixed delay and a sampling period, which is the honest way to model a slow or late sensor (Level 19).
A fixed tendon is a linear combination of joint positions, $l = \sum_j c_j q_j$. The gripper’s grip tendon is $\tfrac12(q_\text{left} + q_\text{right})$, so a single actuator on the tendon drives the average opening. A spatial tendon is a path through sites, optionally wrapping around spheres and cylinders, used for cables and muscles.
An equality constraint forces a relation between two things: connect joins two bodies at a point (a ball joint that closes a loop), weld fixes their relative pose, joint couples two joints by a polynomial, tendon does the same for tendons. The gripper’s mirror equality says $q_\text{left} = q_\text{right}$. Equality constraints are soft, like contacts (their own solref and solimp), which is why the output shows the fingers equal to five decimals rather than exactly.
[!recommendation] Drive the average, constrain the difference The gripper pattern (one actuator on a tendon that averages the fingers, one equality that keeps them symmetric) is the standard way to model an underactuated parallel gripper with a single motor. Driving one finger and coupling the other only by the equality works too, but then the equality has to transmit the full grip force, and a soft constraint transmitting large forces stretches visibly.
A <key> stores a named state: qpos, qvel, act, ctrl, mocap poses (mpos, mquat) and time; anything omitted takes its default (for qpos, the model’s qpos0). mujoco.mj_resetDataKeyframe(model, data, k) restores keyframe k. When a scene attaches a robot that has keyframes, the robot’s keyframes come along and are padded with the defaults of the rest of the scene, which is how this course’s scenes get their home pose for free.
“The servo does nothing.” A position actuator with the default kp="1" on a 2 kg link produces 1 N·m per radian of error, less than gravity. Read the gains back from actuator_gainprm and actuator_biasprm and compare them with the load.
“The robot moves although ctrl is zero.” A position servo with ctrl = 0 is commanding position 0, not “no force”. Use a motor if zero control should mean zero force, or initialize ctrl to the current position.
“The two fingers drift apart under load.” The equality constraint is soft. Stiffen its solref (smaller time constant) or reduce the force it has to carry, as in the recommendation above.
Replace the gripper’s position actuator with a general actuator that has exactly the same behaviour, writing out gainprm and biasprm yourself, and verify with a script that the two models produce identical trajectories for the same control sequence.
Add a force and a torque sensor at a site on the gantry carriage, close the gripper on the cube, lift it slowly, and show that the force sensor’s vertical reading rises by the cube’s weight (0.08 kg times 9.81 m/s²) when the cube leaves the floor. Then explain, from the documentation of the force sensor, which subtree’s forces it measures and in which frame.
Action spaces in robot learning are actuator choices. A policy that outputs joint torques for motor actuators, one that outputs joint targets for position actuators, and one that outputs end-effector targets for a site transmission with a refsite are three different control problems with different learning difficulty, even on the same robot. A paper’s action space should be reported as precisely as its network architecture: actuator type, gains, ranges, and control frequency.
{"id": "2.3-check", "title": "Knowledge check", "questions": [
{"kind": "numeric", "q": "A <code>position</code> actuator has kp = 50, kv = 2 and gear 1. The joint is at 0.2 rad moving at 1 rad/s, and ctrl = 0.5. What is <code>actuator_force</code> (N m)?",
"answer": 13, "tol": 0.001, "unit": "N m",
"explain": "<p>$p = k_p(u - q) - k_v \\dot q = 50 \\times 0.3 - 2 \\times 1 = 13$.</p>"},
{"kind": "mcq", "q": "A motor has <code>gear=\"10\"</code> and ctrl = 1.5. What is the joint torque in <code>qfrc_actuator</code>?",
"options": ["0.15", "1.5", "15", "It depends on the joint velocity"],
"answer": 2,
"explain": "<p>The actuator force is $p = u = 1.5$ in actuator space; the transmission multiplies by the gear: 15 N m.</p>"},
{"kind": "mcq", "q": "A policy outputs ctrl = 3 for an actuator with ctrlrange [-1, 1]. What does <code>data.ctrl</code> hold after <code>mj_step</code>?",
"options": ["1", "3", "0", "NaN"],
"answer": 1,
"explain": "<p>MuJoCo clamps internally when computing the force and leaves <code>ctrl</code> as written. Logs of <code>ctrl</code> show the request, not the applied value.</p>"},
{"kind": "open", "q": "Write the <code>general</code> actuator equivalent to <code><velocity joint=\"j\" kv=\"4\"/></code>.",
"reference": "<p><code><general joint=\"j\" gainprm=\"4\" biastype=\"affine\" biasprm=\"0 0 -4\"/></code>, giving $p = 4u - 4\\dot l$. The trap: for <code>general</code>, <code>gaintype</code> defaults to <code>fixed</code> but <code>biastype</code> defaults to <code>none</code>, and with <code>biastype=\"none\"</code> the <code>biasprm</code> values are compiled into the model and then ignored. Checked in 3.14.0: without <code>biastype</code>, <code>actuator_biastype</code> is 0 (none) although <code>actuator_biasprm</code> holds the three numbers.</p>"}
]}
Lesson 2.4 composes models from parts (include, attach, prefixes), reads compiler errors, and inspects what the compiler actually produced.