Skip to content

Add new termite example #251

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
48 changes: 48 additions & 0 deletions examples/termites/README.md
Original file line number Diff line number Diff line change
@@ -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)
85 changes: 85 additions & 0 deletions examples/termites/app.py
Original file line number Diff line number Diff line change
@@ -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",
)
Empty file.
88 changes: 88 additions & 0 deletions examples/termites/termites/agents.py
Original file line number Diff line number Diff line change
@@ -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()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@quaquel or @tpike3, is there a more elegant way to select a random empty 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
52 changes: 52 additions & 0 deletions examples/termites/termites/model.py
Original file line number Diff line number Diff line change
@@ -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(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a smart way to initialize the PropertyLayer! That’s one of the advantages of using a standard data structure underneath, it can directly be accessed and modified. @Spartan-71, was this intuitive, or should we document this possibility better?

CC @quaquel basically a two-liner to initialize the PorpertyLayers, really cool.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should document this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any suggestion where?

[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")