Scene, Robot, Prop & World
Every Task and Scenario starts with the same call: build(self, scene: simulo.Scene). This page is what goes inside it.
You write a Scene — a declaration of what exists. You put a Robot in it, add movable Props, provide some Terrain to stand on, or load a whole prebuilt World like a warehouse. Simulo spawns your scene as many environments as you ask for, and runs them all at once.
Scene — the declaration
Section titled “Scene — the declaration”A Scene is what exists, not what happens. You write build() once,
however many environments you run:
def build(self, scene: simulo.Scene) -> None: scene.add(simulo.Light.dome(name="light", intensity=2000.0), per_environment=False) scene.add(warehouse, at="/World/Warehouse", per_environment=False) scene.add(self.robot, at="/World/Robot") # per_environment=Trueper_environment is the one argument worth pausing on — it decides whether
scene.add(...) gives every environment its own copy, or shares one:
per_environment |
Behavior | Right for |
|---|---|---|
True (default) |
Every environment gets its own copy, and moves independently. | A robot — each environment trains or scripts its own instance. |
False |
One copy, shared across every environment. | Terrain or a World — 4096 identical warehouses would be 4096× the memory for nothing. |
The thing you’re training or controlling:
self.robot = simulo.Robot( asset=simulo.Asset.from_registry("robot/so-arm-100:v3"), initial_pose=simulo.Pose.identity(),)scene.add(self.robot, at="/World/Robot")initial_pose is where it spawns. Where it actually is, live, is
robot.state — batched across every environment, read straight through to
the simulation’s own buffers at zero extra cost:
def on_step(self) -> None: q = self.robot.state.joint_positions qd = self.robot.state.joint_velocities tau = self.robot.state.joint_efforts p = self.robot.state.pose # live pose, not the spawn poseThose tensors ARE the simulation’s own memory, not a copy, so two things
follow. Writing into one (q[:] = 0) reaches straight into the simulation
— treat every robot.state read as read-only unless you specifically mean
to teleport the robot. And holding a tensor across a physics step is only
safe for three of the six members: joint_positions, joint_velocities,
and joint_efforts are refreshed in place, so a reference from last step
still reads the current value. pose, linear_velocity, and
angular_velocity are refreshed by swapping in a new tensor, so a
reference held across a step can silently go stale. Re-read pose /
linear_velocity / angular_velocity every time you need them; the other
three are safe to cache (e.g. in on_start) if that’s convenient.
There’s also robot.internals — the raw engine handle. Named that way on
purpose: it’s an escape hatch for the rare read robot.state doesn’t cover,
it isn’t stable across engine versions, and you shouldn’t reach for it first.
Prop — an object you move or measure
Section titled “Prop — an object you move or measure”Use a Prop for a rigid object whose live pose matters to the task, such as a
block to lift, a crate to push, or a ball to catch:
self.block = simulo.Prop( simulo.Cuboid( name="block", size=(0.05, 0.05, 0.05), pose=simulo.Pose(position=[0.5, 0.0, 0.025]), physics=simulo.Physics.rigid(mass=0.05), ))scene.add(self.block, at="/World/Block")Read block.state.pose to find the object in every environment. During an
episode reset, use block.set_pose(..., env_ids=env_ids) and
block.set_velocity(...) to move only the environments that finished. If an
object never moves and you never need its live pose, add its shape to the
scene directly instead.
See Franka manipulation for a training task that measures success from a block’s height instead of guessing from the robot’s finger position.
Terrain — ground you generate
Section titled “Terrain — ground you generate”scene.add(simulo.Terrain.plane(name="ground"), per_environment=False)Terrain.plane(...) is the common case: a flat, infinite ground plane,
generated by the engine, one shared copy for the whole scene.
World — a place you bring
Section titled “World — a place you bring”A prebuilt space: a warehouse, a factory floor, a room. You supply it; Simulo loads it as-is. Publish your own, or pull one from the catalog:
simulo asset publish ./my-warehouse --kind world # your own USD/URDFsimulo asset search warehouse # or find onewarehouse = simulo.Asset.from_registry("world/warehouse:v2")scene.add(warehouse, at="/World/Warehouse", per_environment=False)Or point at a USD file directly with World.usd(...):
warehouse = simulo.World.usd(name="warehouse", path="./my-warehouse/warehouse.usd")scene.add(warehouse, at="/World", per_environment=False)Terrain is ground you generate. World is a place you bring. That’s the
whole distinction — a world almost always wants per_environment=False (see
the table above): a World is typically a large, detailed asset, and giving
every environment its own copy multiplies that memory by num_envs for
identical content. Its footprint is finite, though (unlike Terrain.plane’s
infinite ground), so with one shared copy you still need env_spacing sized
so every environment’s robot actually lands inside it.
environment — a live instance of your Scene
Section titled “environment — a live instance of your Scene”A Scene is the blueprint. An environment is one live instance of it — you get
num_envs of them, same as a class and its instances:
@app.job(system=simulo.SystemType.TIER_1)def train(num_envs: int = 4096) -> dict: ... Scene ── declared once: robot + terrain/world + lights │ │ spawned num_envs times, env_spacing apart ▼ ┌──────────┬──────────┬──────────┐ │ env 0 │ env 1 │ env 2 │ ... num_envs │ robot │ robot │ robot │ (per_environment=True) └──────────┴──────────┴──────────┘ └─────── one shared warehouse ───────┘ (per_environment=False)env_spacing sets how far apart environments sit so they don’t collide.
Everything you read back — observations, rewards, robot.state.* — comes
batched across all of them, one row per environment.
Where this fits
Section titled “Where this fits”- Tasks & the RL Loop and
Scenarios both start with a
build(scene)that uses everything on this page. - Volumes & Assets covers
Assetin more depth — publishing, catalog scopes, andAsset.usd()/Asset.urdf()for a model source that isn’t published. It also separatessimulo.Assetfrom the similarly namedsimulo.AssetSource, which is a plain USD/URDF file path rather than a catalog input. - Bring your own robot walks publishing and validating a robot end to end.