diff --git a/gis/geo_pathogen/README.md b/gis/geo_pathogen/README.md new file mode 100644 index 000000000..d7d9d4c9a --- /dev/null +++ b/gis/geo_pathogen/README.md @@ -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 | \ No newline at end of file diff --git a/gis/geo_pathogen/agents.py b/gis/geo_pathogen/agents.py new file mode 100644 index 000000000..06b11ee2d --- /dev/null +++ b/gis/geo_pathogen/agents.py @@ -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" diff --git a/gis/geo_pathogen/app.py b/gis/geo_pathogen/app.py new file mode 100644 index 000000000..7df26eb9c --- /dev/null +++ b/gis/geo_pathogen/app.py @@ -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", +) diff --git a/gis/geo_pathogen/country_data/ne_110m_admin_0_countries.README.html b/gis/geo_pathogen/country_data/ne_110m_admin_0_countries.README.html new file mode 100644 index 000000000..d29bbe88c --- /dev/null +++ b/gis/geo_pathogen/country_data/ne_110m_admin_0_countries.README.html @@ -0,0 +1,547 @@ + + + + + + +Natural Earth » Blog Archive » Admin 0 – Countries - Free vector and raster map data at 1:10m, 1:50m, and 1:110m scales + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+ + +
+

Admin 0 – Countries

+ +
+
+
countries_thumb
+
There are 258 countries in the world. Greenland as separate from Denmark. Most users will want this file instead of sovereign states, though some users will want map units instead when needing to distinguish overseas regions of France.
+
+

Natural Earth shows de facto boundaries by default according to who controls the territory, versus de jure.

+ + +

+
+
+
+

countries_banner

+

About

+

Countries distinguish between metropolitan (homeland) and independent and semi-independent portions of sovereign states. If you want to see the dependent overseas regions broken out (like in ISO codes, see France for example), use map units instead.

+

Each country is coded with a world region that roughly follows the United Nations setup.

+

Includes some thematic data from the United Nations, U.S. Central Intelligence Agency, and elsewhere.

+

Disclaimer

+

Natural Earth Vector draws boundaries of countries according to defacto status. We show who actually controls the situation on the ground. Please feel free to mashup our disputed areas (link) theme to match your particular political outlook.

+

Known Problems

+

None.

+

Version History

+ + +

The master changelog is available on Github »

+
+ + + + +
+ +
+ + + + + +
+ + + + + + +
+
    +
  1. +
    + + + + +

    […] earlier. It’s the result of a conversion of a polygon shapefile of country boundaries (from Natural Earth, a fantastic, public domain, physical/cultural spatial data source) to a raster data […]

    + + +
    +
  2. +
  3. +
    + + + + +

    […] Le mappe sono scaricate da https://www.naturalearthdata.com […]

    + + +
    +
  4. +
  5. +
    + + + + +

    […] Le mappe sono scaricate da https://www.naturalearthdata.com […]

    + + +
    +
  6. +
+
+
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + \ No newline at end of file diff --git a/gis/geo_pathogen/country_data/ne_110m_admin_0_countries.VERSION.txt b/gis/geo_pathogen/country_data/ne_110m_admin_0_countries.VERSION.txt new file mode 100644 index 000000000..d7445e2db --- /dev/null +++ b/gis/geo_pathogen/country_data/ne_110m_admin_0_countries.VERSION.txt @@ -0,0 +1 @@ +5.1.1 diff --git a/gis/geo_pathogen/country_data/ne_110m_admin_0_countries.cpg b/gis/geo_pathogen/country_data/ne_110m_admin_0_countries.cpg new file mode 100644 index 000000000..3ad133c04 --- /dev/null +++ b/gis/geo_pathogen/country_data/ne_110m_admin_0_countries.cpg @@ -0,0 +1 @@ +UTF-8 \ No newline at end of file diff --git a/gis/geo_pathogen/country_data/ne_110m_admin_0_countries.dbf b/gis/geo_pathogen/country_data/ne_110m_admin_0_countries.dbf new file mode 100755 index 000000000..e0acd0664 Binary files /dev/null and b/gis/geo_pathogen/country_data/ne_110m_admin_0_countries.dbf differ diff --git a/gis/geo_pathogen/country_data/ne_110m_admin_0_countries.prj b/gis/geo_pathogen/country_data/ne_110m_admin_0_countries.prj new file mode 100755 index 000000000..b13a71791 --- /dev/null +++ b/gis/geo_pathogen/country_data/ne_110m_admin_0_countries.prj @@ -0,0 +1 @@ +GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.017453292519943295]] \ No newline at end of file diff --git a/gis/geo_pathogen/country_data/ne_110m_admin_0_countries.shp b/gis/geo_pathogen/country_data/ne_110m_admin_0_countries.shp new file mode 100755 index 000000000..9318e45c7 Binary files /dev/null and b/gis/geo_pathogen/country_data/ne_110m_admin_0_countries.shp differ diff --git a/gis/geo_pathogen/country_data/ne_110m_admin_0_countries.shx b/gis/geo_pathogen/country_data/ne_110m_admin_0_countries.shx new file mode 100755 index 000000000..c3728e0dd Binary files /dev/null and b/gis/geo_pathogen/country_data/ne_110m_admin_0_countries.shx differ diff --git a/gis/geo_pathogen/dependencies.txt b/gis/geo_pathogen/dependencies.txt new file mode 100644 index 000000000..ba9e8b7e9 --- /dev/null +++ b/gis/geo_pathogen/dependencies.txt @@ -0,0 +1,3 @@ +mesa >= 3.5 +solara +matplotlib \ No newline at end of file diff --git a/gis/geo_pathogen/images/dense_population.png b/gis/geo_pathogen/images/dense_population.png new file mode 100644 index 000000000..a4d5a7dd2 Binary files /dev/null and b/gis/geo_pathogen/images/dense_population.png differ diff --git a/gis/geo_pathogen/images/full_compliance.png b/gis/geo_pathogen/images/full_compliance.png new file mode 100644 index 000000000..2e0cb808f Binary files /dev/null and b/gis/geo_pathogen/images/full_compliance.png differ diff --git a/gis/geo_pathogen/images/no_compliance.png b/gis/geo_pathogen/images/no_compliance.png new file mode 100644 index 000000000..023bc3935 Binary files /dev/null and b/gis/geo_pathogen/images/no_compliance.png differ diff --git a/gis/geo_pathogen/images/sparse_population.png b/gis/geo_pathogen/images/sparse_population.png new file mode 100644 index 000000000..a683db1e3 Binary files /dev/null and b/gis/geo_pathogen/images/sparse_population.png differ diff --git a/gis/geo_pathogen/model.py b/gis/geo_pathogen/model.py new file mode 100644 index 000000000..e05e2ba0a --- /dev/null +++ b/gis/geo_pathogen/model.py @@ -0,0 +1,96 @@ +from pathlib import Path + +import mesa_geo as mg +from agents import Citizen, CountryAgent +from mesa import DataCollector, Model +from shapely.geometry import Point +from shapely.ops import unary_union + + +def c_healthy(model): + return sum( + 1 for i in model.agents if isinstance(i, Citizen) and i.state == "healthy" + ) + + +def c_infected(model): + return sum( + 1 for i in model.agents if isinstance(i, Citizen) and i.state == "infected" + ) + + +def c_immune(model): + return sum( + 1 for i in model.agents if isinstance(i, Citizen) and i.state == "immune" + ) + + +def c_dead(model): + return sum(1 for i in model.agents if isinstance(i, Citizen) and i.state == "dead") + + +class GeoModel(Model): + def __init__( + self, + n=100, + infn=5, + quarantine_threshold_strt=25, + quarantine_threshold_stp=5, + compliance=0.25, + exposure_distance=5, + mobility_range=5, + ): + 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.exposure = exposure_distance + self.space = mg.GeoSpace(crs="EPSG:4326", warn_crs_conversion=False) + self.mobility_range = mobility_range + self.datacollector = DataCollector( + model_reporters={ + "healthy": c_healthy, + "immune": c_immune, + "infected": c_infected, + "dead": c_dead, + "quarantine": lambda i: int(i.quarantine_status), + } + ) + + ac_countries = mg.AgentCreator(CountryAgent, model=self) + self.countries = ac_countries.from_file( + Path(__file__).resolve().parent + / "country_data/ne_110m_admin_0_countries.shp" + ) + + self.countries = [ + c for c in self.countries if c.CONTINENT in ["Asia", "Europe"] + ] + + for _ in range(n): + country = self.random.choice(self.countries) + bounds = country.geometry.bounds + x = self.random.uniform(bounds[0], bounds[2]) + y = self.random.uniform(bounds[1], bounds[3]) + agent = Citizen(self, Point(x, y), self.space.crs) + self.space.add_agents(agent) + self.asia_boundary = unary_union([c.geometry for c in self.countries]) + self.space.add_agents(self.countries) + citizens = [a for a in self.agents if isinstance(a, Citizen)] + toinfect = min(infn, len(citizens)) + for i in range(toinfect): + citizens[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_by_type[Citizen].shuffle_do("step")