-
Notifications
You must be signed in to change notification settings - Fork 165
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
base: main
Are you sure you want to change the base?
Cholera Voronoi #118
Changes from all commits
ce64001
ccfe6d6
54a8c91
02729b1
c83d8ad
b4d0c60
9f6bbdb
3107d6d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
|
||
To run the model interactively, run ``mesa runserver`` in this directory. e.g. | ||
|
||
``` | ||
$ mesa runserver | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Documentation is outdated. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is for the old visualization, whereas it should have bin |
||
``` | ||
|
||
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/) |
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 |
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 = [ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't this be called |
||
(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() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
matplotlib | ||
mesa | ||
numpy | ||
solara |
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"]], | ||
) |
There was a problem hiding this comment.
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