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:

  • Load simulation parameters from a YAML config file

  • Create and use AgentManager to manage many agents as a group

  • Batch-spawn 100 robots in a grid with mixed types using GridSpawnParams

  • 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, SimulationParams

params = SimulationParams.from_config("config/100robots_config.yaml")
sim = MultiRobotSimulationCore(params)

from_config reads the YAML and maps every key to the corresponding SimulationParams field. 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. Create an AgentManager

from pybullet_fleet.agent_manager import AgentManager

manager = AgentManager(sim_core=sim)

AgentManager does not spawn any agents on its own — it manages agents that are added to it, either via spawn_agents_grid / spawn_agents_grid_mixed or by manually calling manager.add_agent(agent).


4. Define a Grid Layout with GridSpawnParams

GridSpawnParams describes a regular grid of positions:

from pybullet_fleet.agent_manager import GridSpawnParams

grid = GridSpawnParams(
    x_min=0,
    x_max=9,          # 10 columns: indices 0, 1, …, 9
    y_min=0,
    y_max=9,          # 10 rows:    indices 0, 1, …, 9
    spacing=[10.0, 10.0, 0.0],   # 10 m between agents in X and Y
    offset=[-15.0, -15.0, 0.3],  # world-frame offset of the grid origin
)

Total spawned = (x_max - x_min + 1) × (y_max - y_min + 1). With the above settings: 10 × 10 = 100 agents.

The actual world position of grid cell (i, j) is:

x = offset[0] + i * spacing[0]
y = offset[1] + j * spacing[1]
z = offset[2]

5. Define Agent Types

Define AgentSpawnParams for each robot type you want in the fleet. Here we use two: omnidirectional and differential drive.

from pybullet_fleet.agent import AgentSpawnParams, MotionMode
import os

urdf_path = os.path.abspath("robots/mobile_robot.urdf")

spawn_omni = AgentSpawnParams(
    urdf_path=urdf_path,
    mass=0.0,                            # kinematic (teleport-based, no physics)
    max_linear_vel=2.0,                  # m/s
    max_linear_accel=1.0,                # m/s²
    max_angular_vel=2.0,                 # rad/s
    max_angular_accel=5.0,
    motion_mode=MotionMode.OMNIDIRECTIONAL,
    use_fixed_base=False,
)

spawn_diff = AgentSpawnParams(
    urdf_path=urdf_path,
    mass=0.0,
    max_linear_vel=2.0,
    max_linear_accel=1.0,
    max_angular_vel=2.0,
    max_angular_accel=5.0,
    motion_mode=MotionMode.DIFFERENTIAL,
    use_fixed_base=False,
)

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.


6. Spawn 100 Agents in a Mixed Grid

spawn_agents_grid_mixed takes a list of (SpawnParams, probability) tuples. Probabilities must sum to 1.0:

manager.spawn_agents_grid_mixed(
    num_agents=100,
    grid_params=grid,
    spawn_params_list=[
        (spawn_omni, 0.5),   # 50% omnidirectional
        (spawn_diff, 0.5),   # 50% differential
    ],
)

print(f"Spawned: {len(manager.objects)} agents")

After spawning, each agent is accessible through manager.objects:

Note: The snippets below are not in the demo script. They show additional usage patterns you can use in your own code.

for agent in manager.objects:
    print(agent.motion_mode, agent.get_pose().position)

For a uniform grid (one type only), use spawn_agents_grid instead:

manager.spawn_agents_grid(
    num_agents=100,
    grid_params=grid,
    spawn_params=spawn_omni,
)

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.


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


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


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


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


11. 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],
})

12. 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 (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_urdf() 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_urdf_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