Skip to content

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.

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=True

per_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 pose

Those 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.

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.

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.

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:

Terminal window
simulo asset publish ./my-warehouse --kind world # your own USD/URDF
simulo asset search warehouse # or find one
warehouse = 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.

  • Tasks & the RL Loop and Scenarios both start with a build(scene) that uses everything on this page.
  • Volumes & Assets covers Asset in more depth — publishing, catalog scopes, and Asset.usd()/Asset.urdf() for a model source that isn’t published. It also separates simulo.Asset from the similarly named simulo.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.