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
74 changes: 74 additions & 0 deletions gis/geo_pathogen/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Geographic Disease Spread Model

A fast-spreading pathogen outbreak geo-model with no exposed/latency period.
The model simulates how quarantine compliance rates at the population level affect outbreak dynamics in a real geo mapped environment,
analyzing changes in infection, death and immunity rates here the the dots represent not population directly but sets of highly populated areas.

**It showcases:**
- **Disease spread on a real geographic map**Disease spread using real geographic proximity(not grid cells) and agent movement is bounded within the Eurasian continent.
- **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
keep moving freely by not following quarantine instructions.
- **Geographic flee behavior** — 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(more or less realistically) on a geographic map using mesa-geo. A configurable number are set initially as infected.
2. **Disease Spread** — each step, healthy agents check their neighbours within a certain proximity for infected if its within the set infection radius, if there's an infection 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 - flee away from all infected agents.
- Non-compliant agents - 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(this is an intentional mechanic) as a visual indicator to assess how compliance affects the model.

## 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` | Flee behaviour is constrained by crowding and even quarantine doesn't help |
| Sparse population| `n=70` | 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) | Sparse Population (n=80) |
|-----------------------------------|-----------------------------------|
| ![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 |
| `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 |
84 changes: 84 additions & 0 deletions gis/geo_pathogen/agents.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import random

import mesa_geo as mg
from shapely.geometry import Point


class Citizen(mg.GeoAgent):
def __init__(self, model, geometry, crs):
super().__init__(model, geometry, crs)

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.space.get_neighbors_within_distance(
self, self.model.exposure
)

for i in cell_neigh:
if isinstance(i, Citizen) and 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" or self.state == "infected":
return
infected = [
a
for a in self.model.agents
if isinstance(a, Citizen) and a.state == "infected"
]
if not infected:
self.move()
return
nearest = min(infected, key=lambda a: self.geometry.distance(a.geometry))
dx = self.geometry.x - nearest.geometry.x
dy = self.geometry.y - nearest.geometry.y
dist = max((dx**2 + dy**2) ** 0.5, 1)
scale = self.model.mobility_range / dist
np = Point(self.geometry.x + dx * scale, self.geometry.y + dy * scale)
if self.model.asia_boundary.contains(np):
self.geometry = np

def move(self):
if self.state != "dead":
change_x = self.random.randint(
-self.model.mobility_range, self.model.mobility_range
)
change_y = self.random.randint(
-self.model.mobility_range, self.model.mobility_range
)
np = Point(self.geometry.x + change_x, self.geometry.y + change_y)
if self.model.asia_boundary.contains(np):
self.geometry = np


class CountryAgent(mg.GeoAgent):
def __init__(self, model, geometry, crs):
super().__init__(model, geometry, crs)
self.atype = "safe"
59 changes: 59 additions & 0 deletions gis/geo_pathogen/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from agents import Citizen
from mesa.visualization import SolaraViz, make_plot_component
from mesa_geo.visualization import make_geospace_component
from model import GeoModel


def citizen_draw(agent):
if not isinstance(agent, Citizen):
return {"color": "Gray", "fillOpacity": 0.1, "weight": 1}

if agent.state == "healthy":
return {"color": "Green", "radius": 3}
elif agent.state == "infected":
return {"color": "Red", "radius": 3}
elif agent.state == "immune":
return {"color": "Blue", "radius": 3}
elif agent.state == "dead":
return {"color": "Black", "radius": 3}


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,
},
}

model = GeoModel(n=100, infn=5)

renderer = make_geospace_component(citizen_draw)

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