.. _actuators: Actuators ========= Actuators convert high-level commands (position, velocity, effort) into low-level efforts that drive joints. They are configured through the ``articulation`` field of :ref:`EntityCfg `. mjlab provides **built-in** actuators that leverage the physics engine's implicit integration for best stability, and **explicit** actuators for custom control laws and actuator dynamics. Quick start ----------- Basic PD control with ``BuiltinPositionActuator``, the most common starting point. .. code-block:: python from mjlab.actuator import BuiltinPositionActuatorCfg from mjlab.entity import EntityCfg, EntityArticulationInfoCfg robot_cfg = EntityCfg( spec_fn=lambda: load_robot_spec(), articulation=EntityArticulationInfoCfg( actuators=( BuiltinPositionActuatorCfg( target_names_expr=(".*_hip_.*", ".*_knee_.*"), stiffness=80.0, damping=10.0, effort_limit=100.0, ), ), ), ) Add delay fields directly on any actuator config to model communication latency. .. code-block:: python from mjlab.actuator import BuiltinPositionActuatorCfg BuiltinPositionActuatorCfg( target_names_expr=(".*",), stiffness=80.0, damping=10.0, delay_min_lag=2, # Minimum 2 physics steps delay_max_lag=5, # Maximum 5 physics steps ) Built-in vs explicit actuators ------------------------------ The key design decision when configuring actuators is whether to use **built-in** or **explicit** types. The difference comes down to how MuJoCo's integrator handles velocity-dependent forces. **Built-in actuators** (``BuiltinPositionActuator``, ``BuiltinVelocityActuator``, ``BuiltinMotorActuator``, ``BuiltinPdActuator``, ``BuiltinDcMotorActuator``, ``BuiltinMuscleActuator``) create native MuJoCo actuator elements in the MjSpec. The physics engine computes the control law and integrates velocity-dependent damping forces implicitly. This provides the best numerical stability, particularly with high gains or large timesteps. **Explicit actuators** (``IdealPdActuator``, ``DcMotorActuator``, ``LearnedMlpActuator``) compute torques in user code and forward them through a ```` actuator acting as a passthrough. Because the integrator cannot account for the velocity derivatives of these externally computed forces, they are less numerically robust than built-in types. Use explicit actuators when you need custom control laws or actuator dynamics that cannot be expressed with built-in types (e.g., velocity-dependent torque limits, learned actuator networks). The two approaches match closely in the linear, unconstrained regime at small timesteps. At larger timesteps or higher gains, built-in actuators are more forgiving. **Integrator choice.** mjlab places damping inside the actuator rather than in joints. The ``euler`` integrator treats joint damping implicitly but actuator damping explicitly, limiting stability. The ``implicitfast`` integrator treats all known velocity-dependent forces implicitly, handling both proportional and damping terms of the actuator without additional cost. .. note:: mjlab defaults to ``implicitfast``, as it is MuJoCo's recommended integrator and provides superior stability for actuator-side damping. Actuator types -------------- All actuator configs share a few common fields inherited from ``ActuatorCfg``: - ``target_names_expr``: Tuple of regex patterns matched against joint names (or tendon/site names when using a different ``transmission_type``). - ``armature``: Reflected rotor inertia added to the target joint. - ``frictionloss``: Static friction (stiction) modeled as a constraint on the target joint. See MuJoCo's `frictionloss `_. Built-in actuators ^^^^^^^^^^^^^^^^^^ Built-in actuators use MuJoCo's native actuator types via the MjSpec API. **BuiltinPositionActuator**: Creates ```` actuators for PD control. **BuiltinVelocityActuator**: Creates ```` actuators for velocity control. **BuiltinMotorActuator**: Creates ```` actuators for direct torque control. **BuiltinPdActuator**: Native PD that closes on both a position and a velocity target, implemented as paired ```` + ```` actuators summing to ``kp * (p_target - q) + kd * (v_target - qdot)``. ``BuiltinPositionActuator`` puts kd on the ```` element and implicitly assumes a zero velocity reference; use this when the policy emits a non-zero velocity target. Native delivery lets ``implicit`` / ``implicitfast`` see the kd term in their velocity update, unlike ``IdealPdActuator`` which forwards Python-computed torque through an opaque ````. **BuiltinDcMotorActuator**: Wraps MuJoCo's native ` `_ element. Torque is ``tau = K * (V - K * omega) / R``; the back-EMF runs through the native bias path, so ``implicit`` / ``implicitfast`` pick up its velocity derivative as effective damping. Three input modes pick what ``ctrl`` carries: VOLTAGE drives the motor directly; POSITION / VELOCITY close an internal PID (with anti-windup and slew limiting) against a single setpoint, whose Vmax-clamped output becomes torque. POSITION mode pins v_target = 0 (the kd term acts on raw velocity). Optional physics: inductance, thermal model with I^2R heating, cogging ripple, LuGre friction. ``DcMotorActuator`` (the explicit version) is a software PD with a velocity-dependent torque clamp on top of a ````; this is the real electrical model. **BuiltinMuscleActuator**: Creates ```` actuators for biologically-inspired muscle dynamics with force-length-velocity characteristics. .. code-block:: python from mjlab.actuator import BuiltinPositionActuatorCfg, BuiltinVelocityActuatorCfg # Mobile manipulator: PD for arm joints, velocity control for wheels. actuators = ( BuiltinPositionActuatorCfg( target_names_expr=(".*_shoulder_.*", ".*_elbow_.*", ".*_wrist_.*"), stiffness=100.0, damping=10.0, effort_limit=150.0, ), BuiltinVelocityActuatorCfg( target_names_expr=(".*_wheel_.*",), damping=20.0, effort_limit=50.0, ), ) Explicit actuators ^^^^^^^^^^^^^^^^^^ Explicit actuators compute efforts and forward them to an underlying ```` actuator acting as a passthrough. See `Built-in vs explicit actuators`_ above for stability implications. **IdealPdActuator**: Implements an ideal PD controller. Computes torques as ``tau = Kp * pos_error + Kd * vel_error``. **DcMotorActuator**: Extends ``IdealPdActuator`` with velocity-dependent torque saturation to model DC motor torque-speed curves (back-EMF effects). Implements a linear torque-speed curve: maximum torque at zero velocity, zero torque at maximum velocity. **LearnedMlpActuator**: Neural network-based actuator that uses a trained MLP to predict torque outputs from joint state history. Useful when analytical models cannot capture complex actuator dynamics like delays, nonlinearities, and friction effects. Inherits DC motor velocity-based torque limits. .. code-block:: python from mjlab.actuator import IdealPdActuatorCfg, DcMotorActuatorCfg # Ideal PD for hips, DC motor model with torque-speed curve for knees. actuators = ( IdealPdActuatorCfg( target_names_expr=(".*_hip_.*",), stiffness=80.0, damping=10.0, effort_limit=100.0, ), DcMotorActuatorCfg( target_names_expr=(".*_knee_.*",), stiffness=80.0, damping=10.0, effort_limit=25.0, # Continuous torque limit saturation_effort=50.0, # Peak torque at stall velocity_limit=30.0, # No-load speed (rad/s) ), ) XML actuators ^^^^^^^^^^^^^ XML actuators wrap actuators already defined in your robot's XML file. The config finds existing actuators by matching their ``target`` joint name against the ``target_names_expr`` patterns. Each joint must have exactly one matching actuator. **XmlActuator**: Wraps any actuator already defined in the XML. The actuator type (position, velocity, motor, muscle) is auto detected from the XML element, or you can set ``command_field`` explicitly. .. code-block:: python from mjlab.actuator import XmlActuatorCfg # Robot XML already has: # # # # Wrap existing XML actuators. actuators = ( XmlActuatorCfg(target_names_expr=("hip_joint",)), ) Actuator delays ^^^^^^^^^^^^^^^ Any actuator config supports inline delay fields for modeling command latency. On a real robot, the onboard PD loop runs at KHz with direct encoder access, but the position target from the policy arrives late due to inference time and communication bus cycles. Actuator delay models this: the command target is delayed, but the control law still sees fresh joint state. This is distinct from observation delay, which models sensor pipeline latency (stale state going into the policy). Together they cover both legs of the round trip: sensor to policy to motor. .. code-block:: python from mjlab.actuator import IdealPdActuatorCfg # Add 2-5 step delay to position commands. actuators = ( IdealPdActuatorCfg( target_names_expr=(".*",), stiffness=80.0, damping=10.0, delay_min_lag=2, delay_max_lag=5, delay_hold_prob=0.3, # 30% chance to keep current lag delay_update_period=10, # Resample lag every 10 steps ), ) Each step, a lag is sampled uniformly from ``[delay_min_lag, delay_max_lag]``. Delays are quantized to physics timesteps. For example, with 500Hz physics (2ms/step), ``delay_min_lag=2`` represents a 4ms minimum delay. Authoring actuator configs -------------------------- Since actuator parameters are uniform within each config, use separate actuator configs for joints that need different parameters: .. code-block:: python from mjlab.actuator import BuiltinPositionActuatorCfg # G1 humanoid with different gains per joint group. G1_ACTUATORS = ( BuiltinPositionActuatorCfg( target_names_expr=(".*_hip_.*", "waist_yaw_joint"), stiffness=180.0, damping=18.0, effort_limit=88.0, armature=0.0015, ), BuiltinPositionActuatorCfg( target_names_expr=("left_hip_pitch_joint", "right_hip_pitch_joint"), stiffness=200.0, damping=20.0, effort_limit=88.0, armature=0.0015, ), BuiltinPositionActuatorCfg( target_names_expr=(".*_knee_joint",), stiffness=150.0, damping=15.0, effort_limit=139.0, armature=0.0025, ), BuiltinPositionActuatorCfg( target_names_expr=(".*_ankle_.*",), stiffness=40.0, damping=5.0, effort_limit=25.0, armature=0.0008, ), ) This design choice reflects a deliberate simplification in mjlab: each ``ActuatorCfg`` represents a single actuator type (e.g., a specific motor/gearbox model) applied uniformly across all joints it drives. Hardware parameters such as ``armature`` (reflected rotor inertia) and ``gear`` describe properties of the actuator hardware, even though they are implemented in MuJoCo as joint or actuator fields. In other frameworks (like Isaac Lab), these fields may accept ``float | dict[str, float]`` to support per-joint variation. mjlab instead encourages one config per actuator type or per joint group, keeping the hardware model physically consistent and explicit. The main trade-off is verbosity in special cases, such as parallel linkages, where per-joint overrides could have been convenient, but the benefit is clearer semantics and simpler maintenance. See :ref:`actions` for how action terms route policy outputs to actuators (including DifferentialIK for task-space control), and :ref:`domain_randomization` for randomizing gains and effort limits. Computing hardware parameters ------------------------------ This section is relevant when configuring actuators from real motor datasheets. If you are using manually tuned gains, you can skip ahead. mjlab provides utilities in ``mjlab.utils.actuator`` to compute actuator parameters from physical motor specifications. This is particularly useful for computing reflected inertia (``armature``) and deriving appropriate control gains from hardware datasheets. **Example: Unitree G1 motor configuration** .. code-block:: python from math import pi from mjlab.utils.actuator import ( reflected_inertia_from_two_stage_planetary, ElectricActuator ) # Motor specs from manufacturer datasheet. ROTOR_INERTIAS_7520_14 = ( 0.489e-4, # Motor rotor inertia (kg*m**2) 0.098e-4, # Planet carrier inertia 0.533e-4, # Output stage inertia ) GEARS_7520_14 = ( 1, # First stage (motor to planet) 4.5, # Second stage (planet to carrier) 1 + (48/22), # Third stage (carrier to output) ) # Compute reflected inertia at joint output. # J_reflected = J_motor*(N1*N2)**2 + J_carrier*N2**2 + J_output. ARMATURE_7520_14 = reflected_inertia_from_two_stage_planetary( ROTOR_INERTIAS_7520_14, GEARS_7520_14 ) # Create motor spec container. ACTUATOR_7520_14 = ElectricActuator( reflected_inertia=ARMATURE_7520_14, velocity_limit=32.0, # rad/s at joint effort_limit=88.0, # N*m continuous torque ) # Derive PD gains from natural frequency and damping ratio. NATURAL_FREQ = 10 * 2*pi # 10 Hz bandwidth. DAMPING_RATIO = 2.0 # Overdamped, see note below. STIFFNESS = ARMATURE_7520_14 * NATURAL_FREQ**2 DAMPING = 2 * DAMPING_RATIO * ARMATURE_7520_14 * NATURAL_FREQ # Use in actuator config. from mjlab.actuator import BuiltinPositionActuatorCfg actuator = BuiltinPositionActuatorCfg( target_names_expr=(".*_hip_pitch_joint",), stiffness=STIFFNESS, damping=DAMPING, effort_limit=ACTUATOR_7520_14.effort_limit, armature=ACTUATOR_7520_14.reflected_inertia, ) .. note:: The example uses ``DAMPING_RATIO = 2.0`` (overdamped) rather than the critically damped value of 1.0. This is because the reflected inertia calculation only accounts for the motor's rotor inertia, not the apparent inertia of the links being moved. In practice, the total effective inertia at the joint is higher than just the reflected motor inertia, so using an overdamped ratio provides better stability margins when the true system inertia is underestimated. **Parallel linkage approximation:** For joints driven by parallel linkages (like the G1's ankles with dual motors), the effective armature in the nominal configuration can be approximated as the sum of the individual motor armatures: .. code-block:: python # Two 5020 motors driving ankle through parallel linkage. G1_ACTUATOR_ANKLE = BuiltinPositionActuatorCfg( target_names_expr=(".*_ankle_pitch_joint", ".*_ankle_roll_joint"), stiffness=STIFFNESS_5020 * 2, damping=DAMPING_5020 * 2, effort_limit=ACTUATOR_5020.effort_limit * 2, armature=ACTUATOR_5020.reflected_inertia * 2, ) Extending: custom actuators ---------------------------- All actuators implement a unified ``compute()`` interface that receives an ``ActuatorCmd`` (containing position, velocity, and effort targets) and returns control signals for the low-level MuJoCo actuators driving each joint. **Core interface:** .. code-block:: python def compute(self, cmd: ActuatorCmd) -> torch.Tensor: """Convert high-level commands to control signals. Args: cmd: Command containing position_target, velocity_target, effort_target (each is a [num_envs, num_targets] tensor or None) Returns: Control signals for this actuator ([num_envs, num_targets] tensor) """ **Lifecycle hooks:** - ``edit_spec``: Modify MjSpec before compilation (add actuators, set gains) - ``initialize``: Post-compilation setup (resolve indices, allocate buffers) - ``reset``: Per-environment reset logic - ``update``: Pre-step updates - ``compute``: Convert commands to control signals **Properties:** - ``target_ids``: Tensor of local target indices controlled by this actuator - ``target_names``: List of target names controlled by this actuator - ``ctrl_ids``: Tensor of global control input indices for this actuator ``IdealPdActuator`` is the recommended base class for custom explicit actuators. ``DcMotorActuator`` and ``LearnedMlpActuator`` are both built on top of it and serve as examples of the extension pattern.