Overview Functionality Mesa with Basic Multi-agent Grid Model¶
Below is an example of a data-generating multi-agent grid model using Mesa.
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?
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.
# 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¶
# 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
# 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
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.
# 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()
data_model.head() # Note that data_model is a pandas dataframe, so we can use all pandas functionality on it (.head(), .describe(), .plot(), ...)
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 |
data_agents.head()
cell | ||
---|---|---|
Step | AgentID | |
0 | 1 | (47, 20) |
2 | (47, 20) | |
3 | (47, 20) | |
4 | (47, 20) | |
5 | (47, 20) |
# 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
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()
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?
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
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?
# 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'])
## Visualizing multi-iteration simulated data
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'))