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.