{
"cells": [
{
"cell_type": "markdown",
"id": "cfb85ff0",
"metadata": {},
"source": [
"# Agent-Based Modeling\n",
"\n",
"In this set of lectures, we'll study how to design agent-based models in Python. \n",
"\n",
"> An *agent-based model* (ABM) is a simulation model in which many individual entities (*agents*) interact with each other according to fixed rules. \n",
"\n",
"ABMs are often used for modeling a wide range of social and biological systems. In fact, you've already seen an example of an ABM: the SIR model of disease spred that we studied in the previous lecture is one. There, we relied on tools from NetworkX and various other familiar programming paradigms. We'll now explore the topic of agent-based modeling from a somewhat more systematic and flexible perspective. \n",
"\n",
"There exist a large number of dedicated software packages for agent-based modeling. In this course, we'll use a relatively recent package, called [Mesa](https://mesa.readthedocs.io/en/master/index.html), for agent-based modeling in Python. To install the software, run the following code in your terminal: \n",
"\n",
"```\n",
"conda activate PIC16B\n",
"conda install -c conda-forge mesa\n",
"```\n",
"\n",
"# The Schelling Model of Racial Segregation\n",
"\n",
"In this set of lecture notes, we will implement the Schelling model of racial residential segregation. The Schelling model is a parable of how only *mild* individual biases can lead to highly segregated outcomes. \n",
"\n",
"\n",
"In the Schelling Model, individuals of two types begin arranged randomly on a grid, which is often taken to represent a city. Not all grid squares are occupied. Here's an example starting configuration\n",
"\n",
"\n",
"\n",
"Here's how the model works: \n",
"\n",
"1. At each timestep, agents look at their surroundings. An agent is **unhappy** if fewer than 1/3 of their neighbors have the same type, and is **happy** otherwise. \n",
"2. All **unhappy** agents pick a random empty spot and move there. All **happy** agents stay where they are. \n",
"\n",
"We run the model until all agents are happy. The fundamental result of the model is that, even though agents have only mild biases -- they simply prefer not to be outnumbered -- acting on their preferences can still lead to highly segregated outcomes, like this: \n",
"\n",
"\n",
"\n",
"For an excellent interactive demonstration of the Schelling Model, check out [this blog post](https://ncase.me/polygons/) by Vi Hart and Nicky Case. \n",
"\n",
"### A Note on History\n",
"\n",
"The Schelling model does not include any concepts of historical oppression, wealth, or power, all of which contribute to racial segregation. The message of the Schelling model is that these factors are not **needed** for segregation -- mildly racist individual preferences would be enough. It is important, however, not to confuse this mathematical parable with the actual historical circumstances of racial segregation in the US or elsewhere. In most societies, including the US, racial segregation arises because of systematic oppression enforced by policy, violence, and erasure. \n",
"\n",
"### Sources\n",
"\n",
"These lecture notes are closely based on the [Schelling model example](https://github.com/projectmesa/mesa/tree/main/examples/schelling) in the [official Mesa repository](https://github.com/projectmesa/mesa). They also draw on the [Introductory Tutorial](https://mesa.readthedocs.io/en/master/tutorials/intro_tutorial.html) from the official Mesa documentation. \n",
"\n",
"# Implementing the Schelling Model"
]
},
{
"cell_type": "markdown",
"id": "601ffa74",
"metadata": {},
"source": [
"Let's start by implementing a bare-bones model. While there is some flexibility in how one does this, there are a few common features of most Mesa models: \n",
"\n",
"1. There must be an *agent* class, which should inherit from `mesa.Agent`. This class specifies the properties and behaviors of an individual agent in the simulation. \n",
" - This class must call `mesa.Agent.__init__()` as part of its `__init__()` method. \n",
" - This class must have a `step()` method which describes the primary individual behavior. \n",
"2. There must be a *model* class, which should inherit from `mesa.Model`. \n",
" - The `__init__()` method of this class is responsible for creating agents with their properties, as well as the space (often a grid) on which the simulation unfolds. \n",
" - This class must also have a `step()` method which provides a complete description of what happens in a single model time step. Often, this involves using a `Schedule` to call the `step()` method of each of the agents in some specified sequence. \n",
" \n",
"Let's write a very simple model that demonstrates some of these requirements. Our model won't really do very much yet, but it will demonstrate the key techniques of defining the agent and model, adding agents to the model, and calling the `step()` methods. "
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "41b6285e",
"metadata": {},
"outputs": [],
"source": [
"from mesa import Model, Agent\n",
"from mesa.time import RandomActivation\n",
"\n",
"class ToyAgent(Agent):\n",
" \n",
" def __init__(self, name, model):\n",
" super().__init__(name, model)\n",
" self.name = name\n",
" \n",
" def step(self):\n",
" print(f\"Hi, I'm Agent 00{self.name}!\")\n",
"\n",
"class ToyModel(Model):\n",
" \n",
" def __init__(self, n_agents):\n",
" \n",
" self.schedule = RandomActivation(self)\n",
" \n",
" for i in range(n_agents):\n",
" agent = ToyAgent(i, self)\n",
" \n",
" # important and easy to forget! \n",
" # this line \"registers\" the agent\n",
" # with the scheduler so that the \n",
" # agent's step() method will be\n",
" # called when the scheduler's \n",
" # is. \n",
" self.schedule.add(agent)\n",
" \n",
" def step(self):\n",
" self.schedule.step()"
]
},
{
"cell_type": "markdown",
"id": "6112949c",
"metadata": {},
"source": [
"Let's demonstrate the behavior of our toy model: "
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "323b679e",
"metadata": {},
"outputs": [],
"source": [
"TM = ToyModel(10)"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "f64f2900",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Hi, I'm Agent 007!\n",
"Hi, I'm Agent 006!\n",
"Hi, I'm Agent 003!\n",
"Hi, I'm Agent 008!\n",
"Hi, I'm Agent 004!\n",
"Hi, I'm Agent 005!\n",
"Hi, I'm Agent 002!\n",
"Hi, I'm Agent 009!\n",
"Hi, I'm Agent 001!\n",
"Hi, I'm Agent 000!\n"
]
}
],
"source": [
"TM.step()"
]
},
{
"cell_type": "markdown",
"id": "85ccfd11",
"metadata": {},
"source": [
"Observe that, each time we call `TM.step()`, the model sweeps through the various agents and calls their individual `step()` methods. This is because we created a `RandomActivation` schedule, and added each of the agents to this schedule.\n",
"\n",
"With our architecture in place, our next step is learn how to implement more interesting behaviors. \n",
"\n",
"## Spatial Grids\n",
"\n",
"The Schelling model usually evolves on a grid. At the moment, we don't have a grid incorporated. Fortunately, this is easy to bring in. We simply need to add a `SingleGrid` object with specified width and height. The `torus` argument of the grid determines whether the edges \"wrap around.\" If it is selected, then walking off the left side of the grid will put you back on the right side. This is often visualized as allowing the grid to lie on the surface of a torus, or donut: \n",
"\n",
"\n",
"\n",
"The modifications we need to make to our previous code are relatively simple: \n",
"\n",
"1. We need to give each `ToyAgent` a `pos`ition. \n",
"2. We need to give the model a `grid` instance variable. \n",
"3. We need to modify our initialization of agents so that we call `self.grid.position_agent(agent, pos)` in order to place each agent on the grid. "
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "6e4eacad",
"metadata": {},
"outputs": [],
"source": [
"from mesa.space import SingleGrid\n",
"\n",
"class ToyAgent(Agent):\n",
" \n",
" # adding a pos instance variable so that each agent can remember\n",
" # where they are. Note that the pos can take the place of the name. \n",
" def __init__(self, pos, model):\n",
" super().__init__(pos, model)\n",
" self.pos = pos\n",
" \n",
" def step(self):\n",
" print(f\"Hi, I'm an agent at {self.pos}!\")\n",
"\n",
"class ToyModel(Model):\n",
" \n",
" # need to specify width, height, and density of agents\n",
" # in the grid. \n",
" def __init__(self, width, height, density):\n",
" \n",
" self.schedule = RandomActivation(self)\n",
" \n",
" # create the grid\n",
" self.grid = SingleGrid(width, height, torus=True)\n",
" \n",
" # loop through the grid, and add agents so that the \n",
" # overall density is roughly equal to the passed \n",
" # density\n",
" for cell in self.grid.coord_iter():\n",
" x = cell[1]\n",
" y = cell[2]\n",
" if self.random.random() < density:\n",
" \n",
" agent = ToyAgent(pos = (x, y), model = self)\n",
" self.schedule.add(agent) \n",
" self.grid.position_agent(agent, (x, y))\n",
" \n",
" # this doesn't change. \n",
" def step(self):\n",
" self.schedule.step()"
]
},
{
"cell_type": "markdown",
"id": "b8f3a9fd",
"metadata": {},
"source": [
"Now we can again instantiate our model. This time, we need to pass `width`, `height`, and `density`. Here, we're creating a 10x10 grid in which roughly 10% of cells have agents in them. "
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "42cea0f8",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Hi, I'm an agent at (5, 0)!\n",
"Hi, I'm an agent at (1, 3)!\n",
"Hi, I'm an agent at (8, 5)!\n",
"Hi, I'm an agent at (9, 4)!\n",
"Hi, I'm an agent at (1, 4)!\n",
"Hi, I'm an agent at (4, 5)!\n",
"Hi, I'm an agent at (9, 1)!\n"
]
}
],
"source": [
"TM = ToyModel(10, 10, 0.1)\n",
"TM.step()"
]
},
{
"cell_type": "markdown",
"id": "cfce30e5",
"metadata": {},
"source": [
"It's also possible to directly extract the grid and visualize it using familiar tools. In a later lecture, however, we'll see some much better ways to visualize the grid. "
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "04bb3a84",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
""
]
},
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
},
{
"data": {
"image/png": "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\n",
"text/plain": [
"