Domain Randomization#

Domain randomization varies physical parameters during training so that policies are robust to modeling errors and real-world variation. This guide shows how to attach randomization terms to your environment using EventTerm and mdp.randomize_field.

TL;DR#

Use an EventTerm that calls mdp.randomize_field with a target field, a value range (or per-axis ranges), and an operation describing how to apply the draw.

from mjlab.managers.manager_term_config import EventTermCfg as EventTerm
from mjlab.managers.manager_term_config import term
from mjlab.managers.scene_entity_config import SceneEntityCfg
from mjlab.envs import mdp

foot_friction: EventTerm = term(
    EventTerm,
    mode="reset",  # randomize each episode
    func=mdp.randomize_field,
    domain_randomization=True,  # marks this as domain randomization
    params={
        "asset_cfg": SceneEntityCfg("robot", geom_names=[".*_foot.*"]),
        "field": "geom_friction",
        "ranges": (0.3, 1.2),
        "operation": "abs",
    },
)

Domain Randomization Flag#

When creating an EventTerm for domain randomization, set domain_randomization=True. This allows the environment to track which fields are being randomized:

EventTerm(
    mode="reset",
    func=mdp.randomize_field,
    domain_randomization=True,  # required for DR tracking
    params={"field": "geom_friction", ...},
)

This flag is especially useful when using custom class-based event terms instead of mdp.randomize_field.

Event Modes#

  • "startup" - randomize once at initialization

  • "reset" - randomize at every episode reset

  • "interval" - randomize at regular time intervals

Available Fields#

Joint/DOF: dof_armature, dof_frictionloss, dof_damping, jnt_range, jnt_stiffness, qpos0

Body: body_mass, body_ipos, body_iquat, body_inertia, body_pos, body_quat

Geom: geom_friction, geom_pos, geom_quat, geom_rgba

Site: site_pos, site_quat

Randomization Parameters#

Distribution: "uniform" (default), "log_uniform" (values must be > 0), "gaussian" (mean, std)

Operation: "abs" (default, set), "scale" (multiply), "add" (offset)

Axis selection#

Multi-dimensional fields can be randomized per-axis.

Friction. Geoms have three coefficients [tangential, torsional, rolling]. For condim=3 (standard frictional contact), only axis 0 (tangential) affects contact behavior:

# Tangential friction (affects condim=3)
params={"field": "geom_friction", "ranges": {0: (0.3, 1.2)}}

# Tangential + torsional (torsional matters for condim >= 4)
params={"field": "geom_friction", "ranges": {0: (0.5, 1.0), 1: (0.001, 0.01)}}

# X and Y position
params={"field": "body_pos", "axes": [0, 1], "ranges": (-0.1, 0.1)}

Examples#

Friction (reset)#

foot_friction: EventTerm = term(
    EventTerm,
    mode="reset",
    func=mdp.randomize_field,
    domain_randomization=True,
    params={
        "asset_cfg": SceneEntityCfg("robot", geom_names=[".*_foot.*"]),
        "field": "geom_friction",
        "ranges": (0.3, 1.2),
        "operation": "abs",
    },
)

Note

Give your robot’s collision geoms higher priority than terrain (geom priority defaults to 0). Then you only need to randomize robot friction. MuJoCo will use the higher-priority geom’s friction in (robot, terrain) contacts.

from mjlab.utils.spec_config import CollisionCfg

robot_collision = CollisionCfg(
    geom_names_expr=[".*_foot.*"],
    priority=1,
    friction=(0.6,),
    condim=3,
)

Joint Offset (startup)#

Randomize default joint positions to simulate joint offset calibration errors:

joint_offset: EventTerm = term(
    EventTerm,
    mode="startup",
    func=mdp.randomize_field,
    domain_randomization=True,
    params={
        "asset_cfg": SceneEntityCfg("robot", joint_names=[".*"]),
        "field": "qpos0",
        "ranges": (-0.01, 0.01),
        "operation": "add",
    },
)

Center of Mass (COM) (startup)#

com: EventTerm = term(
    EventTerm,
    mode="startup",
    func=mdp.randomize_field,
    domain_randomization=True,
    params={
        "asset_cfg": SceneEntityCfg("robot", body_names=["torso"]),
        "field": "body_ipos",
        "ranges": {0: (-0.02, 0.02), 1: (-0.02, 0.02)},
        "operation": "add",
    },
)

Custom Class-Based Event Terms#

You can create custom event terms using classes instead of functions. This is useful for event terms that need to maintain state or perform initialization logic:

class RandomizeTerrainFriction:
    """Custom event term that randomizes terrain friction."""

    def __init__(self, cfg, env):
        # Find the terrain geom index during initialization
        self._terrain_idx = None
        for idx, geom in enumerate(env.scene.spec.geoms):
            if geom.name == "terrain":
                self._terrain_idx = idx

        if self._terrain_idx is None:
            raise ValueError("Terrain geom not found in the model.")

    def __call__(self, env, env_ids, ranges):
        """Called each time the event is triggered."""
        from mjlab.utils.math import sample_uniform
        env.sim.model.geom_friction[env_ids, self._terrain_idx, 0] = sample_uniform(
            ranges[0], ranges[1], len(env_ids), env.device
        )


# Use the custom class in your environment config
terrain_friction: EventTerm = term(
    EventTerm,
    mode="reset",
    func=RandomizeTerrainFriction,
    domain_randomization=True,
    params={"field": "geom_friction", "ranges": (0.3, 1.2)},
)

Migrating from Isaac Lab#

Isaac Lab exposes explicit friction combination modes (multiply, average, min, max). MuJoCo instead uses priority-based selection: if one contacting geom has higher priority, its friction is used; otherwise the element-wise maximum is used. See the MuJoCo contact documentation for details.