From 00f524cccb14487a9e73eaf69a494cd1ac3fb3c1 Mon Sep 17 00:00:00 2001 From: RodionOm Date: Tue, 24 Mar 2026 23:24:55 +0000 Subject: [PATCH 1/4] Add rumor spread example with Solara visualization --- examples/rumor_spread/README.md | 67 ++++++++++++++++++++++++++ examples/rumor_spread/app.py | 49 +++++++++++++++++++ examples/rumor_spread/model.py | 85 +++++++++++++++++++++++++++++++++ 3 files changed, 201 insertions(+) create mode 100644 examples/rumor_spread/README.md create mode 100644 examples/rumor_spread/app.py create mode 100644 examples/rumor_spread/model.py diff --git a/examples/rumor_spread/README.md b/examples/rumor_spread/README.md new file mode 100644 index 00000000..b6f84a0f --- /dev/null +++ b/examples/rumor_spread/README.md @@ -0,0 +1,67 @@ +# Rumor Spread Model + +## Summary +This is a small agent-based model built with Mesa to simulate rumor propagation on a 2D grid. + +Each agent occupies one cell in the grid. A small number of agents initially know the rumor. At each step, informed agents may spread the rumor to their neighbors. Each agent also has an individual resistance value, which reduces the probability of becoming informed. + +The model includes: +- a spatial visualization of rumor spread +- a time-series plot showing how many agents are informed over time +- interactive controls for the number of initial spreaders and infection strength + +## Purpose +I built this model as part of my Mesa GSoC 2026 preparation to gain hands-on experience with: +- Mesa model structure +- cell-based agents and grid spaces +- Solara-based visualization +- parameterized simulation experiments + +## Model behavior +- Agents are placed on a toroidal 20x20 grid +- A configurable number of agents start informed +- Informed agents try to spread the rumor to neighboring agents +- Each agent has a resistance value between 0 and 1 +- Higher resistance reduces the chance of becoming informed +- Informed agents can forget the rumor with a certain probability (recovery) + +## Behavior + +The model exhibits three regimes: + +- Extinction: rumor dies out +- Equilibrium: stable fluctuations +- Saturation: near-total spread + +These regimes depend on infection_strength and recovery_rate. + +## Parameters +- **Initial Spreaders**: number of agents that start with the rumor +- **Infection Strength**: base strength of rumor transmission +- **Recovery Rate**: probability of forgetting the rumor each step + +## What I learned +While building this model, I learned: +- the difference between generic Mesa agents and `CellAgent` +- how to use `OrthogonalMooreGrid` and assign agents to cells +- how to collect aggregate metrics with `DataCollector` +- how to render a model using `SolaraViz` and `SpaceRenderer` +- Introduced a recovery mechanism to prevent full saturation and allow more realistic steady-state behavior. +- how small design choices strongly affect emergent dynamics + +## How to run +From this directory: + +```bash +solara run app.py +``` + +## Notes + +This model is intentionally simple. Its goal is not to be a realistic rumor diffusion simulator, but to serve as a compact, reproducible Mesa example for experimentation and learning. + +## Quick observations +- Lower infection strength slows down diffusion and makes the growth curve less abrupt. +- A higher number of initial spreaders accelerates saturation. +- Even in a simple model, agent-level heterogeneity (resistance) changes the global diffusion pattern. +- With recovery, the model reaches a steady state instead of full infection, showing realistic epidemic-like dynamics. \ No newline at end of file diff --git a/examples/rumor_spread/app.py b/examples/rumor_spread/app.py new file mode 100644 index 00000000..a22db595 --- /dev/null +++ b/examples/rumor_spread/app.py @@ -0,0 +1,49 @@ +from mesa.visualization import Slider, SolaraViz, SpaceRenderer, make_plot_component +from mesa.visualization.components.portrayal_components import AgentPortrayalStyle + +from model import RumorModel + + +def portrayal(agent): + """Visual style for rumor agents.""" + if agent is None: + return None + + style = AgentPortrayalStyle(size=100, marker="s", zorder=1) + if agent.has_rumor: + style.update(("color", "red")) + else: + style.update(("color", "lightgray")) + return style + + +def post_process_space(ax): + """Improve readability of the grid plot.""" + ax.set_aspect("equal") + ax.set_xticks([]) + ax.set_yticks([]) + + +model_params = { + "initial_spreaders": Slider("Initial Spreaders", 2, 1, 20, 1), + "infection_strength": Slider("Infection Strength", 0.2, 0.05, 0.5, 0.05), + "recovery_rate": Slider("Recovery Rate", 0.05, 0.0, 0.3, 0.01), +} + +model = RumorModel() + +renderer = SpaceRenderer(model, backend="matplotlib").setup_agents(portrayal) +renderer.post_process = post_process_space +renderer.draw_agents() + +plot_component = make_plot_component({"Informed": "tab:blue"}) + +page = SolaraViz( + model, + renderer, + components=[plot_component], + model_params=model_params, + name="Rumor Spread Model", +) + +page \ No newline at end of file diff --git a/examples/rumor_spread/model.py b/examples/rumor_spread/model.py new file mode 100644 index 00000000..e743e602 --- /dev/null +++ b/examples/rumor_spread/model.py @@ -0,0 +1,85 @@ +from mesa import Model +from mesa.datacollection import DataCollector +from mesa.discrete_space import CellAgent +from mesa.discrete_space.grid import OrthogonalMooreGrid + + +class RumorAgent(CellAgent): + """A grid-based agent that may or may not know the rumor.""" + + def __init__(self, model, cell=None, has_rumor=False): + super().__init__(model) + if cell is not None: + self.cell = cell + self.has_rumor = has_rumor + self.resistance = self.random.random() + + def step(self): + """Spread the rumor to neighboring agents probabilistically.""" + if not self.has_rumor: + return + + for neighbor_cell in self.cell.neighborhood: + for agent in neighbor_cell.agents: + if not agent.has_rumor: + spread_probability = self.model.infection_strength * (1 - agent.resistance) * 0.3 + if self.random.random() < spread_probability: + agent.has_rumor = True + + # Recovery (forget rumor) + if self.random.random() < self.model.recovery_rate: + self.has_rumor = False + + +class RumorModel(Model): + """A simple rumor diffusion model on a 2D toroidal grid.""" + + def __init__( + self, + width=20, + height=20, + initial_spreaders=2, + infection_strength=0.2, + recovery_rate=0.05, + rng=None, + ): + super().__init__(rng=rng) + + self.width = width + self.height = height + self.initial_spreaders = initial_spreaders + self.infection_strength = infection_strength + self.recovery_rate = recovery_rate + + self.grid = OrthogonalMooreGrid( + (width, height), + torus=True, + capacity=1, + random=self.random, + ) + + self.datacollector = DataCollector( + model_reporters={ + "Informed": lambda m: sum(agent.has_rumor for agent in m.agents), + } + ) + + self._initialize_agents() + self.running = True + self.datacollector.collect(self) + + def _initialize_agents(self): + """Create and place agents on the grid.""" + all_coords = [(x, y) for x in range(self.width) for y in range(self.height)] + initial_informed = set(self.random.sample(all_coords, self.initial_spreaders)) + + for x, y in all_coords: + has_rumor = (x, y) in initial_informed + cell = self.grid[(x, y)] + agent = RumorAgent(self, cell=cell, has_rumor=has_rumor) + self.agents.add(agent) + + def step(self): + """Advance the model by one step.""" + self.agents.shuffle_do("step") + self.datacollector.collect(self) \ No newline at end of file From a0c20d4ac799da08c4d1979108f1f515c81fa945 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 23:30:41 +0000 Subject: [PATCH 2/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- examples/rumor_spread/app.py | 3 +-- examples/rumor_spread/model.py | 6 ++++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/examples/rumor_spread/app.py b/examples/rumor_spread/app.py index a22db595..ff74e9b2 100644 --- a/examples/rumor_spread/app.py +++ b/examples/rumor_spread/app.py @@ -1,6 +1,5 @@ from mesa.visualization import Slider, SolaraViz, SpaceRenderer, make_plot_component from mesa.visualization.components.portrayal_components import AgentPortrayalStyle - from model import RumorModel @@ -46,4 +45,4 @@ def post_process_space(ax): name="Rumor Spread Model", ) -page \ No newline at end of file +page diff --git a/examples/rumor_spread/model.py b/examples/rumor_spread/model.py index e743e602..b86ba458 100644 --- a/examples/rumor_spread/model.py +++ b/examples/rumor_spread/model.py @@ -22,7 +22,9 @@ def step(self): for neighbor_cell in self.cell.neighborhood: for agent in neighbor_cell.agents: if not agent.has_rumor: - spread_probability = self.model.infection_strength * (1 - agent.resistance) * 0.3 + spread_probability = ( + self.model.infection_strength * (1 - agent.resistance) * 0.3 + ) if self.random.random() < spread_probability: agent.has_rumor = True @@ -82,4 +84,4 @@ def _initialize_agents(self): def step(self): """Advance the model by one step.""" self.agents.shuffle_do("step") - self.datacollector.collect(self) \ No newline at end of file + self.datacollector.collect(self) From 582b7529a45732f69b927f6dad580fcb11b6e1d7 Mon Sep 17 00:00:00 2001 From: RodionOm Date: Tue, 24 Mar 2026 23:39:12 +0000 Subject: [PATCH 3/4] Fix useless expression in app.py --- examples/rumor_spread/app.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/examples/rumor_spread/app.py b/examples/rumor_spread/app.py index ff74e9b2..39f516b5 100644 --- a/examples/rumor_spread/app.py +++ b/examples/rumor_spread/app.py @@ -43,6 +43,4 @@ def post_process_space(ax): components=[plot_component], model_params=model_params, name="Rumor Spread Model", -) - -page +) \ No newline at end of file From a094b8004bb6ec3835cf39bd55f9c34fd51baf36 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 23:39:20 +0000 Subject: [PATCH 4/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- examples/rumor_spread/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/rumor_spread/app.py b/examples/rumor_spread/app.py index 39f516b5..69e192cf 100644 --- a/examples/rumor_spread/app.py +++ b/examples/rumor_spread/app.py @@ -43,4 +43,4 @@ def post_process_space(ax): components=[plot_component], model_params=model_params, name="Rumor Spread Model", -) \ No newline at end of file +)