Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cholera Voronoi #118

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions examples/cholera_voronoi/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Disease dynamics on Voronoi Grid

This folder contains a implementation of Cholera spread analyzed by John Snow at London Soho district during the 19th century. The physicist discovered contaminated water from Broad Street Pump was the source of disease by drawing a Voronoi diagram around pumps and mapping cholera cases.

The model has two agents: people and pumps. Pumps can infect people and neighbor pumps. People start as susceptible, can be infected by pumps and recover or die, according to a simple SIR model. Each cell has only one pump and is connected to neighbor cells according to Voronoi's diagram. The model aims to investigate how fast actions oriented by Voronoi diagrams can prevent disease spread.

## How to Run
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for writing a Readme! Don’t forget to update this section


To run the model interactively, run ``mesa runserver`` in this directory. e.g.

```
$ mesa runserver
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documentation is outdated.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is for the old visualization, whereas it should have bin solara run run.py.

```

Then open your browser to [http://127.0.0.1:8521/](http://127.0.0.1:8521/) and press ``run``.

## Files

* ``cholera_voronoi/agents.py``: Defines Pump and Person agents.
* ``cholera_voronoi/model.py``: Defines the model itself, initialized with John Snow study about Cholera Spread pump locations.
* ``cholera_voronoi/server.py``: Defines an interactive visualization.
* ``run.py``: Launches the visualization

## Further reading
- [R Package for Analyzing John Snow's 1854 Cholera Map ](https://github.com/lindbrook/cholera)
- [Why this pattern shows up everywhere in nature | Voronoi Cell Pattern](https://www.youtube.com/watch?v=GafRRl5XRPM&t=183s)
- [John Snow, Cholera, the Broad Street Pump; Waterborne Diseases Then and Now](https://www.ncbi.nlm.nih.gov/pmc/articles/PMC7150208/)
86 changes: 86 additions & 0 deletions examples/cholera_voronoi/cholera_voronoi/agents.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
from mesa.experimental.cell_space import CellAgent

SUSCEPTIBLE = 0
INFECTIOUS = 1
REMOVED = 2


class Person(CellAgent):
def __init__(self, unique_id, model, mortality_chance, recovery_chance):
super().__init__(unique_id, model)
self.state = SUSCEPTIBLE
self.mortality_chance = mortality_chance
self.recovery_chance = recovery_chance

def step(self):
if self.state == REMOVED:
return

if (
self.state == INFECTIOUS
and self.model.random.random() < self.recovery_chance
):
self.state = SUSCEPTIBLE
self.model.infectious -= 1
self.model.susceptible += 1

if (
self.state == INFECTIOUS
and self.model.random.random() < self.mortality_chance
):
self.state = REMOVED
self.model.infectious -= 1
self.model.removed += 1


class Pump(CellAgent):
def __init__(
self,
unique_id,
model,
contaminated,
pumps_person_contamination_chance,
pumps_neighbor_contamination_chance,
cases_ratio_to_fix_pump,
):
super().__init__(unique_id, model)
self.state = contaminated
self.pumps_person_contamination_chance = pumps_person_contamination_chance
self.pumps_neighbor_contamination_chance = pumps_neighbor_contamination_chance
self.cases_ratio_to_fix_pump = cases_ratio_to_fix_pump

def step(self):
if self.state is INFECTIOUS:
# Infect people in the cell
people = [
obj
for obj in self.cell.agents
if isinstance(obj, Person) and obj.state is not REMOVED
]
for person in people:
if (
person.state is SUSCEPTIBLE
and self.model.random.random()
< self.pumps_person_contamination_chance
):
person.state = INFECTIOUS
self.model.susceptible -= 1
self.model.infectious += 1

# Infect neighbor cells
if self.model.random.random() < self.pumps_neighbor_contamination_chance:
neighbor_cell = self.random.choice(list(self.cell._connections))
neighbor_pump = neighbor_cell.agents[0]
if neighbor_pump.state is SUSCEPTIBLE:
neighbor_pump.state = INFECTIOUS
self.model.infected_pumps += 1

# If cases in total is too high, fix pump
cases = sum(1 for a in people if a.state is INFECTIOUS)
cases_ratio = cases / (
self.model.susceptible + self.model.infectious + 1e-1
)
self.cell.properties["cases_ratio"] = cases_ratio
if cases_ratio > self.cases_ratio_to_fix_pump:
self.state = SUSCEPTIBLE
self.model.infected_pumps -= 1
90 changes: 90 additions & 0 deletions examples/cholera_voronoi/cholera_voronoi/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
from collections.abc import Sequence

import mesa

from .agents import Person, Pump

SUSCEPTIBLE = 0
INFECTIOUS = 1
REMOVED = 2

cell_population = [400] * 8

points = [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be called pump_locations for clarity?

(9.909976449792431, 11.542846828417543),
(0.40972334441912234, 14.266853186123692),
(0.0, 20.0),
(20.0, 5.111991897435429),
(12.566609556906684, 1.57960921165571),
(5.232766132031835, 0.0),
(10.196872670067446, 4.1842030053700165),
(16.553612933660478, 4.449943091510793),
]

is_pump_contaminated = [True, False, False, False, False, False, False, False]


class Cholera(mesa.Model):
def __init__(
self,
cell_population: Sequence[int] = cell_population,
pumps_location: Sequence[Sequence[float]] = points,
is_pump_contaminated: Sequence[bool] = is_pump_contaminated,
cases_ratio_to_fix_pump: float = 9e-1,
pumps_neighbor_contamination_chance: float = 2e-1,
pumps_person_contamination_chance: float = 2e-1,
recovery_chance: float = 2e-1,
mortality_chance: float = 1e-1,
):
super().__init__()
self.susceptible = 0
for population in cell_population:
self.susceptible += population
self.infectious = 0
self.removed = 0

self.infected_pumps = 0
self.number_pumps = len(cell_population)

self.schedule = mesa.time.RandomActivation(self)

self.grid = mesa.experimental.cell_space.VoronoiGrid(
centroids_coordinates=pumps_location,
capacity=int(self.susceptible + 1),
random=self.random,
cell_coloring_property="cases_ratio",
)

for population, cell, contaminated in zip(
cell_population, list(self.grid.all_cells), is_pump_contaminated
):
pump_state = INFECTIOUS if contaminated else SUSCEPTIBLE
self.infected_pumps += pump_state
pump = Pump(
self.next_id(),
self,
pump_state,
pumps_person_contamination_chance,
pumps_neighbor_contamination_chance,
cases_ratio_to_fix_pump,
)
self.schedule.add(pump)
cell.add_agent(pump)
pump.move_to(cell)
for i in range(population):
person = Person(self.next_id(), self, mortality_chance, recovery_chance)
self.schedule.add(person)
cell.add_agent(person)
person.move_to(cell)

self.datacollector = mesa.DataCollector(
model_reporters={
"Susceptible": "susceptible",
"Infectious": "infectious",
"Removed": "removed",
}
)

def step(self):
self.datacollector.collect(self)
self.schedule.step()
4 changes: 4 additions & 0 deletions examples/cholera_voronoi/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
matplotlib
mesa
numpy
solara
81 changes: 81 additions & 0 deletions examples/cholera_voronoi/run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import solara
from cholera_voronoi.model import Cholera, Pump
from mesa.visualization import JupyterViz, Slider

SUSCEPTIBLE = 0
INFECTIOUS = 1
REMOVED = 2


def get_removed_people(model: Cholera):
"""
Display a text count of how many people were removed.
"""
return f"Number of removed people: {model.removed}"


def get_infectious_pumps(model: Cholera):
"""
Display infected/total pumps count.
"""
return f"Infected pumps: {model.infected_pumps}/{model.number_pumps}"


model_params = {
# "cases_ratio_to_fix_pump": {
# 'type': 'SliderFloat',
# 'label': "Ratio of cases in a neighborhood / total person in system to fix pump",
# 'value': 0.1,
# 'min': 0,
# 'max': 0.3,
# 'step': 0.001
# ),
"pumps_neighbor_contamination_chance": Slider(
label="Neighbor contamination ratio",
value=2e-1,
min=0,
max=1,
step=0.05,
),
"pumps_person_contamination_chance": Slider(
label="Person contamination ratio",
value=2e-1,
min=0,
max=1,
step=0.05,
),
"recovery_chance": Slider(
label="Recovery chance",
value=2e-1,
min=0,
max=1,
step=0.05,
),
"mortality_chance": Slider(
label="Mortality chance",
value=1e-1,
min=0,
max=1,
step=0.05,
),
}


def agent_portrayal(agent):
if isinstance(agent, Pump):
if agent.state == INFECTIOUS:
return {"size": 50, "color": "tab:orange"}
elif agent.state == SUSCEPTIBLE:
return {"size": 50, "color": "tab:blue"}
return {"size": 0, "color": "tab:blue"}


@solara.component
def Page():
JupyterViz(
Cholera,
model_params,
name="Cholera Model",
agent_portrayal=agent_portrayal,
measures=[["Susceptible", "Infectious", "Removed"]],
)
Loading