Skip to content

Domain randomization

sim.randomize(
    randomize_colors=True,      # resample object/floor RGB from color_range
    randomize_lighting=True,    # perturb directional + ambient light
    randomize_physics=False,    # mass (mass_range) + friction (friction_range) + damping
    randomize_positions=False,  # add position_noise (m) to every object position
    position_noise=0.02,
    color_range=(0.1, 1.0),
    friction_range=(0.5, 1.5),
    mass_range=(0.5, 2.0),
    seed=42,                    # deterministic sequence
)

Unknown parameters are rejected. randomize() and set_obs_noise() both declare **kwargs to match the backend-agnostic SimEngine signature, so a keyword they do not honor (randomize_position singular, position_range, joint_pos_stdev) would otherwise be dropped and the call still reported as applied. Instead they return status=error naming the unusable keys and the valid set - a misspelled axis can never look like a successful randomization:

sim.randomize(randomize_position=True)   # singular
# status=error: Unknown parameter(s) ['randomize_position'] for action 'randomize'.
#              Valid: ['color_range', 'friction_range', 'mass_range', 'position_noise',
#                      'randomize_colors', 'randomize_lighting', 'randomize_physics',
#                      'randomize_positions', 'seed']

Destructive - writes into MuJoCo model arrays. To restore: load_scene(...) or recreate the sim.

randomize() leaves the sim in a forwarded, render-ready state: the next render() / get_observation() reflects the perturbation immediately, with no manual step() in between. This matters for lighting in particular - the renderer reads light positions from the derived data.light_xpos, not model.light_pos, so a light-position jitter only reaches a render after a forward.

Categories

Flag What changes Range param
randomize_colors Object + floor RGB (alpha fixed at 1.0) color_range
randomize_lighting Directional direction, intensity, ambient -
randomize_physics Per-object mass (mult), per-geom friction (scale), joint damping mass_range, friction_range
randomize_positions Object position offsets (metres) position_noise

Defaults: colors=True, lighting=True; physics and positions default False.

Use in an eval loop

for episode in range(N):
    sim.reset()
    sim.randomize(randomize_colors=True, randomize_physics=True, seed=episode)
    # eval_policy has no randomize= kwarg - call sim.randomize() before each episode
    result = sim.eval_policy(robot_name="so100", n_episodes=1, max_steps=300,
                             success_fn=my_fn)

Targeted per-geom / per-body perturbation

randomize() perturbs the whole scene; set_geom_properties / set_body_properties perturb one entity, which is what you want when only the manipuland's friction or the table's height should change between episodes.

sim.set_geom_properties(geom_name="crate", color=[0.8, 0.2, 0.2],   # RGB or RGBA
                        friction=[0.6, 0.01, 0.001],                # sliding, torsional, rolling
                        size=[0.2, 0.2, 0.05])                      # box: three half-extents
sim.set_body_properties(body_name="crate", mass=1.4)                # inertia scales with it

Every vector must carry the exact component count its target defines. There is no meaningful value to invent for a component you omit, so a partial vector is rejected instead of being mixed with the compiled one:

Parameter Accepted components
color 3 (RGB, alpha set to 1.0) or 4 (RGBA)
friction 3 (sliding, torsional, rolling)
size whatever the geom's type defines: sphere 1, capsule/cylinder 2, box/ellipsoid/plane 3
sim.set_geom_properties(geom_name="crate", size=[0.4])
# status=error: 'size' must have exactly 3 component(s) (box: three half-extents),
#               got 1: [0.4]. Pass every component - a partial 'size' cannot be
#               applied without inventing the missing values.

A mesh / height-field / SDF geom takes its extent from asset data and defines no geom_size component, so size is refused for it (resize the asset instead). Growing a size-defined primitive refreshes its broadphase and mid-phase collision bounds, so other bodies collide with the new extent rather than passing through it. It also re-derives the owning body's mass, center of mass and inertia tensor from the new shape - those are integrated from the body's geoms at compile time and are never recomputed by a step, so without this a resized body would collide as its new shape while resisting rotation as the old one. The values are read from a compile of the persisted spec, so a resize means the same thing whether or not another scene mutation follows it. A body that declares its own <inertial> takes nothing from geometry and is left alone.

Sensor noise

set_obs_noise adds Gaussian measurement noise to observations so a policy is not trained (or evaluated) on noise-free sensing - a cheap sim-to-real robustness lever that is orthogonal to randomize() (which perturbs the world; this perturbs the sensor).

sim.set_obs_noise(
    joint_pos_std=0.01,      # rad, added to joint positions
    joint_vel_std=0.05,      # rad/s, added to per-joint velocities
    camera_jitter_px=2,      # max integer pixel shift per axis on rendered frames
    seed=0,                  # reproducible noise stream
)

Once configured, the noise is applied on every get_observation (joint positions, the <joint>.vel entries, and camera frames), get_robot_state (position + velocity), and render until reconfigured. Pass all-zero std to disable; leaving it unconfigured (the default) is an exact no-op, so existing observations and renders are unchanged. Floating-base base_quat / base_ang_vel signals are left untouched (a quaternion would need renormalization). Values must be finite and non-negative or the call returns status=error.

Newton backend

The Newton (GPU) backend mirrors both the randomize contract for the axes it supports (colors, lighting, physics) and the set_obs_noise sensor-noise contract, so an identical call behaves the same on either backend. See Newton backend.

See also