diff --git a/README.md b/README.md index fc47c4f0..574a4570 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,9 @@ A simple agent-based simulation showing how rumors spread through a population b ## Continuous Space Examples -_No user examples available yet._ +### [Brownian Particles Model](https://github.com/mesa/mesa-examples/tree/main/examples/brownian_particles) + +A minimal continuous space example — particles doing random walk (Brownian motion) with soft repulsion so they spread out naturally. Colour-coded by local density using a plasma colormap. Good starting point for `mesa.experimental.continuous_space`. ## Network Examples diff --git a/examples/brownian_particles/README.md b/examples/brownian_particles/README.md new file mode 100644 index 00000000..6929298a --- /dev/null +++ b/examples/brownian_particles/README.md @@ -0,0 +1,34 @@ +# Brownian Particles + +A simple model of particles diffusing through a 2D continuous space. + +## What it shows + +Each particle moves randomly each step (Brownian/random walk motion) and softly +repels nearby particles to avoid clustering. The colour of each particle reflects +how many neighbours it currently has — blue/purple means isolated, yellow/red +means densely packed. + +This is a minimal example of **continuous space** in Mesa. There is no grid; +agents can be anywhere in a bounded region and interact based on Euclidean distance. + +## How to run + +```bash +cd examples/brownian_particles +solara run app.py +``` + +## Parameters + +| Parameter | What it does | +|---|---| +| Number of Particles | Total agents in the space | +| Diffusion Rate | Size of random jump each step | +| Neighbor Detection Radius | Distance within which particles sense each other | + +## Files + +- `brownian_particles/agents.py` — Particle agent (Brownian motion + soft repulsion) +- `brownian_particles/model.py` — BrownianModel, handles setup and scheduling +- `app.py` — Solara visualization with interactive sliders diff --git a/examples/brownian_particles/app.py b/examples/brownian_particles/app.py new file mode 100644 index 00000000..12eff969 --- /dev/null +++ b/examples/brownian_particles/app.py @@ -0,0 +1,66 @@ +import matplotlib.cm as cm +import matplotlib.colors as mcolors +from brownian_particles.model import BrownianModel, BrownianScenario +from mesa.visualization import Slider, SolaraViz +from mesa.visualization.components.matplotlib_components import SpaceMatplotlib + +# color particles based on how many neighbors they have +# more neighbors = warmer color (yellow/red), fewer = cooler (blue/purple) +_cmap = cm.get_cmap("plasma") +_norm = mcolors.Normalize(vmin=0, vmax=10) + + +def particle_draw(agent): + # clamp neighbor count to 0-10 range for the colormap + nc = min(agent.n_neighbors, 10) + rgba = _cmap(_norm(nc)) + hex_color = mcolors.to_hex(rgba) + return {"color": hex_color, "size": 15} + + +model_params = { + "seed": { + "type": "InputText", + "value": 42, + "label": "Random Seed", + }, + "n_particles": Slider( + label="Number of Particles", + value=80, + min=10, + max=300, + step=10, + ), + "diffusion_rate": Slider( + label="Diffusion Rate", + value=0.5, + min=0.1, + max=3.0, + step=0.1, + ), + "vision": Slider( + label="Neighbor Detection Radius", + value=4.0, + min=1.0, + max=15.0, + step=0.5, + ), +} + + +def make_model(**kwargs): + scenario = BrownianScenario( + n_particles=kwargs.get("n_particles", 80), + diffusion_rate=kwargs.get("diffusion_rate", 0.5), + vision=kwargs.get("vision", 4.0), + ) + return BrownianModel(scenario=scenario) + + +page = SolaraViz( + make_model, + components=[SpaceMatplotlib(particle_draw)], + model_params=model_params, + name="Brownian Particles", +) +page # noqa diff --git a/examples/brownian_particles/brownian_particles/agents.py b/examples/brownian_particles/brownian_particles/agents.py new file mode 100644 index 00000000..23655848 --- /dev/null +++ b/examples/brownian_particles/brownian_particles/agents.py @@ -0,0 +1,38 @@ +import numpy as np +from mesa.experimental.continuous_space import ContinuousSpaceAgent + + +class Particle(ContinuousSpaceAgent): + """ + A particle that moves around randomly (Brownian motion) and + gently pushes away from nearby particles so they don't pile up. + """ + + def __init__(self, model, space, position, diffusion_rate=0.5, vision=3.0): + super().__init__(space, model) + self.position = np.array(position, dtype=float) + self.diffusion_rate = diffusion_rate + self.vision = vision + self.n_neighbors = 0 # how many particles are nearby, used for coloring + + def step(self): + # random kick - this is the Brownian part + noise = self.model.rng.uniform(-1, 1, size=2) * self.diffusion_rate + + # soft repulsion: push away from particles that are too close + neighbors, distances = self.get_neighbors_in_radius(radius=self.vision) + neighbors = [n for n in neighbors if n is not self] + self.n_neighbors = len(neighbors) + + repulsion = np.zeros(2) + if neighbors: + diff = self.space.calculate_difference_vector( + self.position, agents=neighbors + ) + # closer particles push harder + for i, d in enumerate(distances[1:]): # skip self (distance=0) + if d > 0: + repulsion -= diff[i] / (d**2 + 0.1) + repulsion *= 0.05 + + self.position = self.position + noise + repulsion diff --git a/examples/brownian_particles/brownian_particles/model.py b/examples/brownian_particles/brownian_particles/model.py new file mode 100644 index 00000000..7a0569c5 --- /dev/null +++ b/examples/brownian_particles/brownian_particles/model.py @@ -0,0 +1,65 @@ +import mesa +import numpy as np +from brownian_particles.agents import Particle +from mesa.experimental.continuous_space import ContinuousSpace +from mesa.experimental.scenarios import Scenario + + +class BrownianScenario(Scenario): + """ + Parameters for the Brownian particle model. + These show up as sliders in the UI. + """ + + n_particles: int = 80 + width: float = 50.0 + height: float = 50.0 + diffusion_rate: float = 0.5 # how much each particle jumps per step + vision: float = 4.0 # radius in which particles sense neighbors + + +class BrownianModel(mesa.Model): + """ + A simple model where particles diffuse through a 2D continuous space. + They follow Brownian motion with a soft repulsion to avoid overlapping. + + This is a basic example of continuous space in Mesa - good for understanding + how agents move in non-grid environments. + """ + + def __init__(self, scenario=None): + if scenario is None: + scenario = BrownianScenario() + + super().__init__(scenario=scenario) + + self.space = ContinuousSpace( + [[0, scenario.width], [0, scenario.height]], + torus=True, + random=self.random, + n_agents=scenario.n_particles, + ) + + # scatter particles randomly across the space + positions = self.rng.random(size=(scenario.n_particles, 2)) * np.array( + [scenario.width, scenario.height] + ) + + Particle.create_agents( + self, + scenario.n_particles, + self.space, + position=positions, + diffusion_rate=scenario.diffusion_rate, + vision=scenario.vision, + ) + + self.datacollector = mesa.DataCollector( + model_reporters={ + "Avg Neighbors": lambda m: np.mean([a.n_neighbors for a in m.agents]) + } + ) + + def step(self): + self.agents.shuffle_do("step") + self.datacollector.collect(self)