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 them

  • Set fleet-wide controller defaults with fleet_controller:

  • Enable vectorised batch controllers via batch_controller:

  • Retrieve managers at runtime with sim.get_manager()

  • Build Path objects with waypoints and assign them with set_path

  • Iterate 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 Agent objects

AgentManager

Fleet size

1–10

10–10,000

Bulk ops

Manual loops

set_goal_pose_all, set_pose_all, add_action_sequence_all

Grid spawn

Manual

spawn_agents_grid, spawn_agents_grid_mixed

Iteration

agent.get_pose() per agent

manager.objects generator

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.py accepts a --robot argument that resolves the URDF path at runtime via resolve_model(). Because the path is dynamic, the demo merges the base config with managers: / entities: programmatically (from_dict) rather than reading a static YAML file. The structure is identical to the YAML above.

fleet_controller — shared parameter defaults

fleet_controller sets ControllerParams defaults for the whole fleet. When an agent spawns with no explicit controller: key, these values are used. Per-agent overrides (non-None fields) are never overwritten.

See Controller Config for the full list of ControllerParams fields.

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

"batch_omni"

Omnidirectional (XY + Z)

"batch_differential"

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_grid is used in examples/scale/pick_drop_mobile_100robots_demo.py, which also demonstrates SimObjectManager for 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, and path.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 additional AgentManager APIs 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_movingTrue 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

100robots_cube_patrol_demo.py

100 mobile robots patrolling cube paths — config-driven managers: + fleet_controller: (this tutorial)

100robots_grid_demo.py

Mixed fleet (mobile + arm) in a grid with --mode mixed|single

pick_drop_mobile_100robots_demo.py

100 mobile robots with pick/drop action sequences and SimObjectManager

pick_drop_arm_100robots_demo.py

100 fixed-base arms with JointAction pick/drop cycles

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

100robots_grid_demo.py

--robot (mobile)

husky

racecar, mobile_robot

100robots_grid_demo.py

--arm-robot (arm)

panda

kuka_iiwa, arm_robot

100robots_cube_patrol_demo.py

--robot (mobile)

husky

racecar, mobile_robot

pick_drop_mobile_100robots_demo.py

--robot (mobile)

husky

racecar, mobile_robot

pick_drop_arm_100robots_demo.py

--robot (arm)

panda

kuka_iiwa, arm_robot

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=False is the single most important setting for fleet-scale throughput. Physics stepping is O(n) even with kinematic control.

  • collision_check_frequency — set to 1.0 or 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