Overview Functionality Mesa with Basic Multi-agent Grid Model¶

Below is an example of a data-generating multi-agent grid model using Mesa.

In [5]:
import mesa
import os
from mesa.discrete_space import CellAgent, OrthogonalMooreGrid
from mesa.visualization import SolaraViz, make_space_component, make_plot_component

# folders
current_dir = os.path.dirname(os.path.abspath("__file__"))
data_dir = os.path.join(current_dir, 'Data_generated')
plot_dir = os.path.join(data_dir, 'plots')
csv_dir = os.path.join(data_dir, 'csvs')

print("Data directory:", data_dir)
print("Plot directory:", plot_dir)
print("CSV directory:", csv_dir)
Data directory: d:\Research_projects\multi_agent_systems_practicum\models\fullfunc_grid_python_model\grid\Data_generated
Plot directory: d:\Research_projects\multi_agent_systems_practicum\models\fullfunc_grid_python_model\grid\Data_generated\plots
CSV directory: d:\Research_projects\multi_agent_systems_practicum\models\fullfunc_grid_python_model\grid\Data_generated\csvs

So python mesa usually has the following order of operations:

  • Define an agent class

    • class MyAgent(Agent):
          def __init__(self, model):
              super().__init__(model)
              # agent attributes
      
          def step(self):
              # agent behavior
      
  • Define a model class (which may also have data collectors)

    • class MyModel(Model):
          def __init__(self, Nagents, width, height):
              self.num_agents = N
              self.grid = OrthogonalMooreGrid(width, height, torus=True)
      
          def step(self):
            self.agents.do("step")
      
  • visualize the model as it runs, Mesa uses Solara for this (optional)

  • initialize and run the model

    • model = MyModel(Nagents=100, width=10, height=10)
      for i in range(100):
            model.step()
      

So why start with the agent? Mostly conventional. It's easier to understand "what are the individual actors?" before "how do they interact as a system.". You need to know what agents can do before you can design a model that orchestrates them.

Agent class¶

Lets create a simple agent class. Could you write here in natural language what it is doing?

"[write your explanation here]"

  • What does init() do? What are these parameters for this GridAgent?
  • Could you look up in the mesa documentation what a CellAgent is exactly and describe it here?
    • #what is a Cellagent?
      
  • What does super().init() do?
In [ ]:
class GridAgent(CellAgent):
    def __init__(self, model, cell):
        super().__init__(model) # This is calling the __init__ of the parent class (CellAgent in this case) and pass on the model
        self.cell = cell
        
    def step(self):
        self.cell = self.cell.neighborhood.select_random_cell()

Now lets set up a model and and collect some data¶

Wim will run through this. Afterwards you will get some time to write down here what key parts of the below code.

In [ ]:
# function to compute the maximum distance between any two agents (for collecting model level data)
def maxdistance(model):
    positions = [agent.cell.coordinate for agent in model.agents]
    max_dist = 0
    
    # Get grid dimensions
    width, height = model.grid.dimensions
    
    for i in range(len(positions)):
        for j in range(i + 1, len(positions)):
            x1, y1 = positions[i]
            x2, y2 = positions[j]
            # Calculate distance with torus wraparound (based on Mesa's internal method)
            dx = abs(x2 - x1)
            dy = abs(y2 - y1)
            
            # Handle torus wraparound
            dx = min(dx, width - dx)
            dy = min(dy, height - dy)
            
            # Calculate Euclidean distance
            dist = (dx**2 + dy**2)**0.5
            if dist > max_dist:
                max_dist = dist
    return max_dist

class SimpleGridModel(mesa.Model):
    def __init__(self, width=10, height=10, numagents=2, seed = None):
        super().__init__(seed=seed) # This is calling the __init__ of the parent class (Model in this case) with a seed
        self.num_agents = numagents
        self.grid = OrthogonalMooreGrid((width, height), torus=True, random=self.random) # random needs to be set otherwise seed is not used
        # self = model instance, n = number of agents -> is consumed by create_agents, *args=random cell from all cells in the grid -> goes to GridAgent __init__
        GridAgent.create_agents(self, self.num_agents, self.random.choice(list(self.grid.all_cells.cells)))

        # collecting some data
        self.datacollector = mesa.DataCollector(model_reporters={"max_distance": maxdistance}, agent_reporters={"cell": lambda a: a.cell.coordinate})
        self.datacollector.collect(self)

    def step(self):
        self.agents.do("step")
        self.datacollector.collect(self)

Lets run the model for a single step¶

In [19]:
# Lets run the model for one step with two agents
model = SimpleGridModel(100, 100, 1)
for _ in range(2): # run for 10 steps
    model.step()

Visualization of a model run for multiple steps¶

So below we visualize a model run in the simplest way possible by using Mesa built-in visualization. This is not the most beautiful way to visualize a model, but it is quick and easy to set up.

You can however create your own visualizations using matplotlib or other libraries. And have an example of how to do this in the basic network model. To see how to do this: https://mesa.readthedocs.io/latest/tutorials/8_visualization_custom.html#building-custom-components

In [29]:
# Visualization for native Mesa/solara vizualization
def agent_portrayal(agent):
    return {
        "color": "#FF0000",
        "size": 20,
    }

spaceviz = make_space_component(agent_portrayal=agent_portrayal)
max_distance_plot = make_plot_component("max_distance")

model = SimpleGridModel(10, 10, 2)
page = SolaraViz(model, [spaceviz, max_distance_plot], name="Grid Agent")
page
Cannot show widget. You probably want to rerun the code cell above (Click in the code cell, and press Shift+Enter ⇧+↩).

Data collection procedure¶

Note that Mesa is using pandas dataframes to store the data. This is very convenient as it is a go-to library for data manipulation and analysis in Python.

In [ ]:
# Lets run the model
model = SimpleGridModel(100, 100, 10)
for _ in range(10): # run for 10 steps
    model.step()

# we can now retrieve the collected data
data_agents = model.datacollector.get_agent_vars_dataframe() 
data_model = model.datacollector.get_model_vars_dataframe()
In [ ]:
data_model.head() # Note that data_model is a pandas dataframe, so we can use all pandas functionality on it (.head(), .describe(), .plot(), ...)
Out[ ]:
max_distance
count 11.000000
mean 6.443791
std 2.919813
min 0.000000
25% 5.277449
50% 7.211103
75% 8.332292
max 9.433981
In [23]:
data_agents.head()
Out[23]:
cell
Step AgentID
0 1 (47, 20)
2 (47, 20)
3 (47, 20)
4 (47, 20)
5 (47, 20)
In [24]:
# write to csv
data_model.to_csv(os.path.join(csv_dir,'model_data.csv'))
data_agents.to_csv(os.path.join(csv_dir, 'agent_data.csv'))

print("go look in " + csv_dir)
go look in d:\Research_projects\multi_agent_systems_practicum\models\fullfunc_grid_python_model\grid\Data_generated\csvs
In [25]:
import matplotlib.pyplot as plt
# lets now plot the max distance over time and save a plot
plt.figure(figsize=(10, 6))
plt.plot(data_model.index, data_model['max_distance'], marker='o')
plt.title('Max Distance Between Agents Over Time')
plt.xlabel('Step')
plt.ylabel('Max Distance')
plt.grid()
plt.savefig(os.path.join(plot_dir, 'max_distance_plot.png'))
plt.show()
No description has been provided for this image

Model¶

But wait. Were only collecting data from one model. How do we know that our model consistently has similar behavior? Different random picks will yield different initial positions and different movement patterns. And maybe we also want to see how different combinations of parameters affect the model behavior.

Mesa has a built-in way to run multiple iterations of a model with different random seeds. This is done using the mesa.batch_run command.

  • What does "numagents" do in params do? And what does it do here?
  • Inspect the csv file that is generated. What do you see? How is it structured?
In [26]:
import pandas as pd
params = {"width": 10, "height": 10, "numagents": (2, 10)}

results_5seeds = mesa.batch_run(
    SimpleGridModel,
    parameters=params,
    iterations=5, # we now explicitly set seeds to run through
    max_steps=100,
    number_processes=1,
    data_collection_period=1,  # Need to collect every step
    display_progress=True,
)

results_5seeds = pd.DataFrame(results_5seeds)
# write away
results_5seeds.to_csv(os.path.join(csv_dir, 'batch_run_5seeds.csv'))
print("go look for " + csv_dir + 'batch_run_5seeds.csv')
results_5seeds.head()
  0%|          | 0/10 [00:00<?, ?it/s]
go look for d:\Research_projects\multi_agent_systems_practicum\models\fullfunc_grid_python_model\grid\Data_generated\csvsbatch_run_5seeds.csv
Out[26]:
RunId iteration Step width height numagents max_distance AgentID cell
0 0 0 0 10 10 2 0.0 1 (8, 3)
1 0 0 0 10 10 2 0.0 2 (8, 3)
2 0 0 1 10 10 2 1.0 1 (7, 2)
3 0 0 1 10 10 2 1.0 2 (7, 3)
4 0 0 2 10 10 2 1.0 1 (6, 3)

Exctracting relevant model-level data¶

We have agent level data and model level data, how do we extract model level data only?

In [27]:
# Get model data by selecting only AgentID 1 (or any single agent)
model_only_data5s = results_5seeds[results_5seeds['AgentID'] == 1]

# Then drop the AgentID and cell columns since you don't need them
model_only_data5s = model_only_data5s.drop(columns=['AgentID', 'cell'])
In [28]:
## Visualizing multi-iteration simulated data
In [20]:
import matplotlib.pyplot as plt

fig = plt.figure(figsize=(12, 8))
unique_numagents = model_only_data5s['numagents'].unique()
for i, numagents in enumerate(sorted(unique_numagents), 1):
    ax = fig.add_subplot(2, (len(unique_numagents) + 1) // 2, i)
    subset = model_only_data5s[model_only_data5s['numagents'] == numagents]
    for seed, group in subset.groupby('iteration'):
        # Sort by Step to ensure proper line connection
        group_sorted = group.sort_values('Step')
        ax.plot(group_sorted['Step'], group_sorted['max_distance'], 
               marker='o', label=f'seed {seed}')
    ax.set_title(f'Num Agents: {numagents}')
    ax.set_xlabel('Step')
    ax.set_ylabel('Max Distance')
    ax.grid()
    ax.legend()

# lets save the plot
plt.tight_layout()
plt.savefig(os.path.join(plot_dir, 'batch_run_max_distance.png'))
No description has been provided for this image