Scenarios
A scenario defines the task: what the world is made of, how it resets, and what the agents observe and are rewarded for.
The engine knows nothing about tasks — every built-in scenario is written against the same public Scenario ABC you would use for your own.
The ten built-ins
name |
task |
agents |
|
notable |
|---|---|---|---|---|
|
reach a per-agent goal while avoiding collisions |
4 |
19 |
optional obstacles; |
|
Reynolds-style cohesion + alignment, penalized for crowding |
8 |
24 |
pure neighbor-feature reward, no goals |
|
hold assigned slots of a regular polygon |
5 |
6 |
slot |
|
cover scattered targets, each needing several agents nearby |
5 |
19 |
one-off shared reward the step a target is first covered |
|
collect an unknown scalar field, consuming cells |
4 |
13 |
batched sum-of-Gaussians density on a grid; obs is the 3×3 cell neighborhood |
|
push a movable circular package to a goal |
4 |
8 |
package integrated in torch, staggered by one step, so BPTT flows package→agent→action |
|
push a T-shaped rigid body to a target pose |
4 |
18 |
movable compound body with rotation; needs |
|
cross a one-lane intersection without deadlocking |
4 |
40 |
the only non-monotone task: solving it needs an agent to move away from its goal. Observations are in each robot’s own travel frame and carry a per-episode priority token. Rigid contact ( |
|
surround a drifting disc so it cannot escape |
4 |
10 |
reward is the largest angular gap around the disc, not a distance, plus a dense per-agent even-spacing term |
|
drive fleeing sheep into a pen |
3 |
38 |
5 sheep, each running its own flee policy, so the environment pushes back; obs carries the Strömbom collect/drive points |
Defaults shown; every scenario takes constructor keywords (n_agents, world_size, shaping factors, penalties, tolerances).
navigation, flocking and giveway size their observation from neighbor_obs, so their obs_dim moves with it (give-way defaults it to n_agents - 1, since it senses all-pairs rather than off the physics neighbor grid);
shepherding’s moves with both n_sheep and n_agents (16 + 4*n_agents + 2*n_sheep).
The three that are not monotone
The first seven scenarios are all solved by closing a distance, which means a greedy policy that
always reduces its own distance-to-goal does well on every one of them.
giveway, caging and shepherding each break that in a different way, and each one needed a
physics correction that only training exposed — the test suite was green either way.
Give-way is the picture of the problem: four robots, each headed for the arm opposite its own,
jammed at a junction one robot wide.
Getting out requires one of them to pull aside into a side arm and let another past, which is the
one thing distance shaping actively punishes.
(The trained policy does exactly that, and does it forward: 57% of robots swing more than two radii
off their lane centreline — physically into a perpendicular arm — while only 3% ever reverse.)
The contact has to be rigid for any of that to be true — at Push-T’s contact_k=8000 a robot
sinks a third of the way into a corner block, the corridor stops being one lane, and a greedy
policy solves 100% of episodes by squeezing through walls.
Stiffness alone is not enough either: a velocity-mode agent covers max_speed * sub_dt in the
substep before contact can answer, which at substeps=8 is half a radius and is why this
scenario asks for 16. At 16 the overlap measured over a crowded jam is exactly zero.
Rigid contact is also what made the first version of this task unlearnable, in a way worth
recording because the failure was invisible in the metrics.
Zero interpenetration means the old collision penalty — a count of pairs closer than 2r —
multiplied a structurally-zero quantity, so collisions logged 0.000000 for a whole 2000-iteration
run; what the policy actually felt was a binary wall penalty worth 2.7x the entire shaping budget
over an episode, for occupying a corridor whose wall-free band is one sixth of its width.
It learned to stay in its own arm, which is exactly what it was being paid for.
The reward now charges continuous ramps over the contact-activation band instead, and the
binary flag survives only as the wall_contacts metric.
The observation was rebuilt at the same time: every vector feature is expressed in the robot’s own
travel frame (u, n) — plus the axis u itself, without which a rotation-invariant row could not
be turned back into a world-frame velocity — which collapses the plus-shape’s four-fold symmetry, and it carries the
corner-block clearance and its analytic SDF normal, the current corridor half-width (otherwise the
curriculum is a hidden parameter), each neighbour’s own travel direction — the feature that
separates head-on from crossing traffic — and a per-episode priority token.
The token is not a nicety: two robots meeting head-on have observations that are exact 180°
rotations of each other, so a shared-weight policy maps them to 180°-rotated actions and they both
accelerate or both retreat. There is a unit test for that symmetry.
Caging scores the largest angular gap in the ring of agents seen from the disc, so the objective is a shape rather than a distance, and the disc drifts out through whatever gap is left open. Note the gap still open at the upper right of the shot: that is the whole reward.
Shepherding is the only scenario whose environment pushes back. The sheep are not agents and not cargo — they run their own flee policy, so a shepherd that charges straight in scatters the flock, and the sheep have to be slower than the shepherds or the task is quietly impossible.
It was quietly impossible for a different reason, and the failure is worth recording because it
was invisible to a green test suite.
The sheep’s own separation-and-cohesion law has an equilibrium, and at the original
sep_radius = 0.2 five relaxed sheep settle at radii [0.07, 0.14, 0.14, 0.20, 0.20] — a flock
whose own maximum radius is 0.198 against a pen of radius 0.2.
“All sheep inside the pen” therefore demanded the flock centroid be placed within 3 mm of the
pen centre in a world of half-extent 1.0, and shepherds make it worse: the flee force is capped at
1.0 while cohesion is only 0.5 R ≈ 0.08, so any shepherd inside flee_radius inflates the flock
faster than cohesion can pull it back.
A scripted Strömbom controller solved 5% of episodes; 3000 MAPPO iterations learned nothing, and
every unit test passed throughout.
The fix is geometry, not reward: pen_radius 0.3, sep_radius 0.15, 200-step episodes, and a
done that requires the flock to hold for five steps rather than pass through.
The spawn had to change too, and that part is not obvious.
Drawing each sheep at an independent bearing on a ring about the pen puts the flock centroid on
the pen by symmetry, so at the larger pen radius doing nothing solved 23% of episodes — the
ring spawn is almost all collecting and no driving.
The sheep now spawn as a cluster: a flock centre at a distance and bearing from the pen, then
each sheep in a disc about it. Under that spawn a scripted controller solves 84% and both a random
and a do-nothing policy solve exactly zero, which is the property
test_a_scripted_shepherd_solves_most_episodes now pins in CI.
The observation carries the Strömbom collect and drive points directly (dropping obs slots 16–19
recovers the un-hinted task), and set_flock_distance / set_flock_spread are the two curriculum
axes — distance is the one that matters; dispersion is nearly free.
The registry
import swarp
swarp.SCENARIOS # {'navigation': NavigationScenario, ...}
env = swarp.make("flocking", n_envs=1024, n_agents=16, device="cuda:0")
scenario = swarp.make_scenario("pusht", n_agents=6) # scenario only, no Environment
cls = swarp.scenario_class("pusht") # the class itself
SCENARIOS is the one registry; make raises a ValueError listing the valid names for anything else.
make_scenario drops a model= keyword for holonomic-only scenarios, so a sweep over dynamics models can pass it uniformly.
Two execution paths
Every built-in scenario implements its observations and rewards twice:
torch — plain tensor ops over
world.state, differentiable together with the Warp step and used whenever grads are enabled.fused Warp kernels — a
*_kernels.pymodule per scenario, used on the no-grad hot path and foldable into the whole-step CUDA graph.
The fused path is worth 2.5–5× (see Benchmarks), and Environment(fused="auto") picks it automatically when grads are off.
The torch implementation is the parity oracle the fused kernels are tested against, which is why the two are kept independent rather than sharing code — shared helpers would let a bug cancel itself out on both sides.
Reward structure
Scenario separates the two reward terms:
agent_reward(i)— the per-agent term,[n_envs].global_reward()— a shared team term,[n_envs], added to every agent.
rewards() combines them into the [n_envs, n_agents] tensor Environment.step returns.
Keeping them apart is what makes a scenario like navigation switchable between individual and shared credit with one flag, and it documents which part of the signal is a team objective.
Writing your own
Subclass Scenario, implement four members, and you have a task that is differentiable end-to-end and runs on the batch:
class MyScenario(Scenario):
obs_dim = 4
def make_world(self, n_envs, device, dt, substeps, dtype, world_config=None) -> World: ...
def reset_world(self, env_mask=None, *, obs_only=False): ...
def observations(self): ...
def agent_reward(self, agent_idx): ...
The full contract — including the optional members, the FusedScenario tier, and the capture-safety rules a fused kernel must respect — is in Writing a scenario.