Tutorial 3: Managing a 100-Robot Fleet
| Mixed Fleet Grid 100robots_grid_demo.py |
Cube Patrol 100robots_cube_patrol_demo.py |
| Mobile Pick & Drop pick_drop_mobile_100robots_demo.py |
Arm Pick & Drop pick_drop_arm_100robots_demo.py |
Source file: examples/scale/100robots_cube_patrol_demo.py
This tutorial scales up from a single agent to a fleet of 100 robots. You will learn how to:
Declare named managers with
managers:config and route entity grids to themSet fleet-wide controller defaults with
fleet_controller:Enable vectorised batch controllers via
batch_controller:Retrieve managers at runtime with
sim.get_manager()Build
Pathobjects with waypoints and assign them withset_pathIterate over all agents to read state or assign individual paths
Write an efficient monitoring callback for large fleets
After this tutorial, combine what you’ve learned with
Tutorial 2 — Action System to send each agent an action sequence
via manager.add_action_sequence_all.
1. When to Use AgentManager
Direct |
|
|
|---|---|---|
Fleet size |
1–10 |
10–10,000 |
Bulk ops |
Manual loops |
|
Grid spawn |
Manual |
|
Iteration |
|
|
AgentManager is a thin wrapper — each item in manager.objects is a regular Agent
and supports all the same methods from Tutorial 1 and 2.
2. Load Config from YAML
For larger simulations, externalise parameters to a YAML file so you can change settings (number of agents, speed limits, collision mode) without editing Python code:
from pybullet_fleet.core_simulation import MultiRobotSimulationCore
sim = MultiRobotSimulationCore.from_yaml("config/100robots_config.yaml")
from_yaml reads the nested YAML config (under the simulation: key) and creates the
simulation core in one call. Any parameter not present in the file gets its default value.
The config files live in
config/. See Configuration Reference for the full parameter list.
3. Declare Named Managers in Config
The recommended way to set up a multi-fleet simulation is a YAML config file with
managers: and entities: sections. The simulation core creates the managers,
spawns the agents, and wires them together — no Python boilerplate required.
A complete example lives at config/cube_patrol_config.yaml:
managers:
- name: omni_fleet
batch_controller: batch_omni # vectorised batch controller
fleet_controller: # shared defaults for every agent in this fleet
max_linear_vel: 2.0
max_linear_accel: 1.0
max_angular_vel: 2.0
max_angular_accel: 5.0
- name: diff_fleet
batch_controller: batch_differential
fleet_controller:
max_linear_vel: 2.0
max_linear_accel: 1.0
max_angular_vel: 2.0
max_angular_accel: 5.0
entities:
- urdf_path: "robots/mobile_robot.urdf"
mass: 0.0
use_fixed_base: false
motion_mode: omnidirectional
manager: omni_fleet # route this group to omni_fleet
grid:
x_min: 0
x_max: 4
y_min: 0
y_max: 9
spacing: [10.0, 10.0, 0.0]
offset: [-15.0, -15.0, 0.3]
- urdf_path: "robots/mobile_robot.urdf"
mass: 0.0
use_fixed_base: false
motion_mode: differential
manager: diff_fleet # route this group to diff_fleet
grid:
x_min: 5
x_max: 9
y_min: 0
y_max: 9
spacing: [10.0, 10.0, 0.0]
offset: [-15.0, -15.0, 0.3]
Load and retrieve managers:
from pybullet_fleet.core_simulation import MultiRobotSimulationCore
sim = MultiRobotSimulationCore.from_yaml("config/cube_patrol_config.yaml")
omni_manager = sim.get_manager("omni_fleet")
diff_manager = sim.get_manager("diff_fleet")
Note — demo uses Python dicts instead of
from_yaml:100robots_cube_patrol_demo.pyaccepts a--robotargument that resolves the URDF path at runtime viaresolve_model(). Because the path is dynamic, the demo merges the base config withmanagers:/entities:programmatically (from_dict) rather than reading a static YAML file. The structure is identical to the YAML above.
batch_controller — vectorised movement
batch_controller enables a shared BatchKinematicController for all agents in the
manager. Instead of calling compute() per agent, the batch controller advances every
agent in one vectorised NumPy pass. At 100 agents this is roughly equivalent to
per-agent; at 500+ it is 3–5× faster.
Value |
Controller |
|---|---|
|
Omnidirectional (XY + Z) |
|
Unicycle (rotate-then-forward) |
Agents with different motion modes must be in separate managers, because each batch controller supports one motion model.
Alternative: Python API
If you prefer to construct managers imperatively (e.g. when URDF paths are resolved at runtime):
from pybullet_fleet.agent_manager import AgentManager, GridSpawnParams
from pybullet_fleet.agent import AgentSpawnParams, MotionMode
omni_manager = AgentManager(sim_core=sim, batch_controller="batch_omni")
omni_manager.spawn_agents_grid(
num_agents=50,
grid_params=GridSpawnParams(
x_min=0, x_max=4, y_min=0, y_max=9,
spacing=[10.0, 10.0, 0.0],
offset=[-15.0, -15.0, 0.3],
),
spawn_params=AgentSpawnParams(
urdf_path=urdf_path, mass=0.0,
motion_mode=MotionMode.OMNIDIRECTIONAL, use_fixed_base=False,
controller={"max_linear_vel": 2.0, "max_linear_accel": 1.0},
),
)
mass=0.0 (kinematic mode) is the key performance choice. Kinematic control
teleports each robot to its next pose without calling stepSimulation() — this is
why PyBulletFleet can run 100 robots at 40× real time. See the
Benchmark Results for measured throughput.
spawn_agents_gridis used inexamples/scale/pick_drop_mobile_100robots_demo.py, which also demonstratesSimObjectManagerfor batch-spawning pickable objects alongside agents.
4. Build a Path and Assign it to an Agent
A Path is a sequence of Pose waypoints the agent follows in order,
looping back to the start when it reaches the end.
from pybullet_fleet.geometry import Path, Pose
# Build waypoints using Pose.from_euler for full 6-DOF control
waypoints = [
Pose.from_euler(x, y, z, roll=0, pitch=0, yaw=0)
for (x, y, z) in [
[5, 5, 0.3], # corner 1
[-5, 5, 0.3], # corner 2
[-5, -5, 0.3], # corner 3
[5, -5, 0.3], # corner 4
]
]
path = Path(waypoints=waypoints)
# Or build from positions only (orientation = identity on all waypoints).
# This shorthand is not used in the demo, but is handy for quick tests:
path = Path.from_positions([
[5, 5, 0.3], [-5, 5, 0.3], [-5, -5, 0.3], [5, -5, 0.3]
])
Assign to an agent:
# Omnidirectional robot (direction parameter ignored)
agent.set_path(path.waypoints)
# Differential drive — specify movement direction
from pybullet_fleet.agent import MovementDirection
agent.set_path(path.waypoints, direction=MovementDirection.FORWARD)
agent.set_path(path.waypoints, direction=MovementDirection.BACKWARD)
FORWARD: the robot faces toward the next waypoint (normal driving).
BACKWARD: the robot keeps its heading fixed and reverses toward the waypoint —
useful for robots that should always face a particular way (e.g., a forklift mast).
Tip: Call
path.get_total_distance()to inspect the path length before assigning, andpath.visualize(...)to draw it in the GUI.
5. Assign Individual Paths to All 100 Agents
Here each robot patrols a cube centred on its own spawn position. The key is getting each robot’s spawn pose to build a per-robot path:
for robot in manager.objects:
spawn_pos = robot.get_pose().position # current (= spawn) position
# Build a path centred at this robot's position
cx, cy = spawn_pos[0], spawn_pos[1]
half = 2.5
patrol_waypoints = [
Pose.from_euler(cx + half, cy + half, 0.3, roll=0, pitch=0, yaw=0),
Pose.from_euler(cx - half, cy + half, 0.3, roll=0, pitch=0, yaw=0),
Pose.from_euler(cx - half, cy - half, 0.3, roll=0, pitch=0, yaw=0),
Pose.from_euler(cx + half, cy - half, 0.3, roll=0, pitch=0, yaw=0),
]
if robot.motion_mode == MotionMode.DIFFERENTIAL:
import random
direction = random.choice([MovementDirection.FORWARD, MovementDirection.BACKWARD])
robot.set_path(patrol_waypoints, direction=direction)
else:
robot.set_path(patrol_waypoints)
This pattern — iterate manager.objects, get pose, compute per-robot data, call
single-agent API — is the standard way to initialize heterogeneous fleets.
6. Bulk Operations with the Manager
AgentManager provides vectorised versions of the most common per-agent operations.
These are more readable (and slightly faster) than manual loops:
# Set a random goal for every agent
import numpy as np
manager.set_goal_pose_all(
lambda agent: Pose.from_xyz(
np.random.uniform(-10, 10),
np.random.uniform(-10, 10),
0.3,
)
)
# Get all poses at once
poses = manager.get_poses_dict() # {agent_index: Pose}
# Teleport all agents to new poses
manager.set_pose_all(
lambda agent: Pose.from_xyz(agent.get_pose().position[0], 0, 0.3)
)
All bulk methods accept a factory callable (Agent) → result that is called
once per agent.
Note: These snippets are not in the demo script
100robots_cube_patrol_demo.py. They show additionalAgentManagerAPIs you can use in your own code.
7. Write a Monitoring Callback
For large fleets, avoid per-step prints (they dominate step time).
The step_count modulo pattern is efficient:
def monitoring_callback(sim_core, dt):
# Print only every 300 steps (≈ every 30 simulated seconds at timestep=0.1)
if sim_core.step_count % 300 != 0:
return
moving = sum(1 for r in manager.objects if r.is_moving)
speeds = [np.linalg.norm(r.velocity) for r in manager.objects if r.is_moving]
avg_speed = np.mean(speeds) if speeds else 0.0
print(
f"[t={sim_core.sim_time:.0f}s] "
f"Moving: {moving}/{len(manager.objects)} | "
f"Avg speed: {avg_speed:.2f} m/s"
)
sim.register_callback(monitoring_callback, frequency=None)
r.is_moving — True while the agent has a goal or path to follow.
r.velocity — current velocity vector [vx, vy, vz] in world frame.
Printing every step at 100 agents adds ~5–10% overhead. Throttling to every 300 steps costs nothing measurable.
8. Camera Setup
For large grids, let the simulation auto-fit the camera to all spawned agents:
sim.setup_camera() # auto-scales to bounding box of all objects
Or pass an explicit config (the demo uses YAML config instead, but you can override programmatically):
sim.setup_camera(camera_config={
"camera_mode": "manual",
"camera_distance": 80.0,
"camera_yaw": 45,
"camera_pitch": -35,
"camera_target": [0, 0, 0],
})
9. Run
sim.run_simulation()
At 100 agents with physics=False, you should see ~40× RTF (≈ 2.4 ms per step).
See Benchmark Results for the full throughput table.
Scale Demos
All four scale demo scripts live in examples/scale/:
Script |
What it demonstrates |
|---|---|
100 mobile robots patrolling cube paths — config-driven |
|
Mixed fleet (mobile + arm) in a grid with |
|
100 mobile robots with pick/drop action sequences and |
|
100 fixed-base arms with |
python examples/scale/100robots_cube_patrol_demo.py
python examples/scale/100robots_grid_demo.py
python examples/scale/pick_drop_mobile_100robots_demo.py
python examples/scale/pick_drop_arm_100robots_demo.py
Switching Robot Models
All scale demos accept a --robot argument to swap the robot model at runtime.
Pass a model name resolved by resolve_model() or a direct URDF path:
# Mobile demos — use mobile models
python examples/scale/100robots_cube_patrol_demo.py --robot racecar
python examples/scale/pick_drop_mobile_100robots_demo.py --robot mobile_robot
# Arm demo — use arm models
python examples/scale/pick_drop_arm_100robots_demo.py --robot kuka_iiwa
# Grid demo — has both mobile and arm robots
python examples/scale/100robots_grid_demo.py --robot racecar --arm-robot kuka_iiwa
Script |
Argument |
Default |
Alternatives |
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
See Tutorial 6 — Robot Models for the full model resolution system
and python examples/models/resolve_model_demo.py --list for all available names.
Performance Notes
physics=Falseis the single most important setting for fleet-scale throughput. Physics stepping is O(n) even with kinematic control.collision_check_frequency— set to1.0or lower for offline use;null(every step) for real-time collision monitoring. See the Optimization Guide.Motion mode matters — DIFFERENTIAL robots are ~5× more expensive to update than OMNIDIRECTIONAL due to heading alignment computation. Mixed fleets pay a weighted-average cost.
See Also
Tutorial 1 — Spawning Objects: single-agent basics
Tutorial 2 — Action System:
add_action_sequence_allfor fleet-scale tasksBenchmark Results: measured throughput at 100–2000 agents
Optimization Guide: tuning
collision_check_frequency, motion mode, physics flagConfiguration Reference: full YAML parameter list