diff --git a/examples/termites/README.md b/examples/termites/README.md new file mode 100644 index 00000000..302bce01 --- /dev/null +++ b/examples/termites/README.md @@ -0,0 +1,48 @@ +# Termite WoodChip Behaviour + +This model simulates termites interacting with wood chips, inspired by the [NetLogo Termites model](https://ccl.northwestern.edu/netlogo/models/Termites). It explores emergent behavior in decentralized systems, demonstrating how simple agents (termites) collectively organize wood chips into piles without centralized coordination. + +## Summary + +In this simulation, multiple termite agents move randomly on a grid containing scattered wood chips. Each termite follows simple rules: + +1. Search for a wood chip. If found, pick it up and move away. +2. When carrying a wood chip, search for a pile (another wood chip). +3. When a pile is found, find a nearby empty space to place the carried chip. +4. After dropping a chip, move away from the pile. + +Over time, these simple interactions lead to the formation of wood chip piles, illustrating decentralized organization without a central coordinator. + +## Installation + +Make sure that you have installed the `latest` version of mesa. + +## Usage + +To run the simulation: +```bash +solara run app.py +``` + +## Model Details + +### Agents + +- **Termite:** An agent that moves within the grid environment, capable of carrying a single wood chip at a time. The termite follows the precise logic of the original NetLogo model, with each behavior (searching, finding piles, dropping chips) continuing until successful. + +### Environment + +- **Grid:** A two-dimensional toroidal grid where termites interact with the wood chips. The toroidal nature means agents exiting one edge re-enter from the opposite edge. +- **PropertyLayer:** A data structure overlaying the grid, storing the presence of wood chips at each cell. + +### Agent Behaviors + +- **wiggle():** Simulates random movement by selecting a random neighboring cell. +- **search_for_chip():** Looks for a wood chip. If found, picks it up and moves forward significantly. +- **find_new_pile():** When carrying a chip, searches for a cell that already has a wood chip. +- **put_down_chip():** Attempts to place the carried wood chip in an empty cell near a pile. +- **get_away():** After dropping a chip, moves away from the pile to prevent clustering. + +## References + +- Wilensky, U. (1997). NetLogo Termites model. Center for Connected Learning and Computer-Based Modeling, Northwestern University, Evanston, IL. Available at: [NetLogo Termites Model](https://ccl.northwestern.edu/netlogo/models/Termites) \ No newline at end of file diff --git a/examples/termites/app.py b/examples/termites/app.py new file mode 100644 index 00000000..ac736ec0 --- /dev/null +++ b/examples/termites/app.py @@ -0,0 +1,85 @@ +from mesa.visualization import SolaraViz +from mesa.visualization.components.matplotlib_components import make_mpl_space_component +from termites.model import TermiteModel + +wood_chip_portrayal = { + "woodcell": { + "color": "blue", + "alpha": 0.6, + "colorbar": False, + "vmin": 0, + "vmax": 2, + }, +} + + +def agent_portrayal(agent): + return { + "marker": ">", + "color": "red" if agent.has_woodchip else "black", + "size": 10, + } + + +model_params = { + "seed": { + "type": "InputText", + "value": 42, + "label": "Seed", + }, + "num_termites": { + "type": "SliderInt", + "value": 100, + "label": "No. of Termites", + "min": 10, + "max": 1000, + "step": 1, + }, + "wood_chip_density": { + "type": "SliderFloat", + "value": 0.1, + "label": "Wood Chip Density", + "min": 0.01, + "max": 1, + "step": 0.1, + }, + "width": { + "type": "SliderInt", + "value": 100, + "label": "Width", + "min": 10, + "max": 500, + "step": 1, + }, + "height": { + "type": "SliderInt", + "value": 100, + "label": "Height", + "min": 10, + "max": 500, + "step": 1, + }, +} + +model = TermiteModel() + + +def post_process(ax): + ax.set_aspect("equal") + ax.set_xticks([]) + ax.set_yticks([]) + + +woodchips_space = make_mpl_space_component( + agent_portrayal=agent_portrayal, + propertylayer_portrayal=wood_chip_portrayal, + post_process=post_process, + draw_grid=False, +) + +page = SolaraViz( + model, + components=[woodchips_space], + model_params=model_params, + name="Termites Model", +) diff --git a/examples/termites/termites/__init__.py b/examples/termites/termites/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/termites/termites/agents.py b/examples/termites/termites/agents.py new file mode 100644 index 00000000..4bcdcaa3 --- /dev/null +++ b/examples/termites/termites/agents.py @@ -0,0 +1,88 @@ +from mesa.experimental.cell_space import CellAgent + + +class Termite(CellAgent): + """ + A Termite agent that has ability to carry woodchip. + + Attributes: + has_woodchip(bool): True if the agent is carrying a wood chip. + """ + + def __init__(self, model, cell): + """ + Args: + model: The model instance. + cell: The starting cell (position) of the agent. + """ + super().__init__(model) + self.cell = cell + self.has_woodchip = False + + def wiggle(self): + self.cell = self.model.random.choice(self.model.grid.all_cells.cells) + + def search_for_chip(self): + if self.cell.woodcell: + self.cell.woodcell = False + self.has_woodchip = True + + for _ in range(10): + new_cell = self.cell.neighborhood.select_random_cell() + if new_cell.is_empty: + self.cell = new_cell + break + return True + else: + # No chip found, wiggle and return False to continue searching + self.wiggle() + return False + + def find_new_pile(self): + # Continue wiggling until finding a cell with a wood chip. + if not self.cell.woodcell: + self.wiggle() + return False + return True + + def put_down_chip(self): + if not self.has_woodchip: + return True + + if not self.cell.woodcell: + self.cell.woodcell = True + self.has_woodchip = False + + self.get_away() + return True + else: + empty_neighbors = [c for c in self.cell.neighborhood if c.is_empty] + if empty_neighbors: + self.cell = self.model.random.choice(empty_neighbors) + return False + + def get_away(self): + for _ in range(10): + new_cell = self.cell.neighborhood.select_random_cell() + if new_cell.is_empty: + self.cell = new_cell + if self.cell.woodcell: + return self.get_away() + break + + def step(self): + """ + Protocol which termite agent follows: + 1. Search for a wood chip if not carrying one. + 2. Find a new pile (a cell with a wood chip) if carrying a chip. + 3. Put down the chip if a suitable location is found. + """ + if not self.has_woodchip: + while not self.search_for_chip(): + pass + + while not self.find_new_pile(): + pass + + while not self.put_down_chip(): + pass diff --git a/examples/termites/termites/model.py b/examples/termites/termites/model.py new file mode 100644 index 00000000..71a1bd06 --- /dev/null +++ b/examples/termites/termites/model.py @@ -0,0 +1,52 @@ +import numpy as np +from mesa import Model +from mesa.experimental.cell_space import OrthogonalMooreGrid, PropertyLayer + +from .agents import Termite + + +class TermiteModel(Model): + """ + A simulation that shows behavior of termite agents gathering wood chips into piles. + """ + + def __init__( + self, num_termites=100, width=100, height=100, wood_chip_density=0.1, seed=42 + ): + """Initialize the model. + + Args: + num_termites: Number of Termite Agents, + width: Grid width. + height: Grid heights. + wood_chip_density: Density of wood chips in the grid. + seed : Random seed for reproducibility. + """ + super().__init__(seed=seed) + self.num_termites = num_termites + self.wood_chip_density = wood_chip_density + + self.grid = OrthogonalMooreGrid((width, height), torus=True) + + self.wood_chips_layer = PropertyLayer( + "woodcell", (width, height), default_value=False, dtype=bool + ) + + # Randomly distribute wood chips, by directly modifying the layer's underlying ndarray + self.wood_chips_layer.data = np.random.choice( + [True, False], + size=(width, height), + p=[self.wood_chip_density, 1 - self.wood_chip_density], + ) + + self.grid.add_property_layer(self.wood_chips_layer) + + # Create agents and randomly distribute them over the grid + Termite.create_agents( + model=self, + n=self.num_termites, + cell=self.random.sample(self.grid.all_cells.cells, k=self.num_termites), + ) + + def step(self): + self.agents.shuffle_do("step")