Mesa-Geo Introductory Model#

To overview the critical parts of Mesa-Geo this tutorial uses the pandemic modelling approach known as a S(usceptible), I(infected) and R(ecovered) or SIR model.

Components of the model are:

Agents: Each agent in the model represents an individual in the population. Agents have states of susceptible, infected, recovered, or dead. The Agents are point agents, randomly placed into the environment.

Environment: The environment is a set of polygons of a few Toronto neighborhoods.

Interaction Rules: Susceptible agents can become infected with a certain probability, if they come into contact with infected agents. Infected agents then recover after a certain period or perish based on a probability.

Parameters:

  • Population Size (number of human agents in the model)

  • Initial Infection (percent of the population initial infected)

  • Exposure Distance (proximity suscpetible agents must be to infected agents to possibly get infected)

  • Infection Risk (probability of becoming infected)

  • Recovery Rate (time infection lasts)

  • Mobility (distance agent moves)

The tutorial then proceeds in three parts:

  • Part 1 Create the Basic Model

  • Part 2 Add Agent Behaviors and Model Complexity

  • Part 3 Add Visualizations and Interface

(You can use the table of contents button on the left side of the interface to skip to any specific part)

Users can use Google Colab* (Please ensure you run the Colab dependency import cell- below)

Open In Colab

*Based on a recent Google Colab update currently, Solara visuals are not rendering. However, the link still provides a easy way to download the jupyter file. You can see the issue on the Solara GitHub site

You can also download the file directly from GitHub

#Run this if in colab or if you need to install mesa and mesa-geo in your local environment. 
!pip install mesa-geo --quiet
!mkdir -p data
!wget -P data https://raw.githubusercontent.com/projectmesa/mesa-geo/main/docs/tutorials/data/TorontoNeighbourhoods.geojson

Part 1 Create the Basic Model#

This portion initializes the human agents, the neighborhood agents, and the model class that manages the model dynamics.

First we import our dependencies

This cell imports the specific libraries we need to create our model.

  • Shapley a library GIS library for object in the cartesian plane. From Shapely we specifically need the Point class to create our human agents

  • Mesa the parent ABM library to Mesa-Geo

Then of course mesa-geo which although not strictly necessary we also specifically import the visualization part of the library so we do not have to write out mesa-geo.visualization modules when we call them.

from shapely.geometry import Point

import mesa
import mesa_geo as mg
import mesa_geo.visualization as mgv # the warning that appears from Solara is fixed in Mesa 3.0 you can install the pre-release with pip install -U --pre-mesa

Create the person agent class#

The person in this model represents one human being and we initilaize each person agent with two key parts:

  1. The agent attributes, such as recovery rate and death risk

  2. The step function, actions the agent will take each model step

The first thing we are going to do is create the person agent class. This class has several attributes necessary to make a more holistic model.

First, there are the required attributes for any GeoAgent in Mesa-Geo

  • unique_id: Some unique identifier, often an int, this ensure Mesa can keep track of each agent without confusion

  • model: Model object class that we will build later, this is a pointer to the model instance so the agent can get information from the model as it behaves

  • geometry: GIS geometric object in this case a GIS Point

  • crs: A string describing the coordinate reference system the agent is using

As you can see these are inherited from the mesa-geo librarary through the “mg.GeoAgent” in the class instantiation.

Second, the variable attributes these are unique to our SIR model:

  • agent_type: A string which describes the agent state (susceptible, infected, recovered, or dead)

  • mobility_range: Distance the agent can move in meters

  • infection risk: A float from 0.0 to 1.0 that determines the risk of the agent being infected if exposed.

  • recovery_rate: A float from 0.0 to 1.0 that determine how long the agents takes to recover

  • death_risk: A float from 0.0 to 1.0 that determines the probability the agent will die

The __repr__ function is a Python primitive that will print out information as directed by the code. In this case we will print out the agent ID

The step function is a Mesa primitive that the scheduler looks for and describes what action the agent takes each step

class PersonAgent(mg.GeoAgent):
    """Person Agent."""

    def __init__(
        self,
        unique_id,
        model,
        geometry,
        crs,
        agent_type,
        mobility_range,
        infection_risk,
        recovery_rate,
        death_risk
    ):
        super().__init__(unique_id, model, geometry, crs)
        # Agent attributes
        self.atype = agent_type
        self.mobility_range = mobility_range
        self.infection_risk=infection_risk,
        self.recovery_rate = recovery_rate
        self.death_risk = death_risk

    def __repr__(self):
        return "Person " + str(self.unique_id)

    def step(self): 
        print (repr(self))
        print(self.atype, self.death_risk, self.recovery_rate)

Create the neighborhood agent#

The neighborhood in this model represents one geographical area as defined by the geojson file we uploaded.

Similar to the person agent, we initialize each neighborhood agent with the same two key parts.

  1. The agent attributes, such as geometry and state of neighborhood

  2. The step function, behaviors the agent will take during each model step.

Similar to the person agent for the neighborhood agent there are two types of attributes.

The required attributes for any GeoAgent in Mesa-Geo:

  • unique_id: For geographic agents such as a neighborhood mesa-geo will assign a very large integer as the agent id, if desired users can specify their own.

  • model: Model object class that we will build later, this is a pointer to the model instance so the agent can get information from the model as it behaves

  • geometry: GIS geometric object in this case a polygon form the geojson defining the perimeter of the neighborhood

  • crs: A string describing the coordinate reference system the agent is using

Similar to the person agent, “mg.GeoAgent” is inherited from mesa-geo.

Next are the variable attributes:

  • agent_type: A string which describes the state of the neighborhood which will be either safe or hot spot

  • hotspot_threshold: An integer that is the number of infected people in a neighborhood to call it a hotspot

We will also use the __repr__ function to print out the agent ID

Then the step function, which is a primitive that the Mesa scheduler looks for and describes what action the agent takes each step

class NeighbourhoodAgent(mg.GeoAgent):
    """Neighbourhood agent. Changes color according to number of infected inside it."""

    def __init__(
        self, unique_id, model, geometry, crs, agent_type="safe", hotspot_threshold=1
    ):
        super().__init__(unique_id, model, geometry, crs)
        self.atype = agent_type
        self.hotspot_threshold = (
            hotspot_threshold  # When a neighborhood is considered a hot-spot
        )

    def __repr__(self):
        return "Neighbourhood " + str(self.unique_id)
        
    def step(self):
        """Advance agent one step."""
        print(repr(self))
    
    

Create the Model Class#

The model class is the manager that instantiates the agents, then manages what is happening in the model through the step function, and collects data.

We will create the model with parameters that will set the attributes of the agents as it instantiates them and a step function to call the agent step function.

First, we name our class in this case GeoSIR and we inherit the model class from Mesa. We store the path to our GeoJSON file in the object geojson regions. As JSONs mirror Pythons dictionary structure, we store the key for the neighbourhood id (“HOODNUM”) in the variable unique_id.

Second, we set up the python initializer to initiate our model class. To do this we will, set up key word arguments or kwargs of the parameters we want for our model. In this case we will use:

  • population size (pop_size): An integer that determines the number of person agents

  • initial infection (init_infection): A float between 0.0 and 1.0 which determines what percentage of the population is infected as the model initiates

  • exposure_distance (exposure_dist): An integer for the distance in meters a susceptible person agent must within to be infected by a person agent who is infected

  • maximum infection risk (max_infection_risk): A float between 0.0 and 1.0 of which determines the highest suscpetibility rate in the population

Third, we initialize our agents. Mesa-Geo has an AgentCreator class inside is geoagent.py file that can create GeoAgents from files, GeoDataFrames, GeoJSON or Shapely objects.

Creating the NeighbourhoodAgents

In this case we will use the torontoneighbourhoods.geojson file located in the data folder to to create the NeighbourhoodAgents. Next, we will add them to the environment with the space.add_agents function. Then we will iterate through each of the NeighbourhoodAgents to add them to the schedule.

Creating the PersonAgents

We will use Mesa-Geo AgentCreator to create the person agents. To create a heterogeneous (diverse) population we will use the random object created as part of Mesa’s base class to help initialize the population’s parameters.

  • death_risk: A float from 0 to 1

  • agent_type: Compares the model parameter of initial infection of a random float between 0 and 1 and the initial infection parameter. If it is less than the initial infection parameter the agent is initialized as infected.

  • recover: Is an integer between 1 and the recovery rate. This determines the number of steps it takes for the agent to recover.

  • infection_risk: is a float between 0 and the parameter of max_infection_risk, which will then determine how likely a person is to get infected.

  • death_risk: Is a random float between 0 and 1 that will determine how likely a person is to die when infected.

By using Python’s random library to create these attributes for each agent, we can now create a diverse agent population.

Passing these parameters through the AgentCreator class we initialize our agent object.

As Mesa-Geo is an GIS based ABM, we need assign each PersonAgent a Geometry and location. To do this we will use a helper function find_home. This helper function first identifies a NeighbourhoodAgent where the PersonAgent will start. Next it identifies the center of the neighborhood and its boundary and then randomly moving from the center point, put staying within the bounds, it a lat and long to aissgn the PersonAgent is starting location.

Step Function

The final piece is to initialize a step function. This function a Mesa primitive calls the RandomActiviationByType scheduler we set up and then iterates through each agent calling their step function.

The Model

We know have the pieces of our Model. A GIS layer of polygons that creates NeighbourhoodAgents from our GeoJSON file. A diverse population of GIS Point objects, with different infection, recovery and death risks. A model class that initializes these agents, a scheduler to call these agents, a GIS space and step function to execute the simulation

class GeoSIR(mesa.Model):
    """Model class for a simplistic infection model."""

    # Geographical parameters for desired map
    geojson_regions = "data/TorontoNeighbourhoods.geojson"
    unique_id = "HOODNUM"

    def __init__(
        self, pop_size=30, mobility_range=500, init_infection=0.2, exposure_dist=500, max_infection_risk=0.2,
        max_recovery_time=5
    ):
        super().__init__()
        self.schedule = mesa.time.RandomActivationByType(self)
        self.space = mg.GeoSpace(warn_crs_conversion=False)
        
        # SIR model parameters
        self.pop_size = pop_size
        self.mobility_range = mobility_range
        self.initial_infection = init_infection
        self.exposure_distance = exposure_dist
        self.infection_risk = max_infection_risk
        self.recovery_rate = max_recovery_time

        # Set up the Neighbourhood patches for every region in file
        ac = mg.AgentCreator(NeighbourhoodAgent, model=self)
        neighbourhood_agents = ac.from_file(
            self.geojson_regions, unique_id=self.unique_id
        )
        
        #Add neighbourhood agents to space
        self.space.add_agents(neighbourhood_agents)
        
        #Add neighbourhood agents to scheduler 
        for agent in neighbourhood_agents:
            self.schedule.add(agent)
            
  
        # Generate random location, add agent to grid and scheduler
        for i in range(pop_size):
            #assess if they are infected
            if self.random.random() < self.initial_infection: 
                agent_type = "infected"
            else: 
                agent_type = "susceptible"
            #determine movement range
            mobility_range = self.random.randint(0,self.mobility_range)            
            #determine agent recovery rate
            recover = self.random.randint(1,self.recovery_rate)
            #determine agents infection risk
            infection_risk = self.random.uniform(0,self.infection_risk)
            #determine agent death probability 
            death_risk= self.random.random()

            # Generate PersonAgent population
            unique_person = mg.AgentCreator(
            PersonAgent,
            model=self,
            crs=self.space.crs,
            agent_kwargs={"agent_type": agent_type, 
             "mobility_range":mobility_range,
             "recovery_rate":recover,
             "infection_risk": infection_risk,
             "death_risk": death_risk
             }
            )
            
            
            x_home, y_home = self.find_home(neighbourhood_agents)
            
            this_person = unique_person.create_agent(
                Point(x_home, y_home), "P" + str(i), 
            )
            self.space.add_agents(this_person)
            self.schedule.add(this_person)
            
    
    def find_home(self, neighbourhood_agents): 
        """ Find start location of agent """

        #identify location
        this_neighbourhood = self.random.randint(
            0, len(neighbourhood_agents) - 1
        )  # Region where agent starts
        center_x, center_y = neighbourhood_agents[
            this_neighbourhood
        ].geometry.centroid.coords.xy
        this_bounds = neighbourhood_agents[this_neighbourhood].geometry.bounds
        spread_x = int(
            this_bounds[2] - this_bounds[0]
        )  # Heuristic for agent spread in region
        spread_y = int(this_bounds[3] - this_bounds[1])
        this_x = center_x[0] + self.random.randint(0, spread_x) - spread_x / 2
        this_y = center_y[0] + self.random.randint(0, spread_y) - spread_y / 2

        return this_x, this_y

        
    def step(self):
        """Run one step of the model."""
        self.schedule.step()

Run The Base Model#

#explanatory

This cell is fairly simple

1 - We instantiate the SIR model by call the class name “GeoSIR” into the object model.

2 - Then we call the step function to see if it prints out the Agent IDs, infection status, death_risk, and recovery rate as called in the PersonAgent class.

You can also see all the person agents are called and then the neighbourhood agents. This will become important later as we want to update the neighbourhood status later based on its PersonAgent status.

If you are curious about the numbers for the neighbourhood agents, you can open up the GeoJSON in the data folder and see that each neighborhood gets a unique id identified by HOODNUM to ensure this number does not cause a conflict with our agent numbers, we add a “P” to their ID.

model = GeoSIR()
model.step()

Part 2 Add Agent and Model Complexity#

Increase PersonAgent Complexity#

In this section we add behaviors to the PersonAgent to build the necessary SIR dynamics.

To create the SIR dynamics we need the agents move, determine if they have been exposed and if they have process the probability of them being infected and possibly dying.

To do this we will update our step function. The step function logic uses the agent’s atype to determine what actions to process

Part 1

If the PersonAgent atype is susceptible, then we need to identify all PersonAgent’s neighbors within the exposure distance. To do this, we will use Mesa-Geo’s get_neighbors_within_distance function which takes 2 parameters, the agent, and a distance, which in this case is the model parameter for exposure distance in meters. This creates a list of PersonAgents within that distance.

The get_neighbors_within_distance function has two keyword arguments center and relation. center takes True or False on whether to include the center, it is set to False and measures as a buffer around the agent’s geometry. If True it measures from the Center of the point. relation is defaulted to intersects but can take any common spatial relationship, such as contains, within, touches, crosses

The step function then iterates through the list of neighbors to see if any agents are infected. If so it does a probabilistic comparison of a random float compared to the agents infection risk and if True the agent becomes infected and the iteration ends.

Part 2

If the agent atype is infected, then the step function does comparisons. First, it sees how many steps the agent has been infected. To track this the PersonAgent got a new attribute counter which is steps_infected. If the steps are greater than or equal to their recovery rate, the agent is recovered, if not then the function does a probabilistic comparison with the agents death risk to see if the agent dies. If neither of these things happen the steps_infected increases by one.

Part 3

The next part is if the agent atype is not dead then the agent moves. For this we randomly get an integer for the x any (lat and long) between their negative mobility_range and positive mobility range. We pass these two integers into the helper function move_point and then update the agents geometry with this new point.

Finally, we update the counts of agent types.

class PersonAgent(mg.GeoAgent):
    """Person Agent."""

    def __init__(
        self,
        unique_id,
        model,
        geometry,
        crs,
        agent_type,
        mobility_range,
        infection_risk,
        recovery_rate,
        death_risk
    ):
        super().__init__(unique_id, model, geometry, crs)
        # Agent attributes
        self.atype = agent_type
        self.mobility_range = mobility_range
        self.infection_risk=infection_risk,
        self.recovery_rate = recovery_rate
        self.death_risk = death_risk
        self.steps_infected=0
        self.steps_recovered = 0

    def __repr__(self):
        return "Person " + str(self.unique_id)

    #Helper function for moving agent
    def move_point(self, dx, dy):
        """
        Move a point by creating a new one
        :param dx:  Distance to move in x-axis
        :param dy:  Distance to move in y-axis
        """
        return Point(self.geometry.x + dx, self.geometry.y + dy)
    
    
    def step(self): 

        #Part 1 - find neighbors based on infection distance
        if self.atype == "susceptible":
            neighbors = self.model.space.get_neighbors_within_distance(
                self, self.model.exposure_distance
            )
            for neighbor in neighbors:
                if (
                    neighbor.atype == "infected"
                    and self.random.random() < self.model.infection_risk
                ):
                    self.atype = "infected"
                    break #stop process if agent becomes infected

        #Part -2 If infected, check if agent recovers or agent dies
        elif self.atype == "infected":
            if self.steps_infected >= self.recovery_rate:
                self.atype = "recovered"
                self.steps_infected = 0
            elif self.random.random() < self.death_risk:
                self.atype = "dead"
            else:
                self.steps_infected += 1

        elif self.atype == "recovered":
            self.steps_recovered+=1
            if self.steps_recovered >=2: 
                self.atype= "susceptible"
                self.steps_recovered = 0
        
        #Part 3 - If not dead, move
        if self.atype != "dead":
            move_x = self.random.randint(-self.mobility_range, self.mobility_range)
            move_y = self.random.randint(-self.mobility_range, self.mobility_range)
            self.geometry = self.move_point(move_x, move_y)  # Reassign geometry

        self.model.counts[self.atype] += 1  # Count agent type

Increase NeighbourhoodAgent Complexity#

For the NeighbourhoodAgent we want to change their color based on the number of infected PersonAgents in their neighbourhood.

To do this we will create a helper function called color_hotspot. We will then use mesa-geo’s get_intersecting_agents function. We will then iterate through that list to get the agents with atype infected if the list is longer than our hotspot_threshold equal to 1 (so if two agents in the neighborhood are infected) then the atype will change to hotspot.

We then update our model counts.

class NeighbourhoodAgent(mg.GeoAgent):
    """Neighbourhood agent. Changes color according to number of infected inside it."""

    def __init__(
        self, unique_id, model, geometry, crs, agent_type="safe", hotspot_threshold=1
    ):
        super().__init__(unique_id, model, geometry, crs)
        self.atype = agent_type
        self.hotspot_threshold = (
            hotspot_threshold  # When a neighborhood is considered a hot-spot
        )

    def __repr__(self):
        return "Neighbourhood " + str(self.unique_id)
        
    def color_hotspot(self):
        # Decide if this region agent is a hot-spot
        # (if more than threshold person agents are infected)
        neighbors = self.model.space.get_intersecting_agents(self)
        infected_neighbors = [
            neighbor for neighbor in neighbors if neighbor.atype == "infected"
        ]
        if len(infected_neighbors) > self.hotspot_threshold:
            self.atype = "hotspot"
        else:
            self.atype = "safe"
    
    def step(self):
        """Advance agent one step."""
        self.color_hotspot()
        self.model.counts[self.atype] += 1  # Count agent type

Increase model complexity#

For this section will add data collection where we collect the status of the PersonAgents and the NeighbourhoodAgents but counting the different atypes.

As we run our SIR model, we want to ensure we are collecting information about the status of the disease.

To do this we will create helper functions that get this information. In this case we will put them in a separate cell, but depending on the developers preference they could also put them in the model class or collect the information in a handful of other ways.

In this case, we set up an attribute in the model called counts and these functions just get the total number from Mesa’s data collector of each of our statuses.

# Functions needed for datacollector
def get_infected_count(model):
    return model.counts["infected"]


def get_susceptible_count(model):
    return model.counts["susceptible"]


def get_recovered_count(model):
    return model.counts["recovered"]


def get_dead_count(model):
    return model.counts["dead"]

def get_hotspot_count(model): 
    return model.counts["hotspot"]

def get_safe_count(model): 
    return model.counts["safe"]

Now to finish the model so we can add the interface we add datacollection and a stop condition. As these updates are interspersed throughout the class. The comment #added is used to make the changes easier to identify.

First, we add an attribute called self.counts which will track our the agent types (e.g. infected). We will initialize it as None. We then initialize the counts in our next line self.reset_counts(). This helper function located directly above the step function, resets the counts of each type of agent so it is always based on the current situation in the Model.

We are then going to add the attribute self.running so we can input the stop condition. Next we set our our data collector that call our functions from the previous cell which collects our agent types

With these added we can now call self.reset_counts and self.datacollector.collect in our step function so it collect our agent states each step.

Finally we add a stop condition. If no PersonAgent is infected the pandemic is over and we stop the model.

class GeoSIR(mesa.Model):
    """Model class for a simplistic infection model."""

    # Geographical parameters for desired map
    geojson_regions = "data/TorontoNeighbourhoods.geojson"
    unique_id = "HOODNUM"

    def __init__(
        self, pop_size=30, mobility_range=500, init_infection=0.2, exposure_dist=500, max_infection_risk=0.2,
        max_recovery_time=5
    ):
        super().__init__()
        #Scheduler
        self.schedule = mesa.time.RandomActivationByType(self)
        #Space
        self.space = mg.GeoSpace(warn_crs_conversion=False)
        # Data Collection
        self.counts = None #added
        self.reset_counts() #added
        
        # SIR model parameters
        self.pop_size = pop_size
        self.mobility_range = mobility_range
        self.initial_infection = init_infection
        self.exposure_distance = exposure_dist
        self.infection_risk = max_infection_risk
        self.recovery_rate = max_recovery_time
        self.running = True #added
        #added
        self.datacollector = mesa.DataCollector(
            {
                "infected": get_infected_count,
                "susceptible": get_susceptible_count,
                "recovered": get_recovered_count,
                "dead": get_dead_count,
                "safe": get_safe_count, 
                "hotspot": get_hotspot_count
            }
        )
                
        # Set up the Neighbourhood patches for every region in file
        ac = mg.AgentCreator(NeighbourhoodAgent, model=self)
        neighbourhood_agents = ac.from_file(
            self.geojson_regions, unique_id=self.unique_id
        )
        
        #Add neighbourhood agents to space
        self.space.add_agents(neighbourhood_agents)
        
        #Add neighbourhood agents to scheduler 
        for agent in neighbourhood_agents:
            self.schedule.add(agent)
            
  
        # Generate random location, add agent to grid and scheduler
        for i in range(pop_size):
            #assess if they are infected
            if self.random.random() < self.initial_infection: 
                agent_type = "infected"
            else: 
                agent_type = "susceptible"
            #determine movement range
            mobility_range = self.random.randint(0,self.mobility_range)            
            #determine agent recovery rate
            recover = self.random.randint(1,self.recovery_rate)
            #determine agents infection risk
            infection_risk = self.random.uniform(0,self.infection_risk)
            #determine agent death probability 
            death_risk= self.random.uniform(0,0.05)

            # Generate PersonAgent population
            unique_person = mg.AgentCreator(
            PersonAgent,
            model=self,
            crs=self.space.crs,
            agent_kwargs={"agent_type": agent_type, 
             "mobility_range":mobility_range,
             "recovery_rate":recover,
             "infection_risk": infection_risk,
             "death_risk": death_risk
             }
            )
            
            
            x_home, y_home = self.find_home(neighbourhood_agents)
            
            this_person = unique_person.create_agent(
                Point(x_home, y_home), "P" + str(i), 
            )
            self.space.add_agents(this_person)
            self.schedule.add(this_person)
            
    
    def find_home(self, neighbourhood_agents): 
        """ Find start location of agent """

        #identify location
        this_neighbourhood = self.random.randint(
            0, len(neighbourhood_agents) - 1
        )  # Region where agent starts
        center_x, center_y = neighbourhood_agents[
            this_neighbourhood
        ].geometry.centroid.coords.xy
        this_bounds = neighbourhood_agents[this_neighbourhood].geometry.bounds
        spread_x = int(
            this_bounds[2] - this_bounds[0]
        )  # Heuristic for agent spread in region
        spread_y = int(this_bounds[3] - this_bounds[1])
        this_x = center_x[0] + self.random.randint(0, spread_x) - spread_x / 2
        this_y = center_y[0] + self.random.randint(0, spread_y) - spread_y / 2

        return this_x, this_y
    
    #added
    def reset_counts(self):
        self.counts = {
            "susceptible": 0,
            "infected": 0,
            "recovered": 0,
            "dead": 0,
            "safe": 0,
            "hotspot": 0,
            }
    
    
    def step(self):
        """Run one step of the model."""
        
        self.reset_counts() #added
        self.schedule.step()
        self.datacollector.collect(self) #added

        # Run until no one is infected
        if self.counts["infected"] == 0 :
            self.running = False

To test our code we will run the model through 5 steps and then call the model dataframe via data collector with get_model_vars_dataframe(). This will show a Pandas DataFrame.

model = GeoSIR()
for i in range(5): 
    model.step()

model.datacollector.get_model_vars_dataframe()

Part 3 - Add Interface#

Adding the interface requires three steps:

  1. Define the agent portrayal

  2. Set the sliders for the model parameters

  3. Call the model through the Mesa-Geo visualization model

Visualizing agents is done through a function that is is passed in as a parameter. By default agents they are Point geometries are rendered as circles. However, Mesa uses ipyleaflet Users can pass through any Point geometry for their Agent (i.e. Marker, Circle, Icon, AwesomeIcon). To show this we will use different colors for the PersonAgent base don infection status and if they die, we will use the Font Awesome Icons and represent them with an x, in the traditional ipyleaflet marker.

We will also change the color of the NeighbourhoodAgent based whether or not it is a hotspot

Next we will build Sliders for each of our input parameters. These use the Solara’s input approach. This is stored in a dictionary of dictionaries that is then passed through in the model instantiation.

If you want the model to fill the entire screen you can hit the expand button in the upper right.

def SIR_draw(agent):
    """
    Portrayal Method for canvas
    """
    portrayal = {}
    if isinstance(agent, PersonAgent): 
        if agent.atype == "susceptible":
            portrayal["color"] = "Green"
        elif agent.atype == "infected":
            portrayal["color"] = "Red"
        elif agent.atype == "recovered":
            portrayal["color"] = "Blue"
        else: 
            portrayal["marker_type"] = "AwesomeIcon"
            portrayal["name"] = "times"
            portrayal["icon_properties"] = {
                "marker_color": 'black',
                "icon_color":'white'}
            
    if isinstance(agent, NeighbourhoodAgent):
        if agent.atype == "hotspot":
            portrayal["color"] = "Red"
        else: 
            portrayal["color"] = "Green"
    
    return portrayal


model_params = {
    "pop_size": {
        "type": "SliderInt",
        "value": 80,
        "label": "Population Size",
        "min": 0,
        "max": 100, 
        "step": 1,
    },
     "mobility_range": {
        "type": "SliderInt",
        "value": 500,
        "label": "Max Possible Agent Movement",
        "min": 100,
        "max": 1000, 
        "step": 50,
     },
    "init_infection": {
        "type": "SliderFloat",
        "value": 0.4,
        "label": "Initial Infection",
        "min": 0.0,
        "max": 1.0,
        "step": 0.1,
    },
    "exposure_dist": {
        "type": "SliderInt",
        "value": 800,
        "label": "Exposure Distance",
        "min": 100,
        "max": 1000, 
        "step": 50,
    },
    "max_infection_risk": {
        "type": "SliderFloat",
        "value": 0.7,
        "label": "Maximum Infection Risk",
        "min": 0.0,
        "max": 1.0,
        "step": 0.1
    },
     "max_recovery_time": {
      "type": "SliderInt",
      "value": 7,
        "label": "Maximum Number of Steps to Recover",
        "min": 1,
        "max": 10, 
        "step": 1,   
     }}

To create the model with the interface we use Mesa’s GeoJupyterViz module. First we pass in the model class and next the parameters. We then switch to key word arguments. First measures, in this case of list of lists, where the first list will be a chart of the PersonAgent statuses and the second chart will be the NeighbourhoodAgent statuses. We also pass in a name, our agent portrayal function a zoom level and in this case set the scroll wheel zoom to false.

page = mgv.GeoJupyterViz(
    GeoSIR,
    model_params,
    measures= [["infected", "susceptible", "recovered", "dead"], ["safe", "hotspot"]],
    name="GeoSIR",
    agent_portrayal=SIR_draw,
    zoom=12,
    scroll_wheel_zoom=False
)
# This is required to render the visualization in the Jupyter notebook
page