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..69e192cf --- /dev/null +++ b/examples/rumor_spread/app.py @@ -0,0 +1,46 @@ +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", +) diff --git a/examples/rumor_spread/model.py b/examples/rumor_spread/model.py new file mode 100644 index 00000000..b86ba458 --- /dev/null +++ b/examples/rumor_spread/model.py @@ -0,0 +1,87 @@ +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)