Domain Randomization for Robust Robot Learning Domain randomization is the idea that, instead of trying to perfectly match simulation to reality, you randomize simulation parameters widely enough that reality just looks like "one more variation" the policy has already seen. It's one of the highest-leverage techniques in sim-to-real robot learning, and it's straightforward to bolt onto an existing MuJoCo (or other simulator) pipeline. The Core Idea If you train a policy across a wide distribution of: lighting conditions object colors/textures camera positions and lens distortion friction, mass, and damping values sensor noise levels ...then the real world simply becomes one more sample from a distribution the policy has already learned to handle, rather than an out-of-distribution shock. Categories of Randomization Visual Randomization Targets the visual gap between rendered and real images. import numpy as np def randomize_visuals(model, data): # Randomize object color obj_geom_id = model.geom("target_object_geom").id model.geom_rgba[obj_geom_id][:3] = np.random.uniform(0, 1, 3) # Randomize lighting light_id = model.light("main_light").id model.light_diffuse[light_id] = np.random.uniform(0.4, 1.0, 3) model.light_pos[light_id] += np.random.uniform(-0.3, 0.3, 3) # Randomize table/background texture (if using textured materials) table_mat_id = model.material("table_material").id model.mat_rgba[table_mat_id][:3] = np.random.uniform(0.2, 0.8, 3) Dynamics Randomization Targets the physics/dynamics gap. def randomize_dynamics(model, data): # Randomize friction for geom_id in [model.geom("target_object_geom").id, model.geom("table").id]: model.geom_friction[geom_id][0] = np.random.uniform(0.4, 1.2) # Randomize object mass obj_body_id = model.body("target_object").id model.body_mass[obj_body_id] = np.random.uniform(0.05, 0.3) # Randomize actuator gains for i in range(model.nu): model.actuator_gainprm[i][0] *= np.random.uniform(0.85, 1.15) Sensor and Latency Randomization Targets the sensing/timing gap, which is often overlooked but very impactful. def randomize_observation(obs, image_noise_std=0.02, state_noise_std=0.01, latency_steps=(0, 3)): noisy_obs = obs.copy() noisy_obs["image"] = np.clip( obs["image"] + np.random.normal(0, image_noise_std, obs["image"].shape), 0, 1 ) noisy_obs["joint_positions"] = obs["joint_positions"] + np.random.normal( 0, state_noise_std, obs["joint_positions"].shape ) # Simulate variable sensor latency by optionally returning a delayed observation delay = np.random.randint(*latency_steps) noisy_obs["_simulated_delay"] = delay return noisy_obs Camera Pose Randomization Small camera placement errors are inevitable when mounting real hardware — simulate that variance too. def randomize_camera_pose(model, camera_name, pos_noise=0.01, angle_noise_deg=2.0): cam_id = model.camera(camera_name).id model.cam_pos[cam_id] += np.random.uniform(-pos_noise, pos_noise, 3) angle_rad = np.radians(np.random.uniform(-angle_noise_deg, angle_noise_deg, 3)) # Apply small rotation perturbation to cam_quat via your rotation utility of choice model.cam_quat[cam_id] = perturb_quaternion(model.cam_quat[cam_id], angle_rad) Wiring Randomization into the Environment Apply randomization at reset() time so every episode is a fresh sample from the randomized distribution: class DomainRandomizedEnv(RenderedMuJoCoEnv): def __init__(self, xml_path, randomize=True, **kwargs): super().__init__(xml_path, **kwargs) self.randomize = randomize def reset(self): obs = super().reset() if self.randomize: randomize_visuals(self.model, self.data) randomize_dynamics(self.model, self.data) mujoco.mj_forward(self.model, self.data) obs = self._get_obs() obs["image"] = self.render() return obs def step(self, action): obs, reward, done, info = super().step(action) if self.randomize: obs = randomize_observation(obs) return obs, reward, done, info How Much Randomization Is Too Much? This is the central tuning problem of domain randomization: too little and you don't cover the real-world distribution; too much and the task becomes so variable the policy fails to learn anything useful (or requires far more capacity and data). A practical approach: Start with a narrow randomization range around your best system-identified parameters Train and evaluate in sim; confirm the policy still solves the task reliably across the randomized range Gradually widen ranges, re-training and re-evaluating, until sim performance starts to degrade meaningfully Test the resulting policy on real hardware and compare against a non-randomized baseline def randomization_sweep(base_ranges, scale_factors, env_factory, train_fn, eval_fn): results = {} for scale in scale_factors: scaled_ranges = {k: (lo * scale, hi * scale) for k, (lo, hi) in base_ranges.items()} env = env_factory(randomization_ranges=scaled_ranges) policy = train_fn(env) success_rate = eval_fn(env, policy) results[scale] = success_rate print(f"Randomization scale {scale}: {success_rate:.1%} sim success") return results Curriculum-Style Randomization Rather than fixed randomization ranges for the entire training run, many pipelines ramp randomization up over training — starting narrow (easier to learn) and widening as the policy improves: def get_randomization_scale(training_step, total_steps, start_scale=0.2, end_scale=1.0): progress = min(training_step / total_steps, 1.0) return start_scale + progress * (end_scale - start_scale) Practical Tips Randomize what actually varies in your deployment environment. If your robot always operates under fixed studio lighting, heavy lighting randomization may cost more in sample efficiency than it buys in robustness — profile your real environment first. Track per-parameter ablations. When a policy fails to transfer, disable randomization dimensions one at a time in evaluation to isolate which one the policy is actually sensitive to. Combine with the fine-tuning strategy from the sim-to-real tutorial — domain randomization gets you a robust starting policy; a small amount of real data closes the remaining gap. What's Next Domain randomization pairs naturally with generating large volumes of synthetic training data — the subject of the final tutorial in this series. Useful Links Website: www.v-modal.com SDK Flutter: v-modal/vmodal_sdk_flutter SDK Android: v-modal/vmodal_sdk_android Discord: https://discord.gg/K72z28KUx