-
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 5 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,83 @@ | ||
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: | ||
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. I have a hunch this whole section could be simpler. Let me check tomorrow. |
||
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 | ||
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 agents be just removed from cell.agents instead of having a state “removed”? |
||
] | ||
for person in people: | ||
EwoutH marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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) | ||
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. Can you do len() instead of sum()? |
||
cases_ratio = cases / (self.model.susceptible + self.model.infectious) | ||
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,88 @@ | ||
from collections.abc import Sequence | ||
|
||
import mesa | ||
from agents import Person, Pump | ||
|
||
SUSCEPTIBLE = 0 | ||
INFECTIOUS = 1 | ||
REMOVED = 2 | ||
|
||
cell_population = [400] * 8 | ||
|
||
points = [ | ||
(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, | ||
) | ||
|
||
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,48 @@ | ||
{ | ||
"cells": [ | ||
{ | ||
"cell_type": "code", | ||
"execution_count": 1, | ||
"metadata": {}, | ||
"outputs": [], | ||
"source": [] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": null, | ||
"metadata": {}, | ||
"outputs": [], | ||
"source": [ | ||
"page" | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": null, | ||
"metadata": {}, | ||
"outputs": [], | ||
"source": [] | ||
} | ||
], | ||
"metadata": { | ||
"kernelspec": { | ||
"display_name": "venv", | ||
"language": "python", | ||
"name": "python3" | ||
}, | ||
"language_info": { | ||
"codemirror_mode": { | ||
"name": "ipython", | ||
"version": 3 | ||
}, | ||
"file_extension": ".py", | ||
"mimetype": "text/x-python", | ||
"name": "python", | ||
"nbconvert_exporter": "python", | ||
"pygments_lexer": "ipython3", | ||
"version": "3.12.3" | ||
} | ||
}, | ||
"nbformat": 4, | ||
"nbformat_minor": 2 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
import solara | ||
from mesa.visualization import JupyterViz | ||
from model import Cholera, Pump | ||
|
||
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_value': 0, | ||
# 'max_value': 0.3, | ||
# 'step': 0.001 | ||
# }, | ||
"pumps_neighbor_contamination_chance": { | ||
"type": "SliderFloat", | ||
"label": "Neighbor contamination ratio", | ||
"value": 2e-1, | ||
"min_value": 0, | ||
"max_value": 1, | ||
"step": 0.05, | ||
}, | ||
"pumps_person_contamination_chance": { | ||
"type": "SliderFloat", | ||
"label": "Person contamination ratio", | ||
"value": 2e-1, | ||
"min_value": 0, | ||
"max_value": 1, | ||
"step": 0.05, | ||
}, | ||
"recovery_chance": { | ||
"type": "SliderFloat", | ||
"label": "Recovery chance", | ||
"value": 2e-1, | ||
"min_value": 0, | ||
"max_value": 1, | ||
"step": 0.05, | ||
}, | ||
"mortality_chance": { | ||
"type": "SliderFloat", | ||
"label": "Mortality chance", | ||
"value": 1e-1, | ||
"min_value": 0, | ||
"max_value": 1, | ||
"step": 0.05, | ||
}, | ||
} | ||
|
||
|
||
def agent_portrayal(agent): | ||
if isinstance(agent, Pump): | ||
if agent.state == INFECTIOUS: | ||
return {"size": 200, "color": "tab:orange"} | ||
elif agent.state == SUSCEPTIBLE: | ||
return {"size": 200, "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