diff --git a/examples/cholera_voronoi/README.md b/examples/cholera_voronoi/README.md new file mode 100644 index 00000000..e1c589b1 --- /dev/null +++ b/examples/cholera_voronoi/README.md @@ -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 +``` + +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/) \ No newline at end of file diff --git a/examples/cholera_voronoi/cholera_voronoi/agents.py b/examples/cholera_voronoi/cholera_voronoi/agents.py new file mode 100644 index 00000000..1c7e6536 --- /dev/null +++ b/examples/cholera_voronoi/cholera_voronoi/agents.py @@ -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 diff --git a/examples/cholera_voronoi/cholera_voronoi/model.py b/examples/cholera_voronoi/cholera_voronoi/model.py new file mode 100644 index 00000000..56e910fd --- /dev/null +++ b/examples/cholera_voronoi/cholera_voronoi/model.py @@ -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 = [ + (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() diff --git a/examples/cholera_voronoi/requirements.txt b/examples/cholera_voronoi/requirements.txt new file mode 100644 index 00000000..f98d4fb8 --- /dev/null +++ b/examples/cholera_voronoi/requirements.txt @@ -0,0 +1,4 @@ +matplotlib +mesa +numpy +solara \ No newline at end of file diff --git a/examples/cholera_voronoi/run.py b/examples/cholera_voronoi/run.py new file mode 100644 index 00000000..45e9bde2 --- /dev/null +++ b/examples/cholera_voronoi/run.py @@ -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"]], + )