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)
*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.!pipinstall-U--premesa-geo--quiet !mkdir-pdata !wget-Pdatahttps://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.
fromshapely.geometryimportPointimportmesafrommesa.visualizationimportSolaraViz,make_plot_componentimportmesa_geoasmgfrommesa_geo.visualizationimportmake_geospace_component
Create the person agent class#
The person in this model represents one human being and we initialize each person agent with two key parts:
The agent attributes, such as recovery rate and death risk
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
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 describes what action the agent takes each step
classPersonAgent(mg.GeoAgent):"""Person Agent."""def__init__(self,model,geometry,crs,agent_type,mobility_range,infection_risk,recovery_rate,death_risk,):super().__init__(model,geometry,crs)# Agent attributesself.atype=agent_typeself.mobility_range=mobility_rangeself.infection_risk=infection_riskself.recovery_rate=recovery_rateself.death_risk=death_riskdef__repr__(self):returnf"Person {self.unique_id}"defstep(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.
The agent attributes, such as geometry and state of neighborhood
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:
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 describes what action the agent takes each step
classNeighbourhoodAgent(mg.GeoAgent):"""Neighbourhood agent. Changes color according to number of infected inside it."""def__init__(self,model,geometry,crs,agent_type="safe",hotspot_threshold=1):super().__init__(model,geometry,crs)self.atype=agent_typeself.hotspot_threshold=(hotspot_threshold# When a neighborhood is considered a hot-spot)def__repr__(self):returnf"Neighbourhood {self.HOODNUM}"defstep(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”) as attribute, and update the repr function to print out the neighbourhood 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.
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 is a Mesa primitive that 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 GIS space and step function to execute the simulation
classGeoSIR(mesa.Model):"""Model class for a simplistic infection model."""# Geographical parameters for desired mapgeojson_regions="data/TorontoNeighbourhoods.geojson"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.space=mg.GeoSpace(warn_crs_conversion=False)# SIR model parametersself.pop_size=pop_sizeself.mobility_range=mobility_rangeself.initial_infection=init_infectionself.exposure_distance=exposure_distself.infection_risk=max_infection_riskself.recovery_rate=max_recovery_time# Set up the Neighbourhood patches for every region in fileac=mg.AgentCreator(NeighbourhoodAgent,model=self)neighbourhood_agents=ac.from_file(self.geojson_regions)# Add neighbourhood agents to spaceself.space.add_agents(neighbourhood_agents)# Generate random location, add agent to gridforiinrange(pop_size):# assess if they are infectedifself.random.random()<self.initial_infection:agent_type="infected"else:agent_type="susceptible"# determine movement rangemobility_range=self.random.randint(0,self.mobility_range)# determine agent recovery raterecover=self.random.randint(1,self.recovery_rate)# determine agents infection riskinfection_risk=self.random.uniform(0,self.infection_risk)# determine agent death probabilitydeath_risk=self.random.random()# Generate PersonAgent populationunique_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))self.space.add_agents(this_person)deffind_home(self,neighbourhood_agents):"""Find start location of agent"""# identify locationthis_neighbourhood=self.random.randint(0,len(neighbourhood_agents)-1)# Region where agent startscenter_x,center_y=neighbourhood_agents[this_neighbourhood].geometry.centroid.coords.xythis_bounds=neighbourhood_agents[this_neighbourhood].geometry.boundsspread_x=int(this_bounds[2]-this_bounds[0])# Heuristic for agent spread in regionspread_y=int(this_bounds[3]-this_bounds[1])this_x=center_x[0]+self.random.randint(0,spread_x)-spread_x/2this_y=center_y[0]+self.random.randint(0,spread_y)-spread_y/2returnthis_x,this_ydefstep(self):"""Run one step of the model."""# Activate PersonAgents in random orderself.agents_by_type[PersonAgent].shuffle_do("step")# For NeighbourhoodAgents the order doesn't matter, since they update independently from each otherself.agents_by_type[NeighbourhoodAgent].do("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 HOODNUM
attribute assigned.
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 mobilityrange
. 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.
classPersonAgent(mg.GeoAgent):"""Person Agent."""def__init__(self,model,geometry,crs,agent_type,mobility_range,infection_risk,recovery_rate,death_risk,):super().__init__(model,geometry,crs)# Agent attributesself.atype=agent_typeself.mobility_range=mobility_rangeself.infection_risk=(infection_risk,)self.recovery_rate=recovery_rateself.death_risk=death_riskself.steps_infected=0self.steps_recovered=0def__repr__(self):returnf"Person {self.unique_id}"# Helper function for moving agentdefmove_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 """returnPoint(self.geometry.x+dx,self.geometry.y+dy)defstep(self):# Part 1 - find neighbors based on infection distanceifself.atype=="susceptible":neighbors=self.model.space.get_neighbors_within_distance(self,self.model.exposure_distance)forneighborinneighbors:if(neighbor.atype=="infected"andself.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 dieselifself.atype=="infected":ifself.steps_infected>=self.recovery_rate:self.atype="recovered"self.steps_infected=0elifself.random.random()<self.death_risk:self.atype="dead"else:self.steps_infected+=1elifself.atype=="recovered":self.steps_recovered+=1ifself.steps_recovered>=2:self.atype="susceptible"self.steps_recovered=0# Part 3 - If not dead, moveifself.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 geometryself.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.
classNeighbourhoodAgent(mg.GeoAgent):"""Neighbourhood agent. Changes color according to number of infected inside it."""def__init__(self,model,geometry,crs,agent_type="safe",hotspot_threshold=1):super().__init__(model,geometry,crs)self.atype=agent_typeself.hotspot_threshold=(hotspot_threshold# When a neighborhood is considered a hot-spot)def__repr__(self):returnf"Neighbourhood {self.unique_id}"defcolor_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=[neighborforneighborinneighborsifneighbor.atype=="infected"]iflen(infected_neighbors)>self.hotspot_threshold:self.atype="hotspot"else:self.atype="safe"defstep(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 datacollectordefget_infected_count(model):returnmodel.counts["infected"]defget_susceptible_count(model):returnmodel.counts["susceptible"]defget_recovered_count(model):returnmodel.counts["recovered"]defget_dead_count(model):returnmodel.counts["dead"]defget_hotspot_count(model):returnmodel.counts["hotspot"]defget_safe_count(model):returnmodel.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.
classGeoSIR(mesa.Model):"""Model class for a simplistic infection model."""# Geographical parameters for desired mapgeojson_regions="data/TorontoNeighbourhoods.geojson"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__()# Spaceself.space=mg.GeoSpace(warn_crs_conversion=False)# Data Collectionself.counts=None# addedself.reset_counts()# added# SIR model parametersself.pop_size=pop_sizeself.mobility_range=mobility_rangeself.initial_infection=init_infectionself.exposure_distance=exposure_distself.infection_risk=max_infection_riskself.recovery_rate=max_recovery_timeself.running=True# added# addedself.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 fileac=mg.AgentCreator(NeighbourhoodAgent,model=self)neighbourhood_agents=ac.from_file(self.geojson_regions)# Add neighbourhood agents to spaceself.space.add_agents(neighbourhood_agents)# Generate random location, add agent to gridforiinrange(pop_size):# assess if they are infectedifself.random.random()<self.initial_infection:agent_type="infected"else:agent_type="susceptible"# determine movement rangemobility_range=self.random.randint(0,self.mobility_range)# determine agent recovery raterecover=self.random.randint(1,self.recovery_rate)# determine agents infection riskinfection_risk=self.random.uniform(0,self.infection_risk)# determine agent death probabilitydeath_risk=self.random.uniform(0,0.05)# Generate PersonAgent populationunique_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))self.space.add_agents(this_person)deffind_home(self,neighbourhood_agents):"""Find start location of agent"""# identify locationthis_neighbourhood=self.random.randint(0,len(neighbourhood_agents)-1)# Region where agent startscenter_x,center_y=neighbourhood_agents[this_neighbourhood].geometry.centroid.coords.xythis_bounds=neighbourhood_agents[this_neighbourhood].geometry.boundsspread_x=int(this_bounds[2]-this_bounds[0])# Heuristic for agent spread in regionspread_y=int(this_bounds[3]-this_bounds[1])this_x=center_x[0]+self.random.randint(0,spread_x)-spread_x/2this_y=center_y[0]+self.random.randint(0,spread_y)-spread_y/2returnthis_x,this_y# addeddefreset_counts(self):self.counts={"susceptible":0,"infected":0,"recovered":0,"dead":0,"safe":0,"hotspot":0,}defstep(self):"""Run one step of the model."""self.reset_counts()# added# Activate PersonAgents in random orderself.agents_by_type[PersonAgent].shuffle_do("step")# For NeighbourhoodAgents the order doesn't matter, since they update independently from each otherself.agents_by_type[NeighbourhoodAgent].do("step")self.datacollector.collect(self)# added# Run until no one is infectedifself.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()foriinrange(5):model.step()model.datacollector.get_model_vars_dataframe()
Part 3 - Add Interface#
Adding the interface requires three steps:
Define the agent portrayal
Set the sliders for the model parameters
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.
defSIR_draw(agent):""" Portrayal Method for canvas """portrayal={}ifisinstance(agent,PersonAgent):ifagent.atype=="susceptible":portrayal["color"]="Green"elifagent.atype=="infected":portrayal["color"]="Red"elifagent.atype=="recovered":portrayal["color"]="Blue"else:portrayal["marker_type"]="AwesomeIcon"portrayal["name"]="times"portrayal["icon_properties"]={"marker_color":"black","icon_color":"white",}ifisinstance(agent,NeighbourhoodAgent):ifagent.atype=="hotspot":portrayal["color"]="Red"else:portrayal["color"]="Green"returnportrayalmodel_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.
model=GeoSIR()page=SolaraViz(model,name="GeoSIR",model_params=model_params,components=[make_geospace_component(SIR_draw,zoom=12,scroll_wheel_zoom=False),make_plot_component(["infected","susceptible","recovered","dead"]),make_plot_component(["safe","hotspot"]),],)# This is required to render the visualization in the Jupyter notebookpage