Structure of RobotSwarmSimulator#

Let’s take a look at the different components of RobotSwarmSimulator.

World Map#

The World

The world contains all the objects that will be simulated.

Population

The population is the collection of agents in the world.

Agent

An agent is an object that exists in the world and has volition.

Sensor

An agent can have sensors which distill information from the world into observations for the agent to use on each step() of the simulation.

Controller

Each agent has a controller that can control the agent’s movement and act based on the sensor information each step() of the simulation.

Agent

A population often has multiple agents, each of which can have a different type, controller, or set of sensors.

World Objects (props)

objects are a special type of agent that are not part of the population. It is used to represent objects in the world that are not agents, such as walls, props, and triggers. They are stored in the world’s objects list.

Spawners

spawners can create new agents and add them to the population.

Metrics

A world can have one or more metrics which reduce the state of the world. They can describe the behavior of the agents and are useful for quantifying or training global behaviors.

Subscribers

A world can have subscribers which allow user-defined hooks to run each step() of the simulation.

Initialization Order#

The initialization system machinery typically starts with the World_from_config() function. Then, the world object runs its __init__() method.

If using the simulate module, the simulation initializes the world by calling world.setup().

Here is the order in which the initialization system runs:

  • simulate

    First, the simulation initializes the world.

    • World.setup

      The world then runs setup(), which creates the following:

      1. World.population Agent.__init__()

        The world then creates agents from agent configs in its config.agents list, and back-references to the world are added to these agents. These agents are then appended to the world’s population list.

        Upon initialization, each agent also initializes its controller and sensors from its config, and back-references to the agent are passed to them.

        1. Agent.controller Controller.__init__()

        2. Agent.sensors Sensor.__init__()

      2. World.spawners Spawner.__init__()

        Spawners are created from spawner configs and appended to the world’s config.spawners list.

        Note

        Spawners are step()ed once during the world’s World.setup() method.

      3. World.objects WorldObject.__init__()

        A similar process to the population initialization is carried out for world objects.

      4. World.metrics metrics.__init__

        Lastly, any metrics in the world’s config.metrics list are created and appended to the world’s metrics list.

      5. World.spawners Spawner.step()

        By default, step() is called on all spawners in the world’s spawners list. This is done so that spawners have a chance to run before the first step() of the simulation, but this can be disabled with the step_spawners=False argument to World.setup().

Hint

Keep in mind that the initialization system operates on the configs, but you can side-step it entirely by creating the objects in code yourself!

Simulation Loop#

The simulator runs on a single thread. Let’s take a look at the execution order inside the simulation loop. On each tick of the simulation, the following happens:

The swarmsim.world.simulate.main() function runs the main simulation loop.

Event Handling

If the simulation is not in headless mode, input events are handled first.

The simulator then asks the world object to step() itself.

Outputting to the screen can be slow, so when the user requests the simulation to speedup, step() will be called multiple times, skipping event handling and draw() calls.

RectangularWorld.step()

For each spawner in World.spawners

spawner.step()

Each spawner performs its step() method at this point.

Spawners have a mark_for_deletion flag to remove them from the list of World.spawners. If a spawner has oneshot=True, then it will be marked for deletion after its first step().

For each agent in World.population:

Agent.step()

Each agent performs its step() method at this point.

Its step() method may call step() on its controller or sensors.

Here’s the execution order for MazeAgent.step():

Controller.get_actions()

The controller returns actions to take based on the Agent.sensors.

agent changes its state

The agent carries out the actions from the controller, moving according to its agent-specific dynamics.

For each sensor in Agent.sensors:

Sensor.step()

The agent updates its sensors so that the new current_state will be available on the next step().

For each object in World.objects:

WorldObject.step()

A world object is based on the Agent class, so its step() method may also call step() on its controller or sensors, if it has any.

For each metric in World.metrics:

AbstractMetric.calculate()

Following the step() , each metric takes an observation of the world’s state and performs its calculations, storing the results in its value_history attribute.

If the simulation is not operating headlessly, then the simulator calls draw() on the world object.

Step rate is not tied to FPS!

draw() is not guaranteed to be called once per step()! It is called even when the simulation is paused.

When the simulation is sped up, some draw() calls will be skipped.

draw

There is currently no layering system, so the first things that are drawn are the bottom-most ‘layer’ and succeeding draw() calls draw on top.

First, the screen is cleared. Then, draw() is called:

For each object in World.objects:

Object.draw()

For each agent in World.population:

Agent.draw()

As an example, see StaticAgent.draw().