Tutorial 4 — Reinforcement Learning¶
forge3d ships two Gymnasium-compatible environments for robot learning.
Training runs headless (no renderer), and with JAX vmap you can step
thousands of parallel environments at near-native speed.
ReachEnv — robot reaching¶
The ReachEnv is available via the forge3d.sim module or as a direct Gymnasium env.
Observation (12 dims)¶
| Index | Description |
|---|---|
| 0–5 | Joint angles q₀–q₅ (rad) |
| 6–8 | End-effector position (m) |
| 9–11 | Target position (m) |
Action (6 dims)¶
Delta joint angles Δq ∈ [−1, 1] rad/step.
Training with SB3¶
import gymnasium as gym
from stable_baselines3 import PPO
# Import the Gymnasium wrapper directly
from forge3d.sim.jax_batch import make_reach_env
env = make_reach_env() # headless, fast
model = PPO("MlpPolicy", env, verbose=1, n_steps=2048, batch_size=64)
model.learn(total_timesteps=200_000)
model.save("reach_policy")
Rendering a rollout¶
import forge3d as f3d
import numpy as np
world = f3d.World(gravity=(0, 0, -9.81))
world.add_ground()
# … add robot arm …
model = PPO.load("reach_policy")
rec = f3d.Recorder(world, mode="hq", output="rollout.mp4")
rec.run_policy(model, env=None, duration=5.0)
JAX batch stepping — 2,000× speedup¶
import jax
import jax.numpy as jnp
from forge3d.sim.jax_batch import batch_reach_reset, batch_reach_step
key = jax.random.PRNGKey(42)
q, tgt, obs = batch_reach_reset(key, n_envs=256)
# One batched step across 256 environments — single JIT kernel
q, obs, rew, done = batch_reach_step(q, tgt, jnp.zeros((256, 6)))
print(f"rewards: {rew.shape}") # (256,)
JAX JIT + vmap runs all 256 environments in a single compiled kernel.
No Python loop overhead, no device-host round-trips per step.
Domain randomization¶
from forge3d.sim.domain_rand import DomainRand
rand = DomainRand(
mass_scale=(0.8, 1.2), # uniform random mass factor
friction_range=(0.3, 0.9), # random friction coefficient
gravity_noise=0.1, # small perturbation in gravity magnitude
joint_damping=(0.0, 0.05), # random damping per joint
)
# Apply randomization to a world at episode start
world = f3d.World()
rand.apply(world, key=jax.random.PRNGKey(episode))
Domain randomization improves sim-to-real transfer by training the policy across a distribution of physical parameters rather than a single point.
Switch to JAX backend¶
The entire physics stack — including collision detection and contact solving —
runs under JAX JIT when ENGINE_BACKEND=jax. This allows jax.grad through
the physics step for model-based RL.
PickPlaceEnv — pick and place¶
The pick-and-place environment uses the weld kinematic constraint to simulate grasping.
from forge3d.sim.jax_batch import make_pick_place_env
env = make_pick_place_env()
obs, info = env.reset()
print(f"Obs shape: {obs.shape}") # (18,)
Observation (18 dims)¶
| Index | Description |
|---|---|
| 0–5 | Joint angles q₀–q₅ (rad) |
| 6–8 | End-effector position (m) |
| 9–11 | Object position (m) |
| 12–14 | Object orientation (Euler, rad) |
| 15–17 | Target position (m) |