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']

An axis flag must be a boolean. The four flags select a posture - run this axis or leave it alone - so each is checked rather than read for truthiness. Every non-empty string is truthy, so "false", "no", "off" and "0" - the spellings you reach for when turning an axis off - each turned that axis on, and the call reported it applied. That is the same guarantee as the misspelled key above, one level down: a value an axis cannot be read from can never look like a successful randomization either.

sim.randomize(randomize_physics="false")
# status=error: randomize: randomize_physics must be a boolean, got 'false'.
#              It selects a posture, so it is not read for truthiness ...

Both backends that implement randomize() (MuJoCo and Newton) check the same domain, so an axis you can turn off on one is not left on by the other. The numeric knobs in the same signature keep their own domain: a mass_range is a quantity, and it is still refused as a range rather than as a flag.

They also declare the parameters they share in the order shown above. Newton adds no axis of its own and MuJoCo adds two (randomize_positions, position_noise), so the two signatures are not the same length - but neither reorders a name the other also carries, so the three ranges cannot be read as each other.

Destructive - writes into MuJoCo model arrays. To restore: load_scene(...) or recreate the sim. Every other scene mutation restores it too, as a side effect of rebuilding the model from the spec: add_object, remove_object, add_camera, remove_camera, add_robot, remove_robot and patch_scene_mjcf. There is no recompile action - any of those is the undo, so randomize after the episode's scene is built.

Every axis survives reset(). A reset restores the world's initial state, and each axis writes that initial state rather than only the live state - the colour, friction and mass axes write model arrays, and randomize_positions writes model.qpos0 (the pose a reset restores) alongside the live data.qpos. This is what makes randomization reach a rollout at all: run_policy and eval_policy reset before an episode's first step, so an axis a reset undid would be gone before the policy ever saw it.

randomize_positions measures its offset from each object's commanded pose - where add_object / move_object placed it - not from wherever physics has left it. The commanded pose is a fixed reference, so calling randomize() once per episode draws independent offsets that always stay inside position_noise instead of compounding into a random walk that eventually leaves the workspace.

randomize_lighting measures its offset the same way, from each light's authored position - the pose the scene spec declares, which is also what a recompile restores - so a per-episode loop keeps every light inside +/-0.5 m of where the scene put it. Both axes need a fixed reference for the same reason: offsetting the live value makes each call start from the previous call's result, and the displacement then grows without limit while every individual draw still looks correctly bounded. A light authored 3.5 m above the scene walks 4.7 m away over 50 episodes that way, which is 9.4x the advertised bound. Because the offset is absolute rather than cumulative, replaying a seed now reproduces the same lighting regardless of how many calls preceded it.

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 Light position (+/-0.5 m of its authored pose) + diffuse colour -
randomize_physics Per-object mass (mult), per-geom friction (scale), joint damping mass_range, friction_range
randomize_positions Dynamic-object position offsets (metres); static objects have no pose DOF and are skipped position_noise

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

Use in an eval loop

for episode in range(N):
    sim.randomize(randomize_colors=True, randomize_physics=True,
                  randomize_positions=True, position_noise=0.03, seed=episode)
    # eval_policy has no randomize= kwarg - call sim.randomize() before each episode.
    # It resets at the start of every episode, which is why the perturbation has to
    # survive a reset; no explicit sim.reset() is needed here.
    # Order matters: a scene mutation after randomize() (adding this episode's
    # distractors, say) rebuilds the model from the spec and undoes every axis, so
    # build the episode's scene first and randomize last.
    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

size here is the geom's own geom_size - half-extents for a box - and not add_object's full extents, so the same vector means two different objects depending on which call it is passed to. add_object(size=[0.2, 0.2, 0.2]) builds a 20 cm box; set_geom_properties(size=[0.2, 0.2, 0.2]) resizes that box to 40 cm. A capsule is [radius, half-length] here and [diameter, unused, height] there.

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