import matplotlib
if not hasattr(matplotlib.RcParams, "_get"):
matplotlib.RcParams._get = dict.get
4.2.1: Exercises#
Important
For live code, launch Thebe by clicking on the in the toolbar. Your code edits are automatically saved in your browser. Use the button to export your progress to a file, to import a previously saved session, or to reset to the original code.
4.2.1.1: The base model#
Attention
This model is up-to-date with mesa version 3.5.0
%pip install mesa==3.5.0
%pip install seaborn
%pip install networkx
4.2.1.1.1: Understand the Game of Life model#
#import libraries
import mesa # Importing the Mesa library for agent-based modeling
# Print the version of the mesa library being used
print("You are using mesa version: " + mesa.__version__)
from mesa import Model # Importing the Model class from Mesa
from mesa.discrete_space import CellAgent, OrthogonalMooreGrid # Importing the OrthogonalMooreGrid for a grid space where multiple agents can occupy the same cell
from mesa.datacollection import DataCollector # Importing DataCollector to collect data from the model
import seaborn as sns # Importing Seaborn for data visualization
import pandas as pd # Importing Pandas for data manipulation
import matplotlib.pyplot as plt # Importing Matplotlib for plotting
Model metrics#
# Define a function to compute the number of alive cells
def compute_alive(model):
# Count and return the number of cells that are currently alive in the model
return sum(1 for cell in model.agents if cell.state == "Alive")
Agent class#
# Define the Cell agent, which represents a single cell in the Game of Life.
class Cell(CellAgent):
def __init__(self, model, cell, initial_state="Alive"):
super().__init__(model) # Initialize the parent Agent class with the position as the unique ID
self.cell = cell # x and y coordinates of the cell
self.state = initial_state # The current state of the cell, either "Alive" or "Dead"
self.next_state = None # This stores the state the cell will have in the next step
def step(self):
# Count the number of alive neighbors
# Get the agents which are alive directly next to the agent.
# Since cell.neighborhood returns a list we iterate over that list to find the alive cells.
living_neighbors = sum(1 for neighbor in self.cell.neighborhood for agent in neighbor.agents if agent.state == "Alive")
# Apply Conway's Game of Life rules to determine the next state
if self.state == "Alive":
# Any live cell with fewer than two live neighbors dies (underpopulation)
# Any live cell with more than three live neighbors dies (overpopulation)
if living_neighbors < 2 or living_neighbors > 3:
self.next_state = "Dead"
else:
self.next_state = "Alive" # Any live cell with two or three live neighbors lives on to the next generation
else:
# Any dead cell with exactly three live neighbors becomes a live cell (reproduction)
if living_neighbors == 3:
self.next_state = "Alive"
else:
self.next_state = "Dead" # Otherwise, the cell remains dead
# SimultaneousActivation requires the advance function to update the state of the cells after all cells have completed their steps.
# This function is called internally by the model. This function is only necessary when using SimultaneousActivation.
def advance(self):
# Update the current state to the next state determined in the step() method
self.state = self.next_state
Model class#
# Define the main model for the Game of Life
class LifeModel(Model):
def __init__(self, width, height, seed=None):
super().__init__(rng=seed) # Initialize the parent Model class
# Create a grid of the given width and height. The torus argument makes the grid wrap around the edges.
# Capacity means that the grid will only take 1 agent present in a single cell
self.grid = OrthogonalMooreGrid((width, height), capacity = 1, torus=True, random = self.random)
self.grid_size = width* height
# Initialize the DataCollector to collect data on the number of alive cells at each step
self.datacollector = DataCollector(
model_reporters={"Alive": compute_alive}
)
# Create the agent cells. By adding weights to the choices we confirm that around 25% of the cells are Alive.
cells = Cell.create_agents(
model = self,
n = self.grid_size,
initial_state = self.random.choices(["Alive", "Dead"], weights = [0.25, 0.75], k = self.grid_size),
cell=self.grid.all_cells.cells,
)
# Collect data at step 0 (initial state of the model)
self.datacollector.collect(self)
def step(self):
# Advance the model by one step, activating all agents simultaneously
self.agents.do("step")
self.agents.do("advance")
# Collect data after agents take a step
self.datacollector.collect(self)
You have just read how the Cell agent decides its next state from the number of live neighbours around it. Before moving on, use the cards below to fix the four rules that drive that decision firmly in mind.
Cell agent's step method.With these four conditions clear, the flow-chart task in the next subsection becomes a question of when each rule is applied within a single step.
With the four rules fresh in mind, the surest way to make them stick is to apply them. Each small neighbourhood below shows one cell (ringed) and the live neighbours around it; decide what that cell becomes on the next tick.
Applied one cell at a time, the four rules are simple bookkeeping on a neighbour count; the richness of the Game of Life comes from applying them to every cell of the grid at once, which is what the flow chart in the next task must capture.
Task: convert the base model into a flow chart#
Convert the base model into a flow chart that tells a story of what is going on. You may use pen and paper for this.
A flow chart is really just the model’s steps drawn in the right order. Before you draw your own, try rebuilding one tick of the Game of Life from its shuffled steps below.
Notice that only the setup happens once, while counting, deciding, and committing repeat every tick, and that counting must finish before any cell is written: that reads-before-writes loop is precisely what your flow chart needs to capture.
Solution
To create a flowchart that represents the base agent-based model, you can follow these steps:
Start:
Begin the flowchart with an oval labeled “Start.”
Initialise the Model:
Process Box: Draw a rectangle and label it “Initialise Grid (Width, Height)”.
Process Box: Another rectangle labeled “Randomly select 25% of cells as ‘Alive’”.
Process Box: Add a rectangle labeled “Create Cell agents and place on grid”.
Process Box: “Initialise Simultaneous Activation” to ensure all agents update simultaneously.
Main Loop:
Decision Box: Draw a diamond labeled “Has Termination Condition Been Met?” (This condition can be a fixed number of steps).
Yes Path: Draw an arrow from this decision box to an oval labeled “End”.
No Path: If the termination condition isn’t met, continue to the next steps in the loop.
Step 1: Count Neighbours:
Process Box: Draw a rectangle labeled “Each cell counts its living neighbours (Moore neighborhood)”.
Step 2: Determine Next State:
Decision Box: Draw a diamond labeled “Is Cell Alive?”.
Yes Path:
Decision Box: Draw another diamond labeled “Living neighbours < 2 or > 3?”.
Yes Path: “Next State = Dead” (draw a rectangle).
No Path: “Next State = Alive” (draw a rectangle).
No Path:
Decision Box: Draw another diamond labeled “Living neighbours = 3?”.
Yes Path: “Next State = Alive” (draw a rectangle).
No Path: “Next State = Dead” (draw a rectangle).
Step 3: Update States:
Process Box: Draw a rectangle labeled “Update all cells to next state simultaneously”.
Data Collection:
Process Box: Draw a rectangle labeled “Collect data on the number of alive cells”.
Repeat Loop:
Draw an arrow from “Collect data on the number of alive cells” back to the main loop’s decision box labeled “Has Termination Condition Been Met?”.
This indicates the repetition of the loop until the termination condition is satisfied.
End:
When the termination condition is met, direct the flow to an oval labeled “End”.
Summary:
Start -> Initialise Grid -> Randomly Select Alive Cells -> Create Cell Agents -> Initialise Simultaneous Activation -> (Loop Start) Has Termination Condition Been Met? -> No -> Count Neighbours -> Determine Next State -> Update States -> Collect Data -> (Loop Back to Condition Check) -> Yes -> End
Draw the flowchart: You can use pen and paper or software tools like Google Drawings or Lucidchart.
4.2.1.1.2: Run and visualise the base model#
# Animation and plotting code
# Note: Rendering 600 frames takes about 1-2 minutes, so please be patient.
# You can reduce the number of frames to decrease the time required.
# The number of frames is set to 600, but you might need to increase it to observe behaviors that won't be apparent at frame 600.
# You do not have to understand every line in the code below. This has been prepared for you to experiment. However, it is beneficial if you understand what is going on.
from matplotlib.animation import FuncAnimation # For creating animations
from IPython.display import HTML # For displaying animations inline in Jupyter notebooks
import seaborn as sns # For data visualization
import matplotlib # For general plotting
import matplotlib.pyplot as plt # For creating plots
import matplotlib.colors as mcolors # For handling colors in plots
import time # For tracking execution time
# Increase the limit for embedding animations in notebooks
matplotlib.rcParams['animation.embed_limit'] = 2**128
start_time = time.time() # Record the start time to measure execution duration
# Initialize a new Game of Life model
model = LifeModel(50, 50) # Replace these dimensions as needed
model.steps = 1 # Set the initial step count to 1
# Create a figure with one subplot for displaying the grid
fig, ax = plt.subplots(figsize=(5, 5), dpi=100)
# Create a custom color map: light grey for dead cells, green for alive cells
cmap = mcolors.ListedColormap(['lightgrey', 'green'])
bounds = [0, 1, 2] # Define boundaries for the color map
norm = mcolors.BoundaryNorm(bounds, cmap.N) # Normalize the color map
# Function to plot the grid at each step
def plot_grid(model):
ax.clear() # Clear the axis to prepare for the new plot
# Initialize a grid filled with 0s (dead cells)
grid = [[0 for _ in range(model.grid.width)] for _ in range(model.grid.height)]
# Update the grid with alive cells (set to 1)
for cell in model.agents:
x, y = cell.cell.coordinate #Get the x and y coordinate of the cell
grid[x][y] = 1 if cell.state == 'Alive' else 0
# Use Seaborn to create a heatmap of the grid
sns.heatmap(grid[::-1], cbar=False, ax=ax, square=True, cmap=cmap, norm=norm, linewidths=.5)
# Set the title of the plot with the current step number
ax.set_title(f"Game of Life: Step {model.steps}", fontsize=16)
ax.tick_params(axis='both', which='major', labelsize=8)
# Set ticks on the x and y axes
ax.set_xticks(range(0, model.grid.width, 5))
ax.set_yticks(range(0, model.grid.height, 5))
ax.set_xticklabels(range(0, model.grid.width, 5))
ax.set_yticklabels(range(model.grid.height - 1, -1, -5))
# Initial plot to display the starting grid
plot_grid(model)
# Update function for the animation
def update(frame):
if frame > 0:
model.step() # Advance the model by one step
plot_grid(model) # Update the plot with the new grid state
# Create the animation, updating every frame
anim = FuncAnimation(fig, update, frames=600, repeat=False)
# Display the animation in the notebook
output = HTML(anim.to_jshtml())
end_time = time.time() # Record the end time to calculate total duration
elapsed_time = end_time - start_time # Calculate elapsed time
print(f"Time taken: {elapsed_time} seconds") # Output the time taken to complete the process
output # Return the animation output to be displayed
Time taken: 47.93819212913513 seconds