Skip to content
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
80 changes: 80 additions & 0 deletions examples/pathogen_outbreak/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Pathogen Outbreak & Quarantine Compliance Model

A fast-spreading pathogen outbreak model with no exposed/latency period
inspired loosely from the game Plague Inc, though the goal here is containment
rather than world domination. The model simulates how quarantine
compliance rates at the population level affect outbreak dynamics,
analyzing changes in infection, death and immunity rates.

**It showcases:**

- **Two-threshold quarantine system** - quarantine triggers when infected
count exceeds an upper threshold and lifts only when it drops below a
lower one, preventing quarantine from lifting the moment a single agent
recovers
- **Compliance rate** - a configurable fraction of citizens actually follow
quarantine orders, attempting to simulate real-world partial adherence. The rest
(shown as squares) keep moving freely by not following quarantine instructions.
- **Flee behaviour** — compliant healthy agents moves away from quarantined zones during lockdown.
- **Infected agents freeze** — simulating a government isolating/quarantining an infected
zone. Non-compliant agents ignore this entirely.
- **Emergence of different outcomes** — combination of different configurations produce dramatically different outbreak curves.

## How It Works

1. **Initialization** — citizens are placed randomly on a MultiGrid.
A configurable number are set initially as infected.
2. **Disease Spread** — each step, healthy agents check their neighbours including diagonals.
If any are infected, there is a configurable chance of transmission.
No latency period — infection is immediate on contact,chance of getting an infection is 60%.
3. **Quarantine System** — the model monitors total infected count each step.
When it exceeds the upper threshold, quarantine activates. It only lifts
when infected drops below the lower threshold.
4. **Agent Behaviour during Quarantine:**
- Compliant agents(circles) - use Manhattan distance
to flee away from all infected agents.
- Non-compliant agents(squares) — move randomly, ignoring quarantine
- Infected agents - compliant ones, freeze in place, simulating isolation or a lockdowned zone
5. **Recovery** — after 10 steps of infection, agents recover to full
immunity or die with the probability of 10%. Dead agents remain
on the grid as red circles/squares(this is an intentional mechanic) as a visual indicator to assess how compliance affects the compliant as well as non-compliant groups.

## Interesting Cases to Observe

| Scenario | Parameters | What to observe |
|----------|-----------|-----------------|
| No compliance | `compliance=0.0` | Unconstrained outbreak, maximum spread |
| Full compliance | `compliance=1.0` | Quarantine collapses the outbreak |
| Partial compliance | `compliance=0.5` | Realistic middle ground |
| Late quarantine | `quarantine_threshold_strt=60` | Triggers too late to matter |
| Dense population | `n=240, width=25` | Flee behaviour is constrained by crowding and even quarantine doesn't help |
| Sparse population| `n=70, width=25` | May stop the outbreak almost immediately or sometimes quarantine doesn't trigger this causes slow wide spread infection|

The model proposed here is simple and often can provide some quite unexpected outcomes in certain configurations. It is recommended to play around with the model parameters as outcomes can be dramatic even with small changes.

| Compliance = 0.0 | Compliance = 1.0 |
|-----------------|-----------------|
| ![no compliance](images/no_compliance.png) | ![full compliance](images/full_compliance.png) |

| Dense Population (n=240, width=25) | Sparse Population (n=80, width=25) |
|-----------------------------------|-----------------------------------|
| ![dense](images/dense_population.png) | ![sparse](images/sparse_population.png) |

## Usage
```
pip install -r requirements.txt
solara run app.py
```

## Default Parameters

| Parameter | Description | Default |
|-----------|-------------|---------|
| `n` | Number of citizens | 100 |
| `infn` | Initially infected | 5 |
| `width` / `height` | Grid dimensions | 20 x 20 |
| `spread_chance` | Transmission probability on contact | 0.6 |
| `death_chance` | probability of death after infection(only once) | 0.1 |
| `compliance` | Fraction of citizens who follow quarantine | 0.25 |
| `quarantine_threshold_strt` | Infected count that triggers quarantine | 25 |
| `quarantine_threshold_stp` | Infected count that lifts quarantine | 5 |
85 changes: 85 additions & 0 deletions examples/pathogen_outbreak/agents.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import random

from mesa import Agent


class Citizen(Agent):
def __init__(self, model):
super().__init__(model)

self.state = "healthy"
self.stage = 0
self.compliant = random.random() < self.model.compliance_rate

def step(self):
if self.model.quarantine_status and self.compliant:
self.quarantine()
else:
self.move()

if self.state == "infected":
self.stage += 1
if self.stage > 9:
chance_of_death = random.random()

if chance_of_death < 0.1:
self.state = "dead"
# self.remove()

else:
self.state = "immune"

cell_state = "healthy"

cell_neigh = self.model.grid.get_neighbors(
self.pos, moore=True, include_center=False
)

for i in cell_neigh:
if i.state == "infected":
cell_state = "infected"

if self.state == "healthy" and cell_state == "infected":
chances = random.random()
if chances > 0.40:
self.state = "infected"

def quarantine(self):
if self.state != "dead":
if self.state == "infected":
return
else:
infected_pos = []
for i in self.model.agents:
if i.state == "infected":
infected_pos.append(i.pos)

possible_ops = self.model.grid.get_neighborhood(
self.pos, moore=False, include_center=False
)
emptys = []
for i in possible_ops:
if self.model.grid.is_cell_empty(i):
emptys.append(i)

if len(emptys) > 0:
best_cell = max(
emptys,
key=lambda pos: min(
abs(pos[0] - i[0]) + abs(pos[1] - i[1])
for i in infected_pos
),
)
self.model.grid.move_agent(self, best_cell)

def move(self):
if self.state != "dead":
possible_ops = self.model.grid.get_neighborhood(
self.pos, moore=False, include_center=False
)
emptys = []
for i in possible_ops:
if self.model.grid.is_cell_empty(i):
emptys.append(i)
if len(emptys) > 0:
self.model.grid.move_agent(self, self.random.choice(emptys))
65 changes: 65 additions & 0 deletions examples/pathogen_outbreak/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from mesa.visualization import SolaraViz, make_plot_component, make_space_component
from model import PathogenModel


def make_agent(agent):
color_map = {
"healthy": "tab:green",
"infected": "tab:orange",
"immune": "tab:blue",
"dead": "tab:red",
}
return {
"color": color_map.get(agent.state, "black"),
"size": 20,
"marker": "s" if not agent.compliant else "o",
}


model_params = {
"compliance": {
"type": "SliderFloat",
"value": 0.7,
"label": "Quarantine Compliance Rate",
"min": 0.0,
"max": 1.0,
"step": 0.1,
},
"n": {
"type": "SliderInt",
"value": 100,
"label": "Citizens",
"min": 10,
"max": 300,
"step": 10,
},
"infn": {
"type": "SliderInt",
"value": 5,
"label": "Infected",
"min": 1,
"max": 20,
"step": 1,
},
"width": {"type": "SliderInt", "value": 20, "label": "Width", "min": 10, "max": 50},
"height": {
"type": "SliderInt",
"value": 20,
"label": "Height",
"min": 10,
"max": 50,
},
}

model = PathogenModel(n=100, infn=5, width=25, height=25)

renderer = make_space_component(make_agent)

graph_ot = make_plot_component(["healthy", "infected", "immune", "dead", "quarantine"])

page = SolaraViz(
model,
components=[renderer, graph_ot],
model_params=model_params,
name="Compliance/Quarantine during Outbreak Model",
)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
74 changes: 74 additions & 0 deletions examples/pathogen_outbreak/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import random

from mesa import DataCollector, Model
from mesa.space import MultiGrid

from .agents import Citizen


def c_healthy(model):
return sum(1 for i in model.agents if i.state == "healthy")


def c_infected(model):
return sum(1 for i in model.agents if i.state == "infected")


def c_immune(model):
return sum(1 for i in model.agents if i.state == "immune")


def c_dead(model):
return sum(1 for i in model.agents if i.state == "dead")


class PathogenModel(Model):
def __init__(
self,
n=100,
infn=5,
width=20,
height=20,
quarantine_threshold_strt=25,
quarantine_threshold_stp=5,
compliance=0.25,
):
super().__init__()
self.compliance_rate = compliance
self.quarantine_thresh_up = quarantine_threshold_strt
self.quarantine_thresh_lw = quarantine_threshold_stp
self.infected_count = 0
self.quarantine_status = False

self.grid = MultiGrid(width, height, False)

self.datacollector = DataCollector(
model_reporters={
"healthy": c_healthy,
"immune": c_immune,
"infected": c_infected,
"dead": c_dead,
"quarantine": lambda i: int(i.quarantine_status),
}
)

for _ in range(n):
agent = Citizen(self)
self.grid.place_agent(
agent, (random.randrange(width), random.randrange(height))
)

for i in range(infn):
self.agents[i].state = "infected"

def step(self):
self.infected_count = c_infected(self)

if self.infected_count > self.quarantine_thresh_up:
self.quarantine_status = True
elif self.infected_count < self.quarantine_thresh_lw:
self.quarantine_status = False

self.datacollector.collect(self)

self.agents.do("step")
3 changes: 3 additions & 0 deletions examples/pathogen_outbreak/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mesa>=3.5
solara
matplotlib