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

Merged
merged 13 commits into from
May 4, 2025
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 i.e `3.2` onwards.

## 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)
64 changes: 64 additions & 0 deletions examples/termites/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
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.hasWoodChip else "black", "size": 10}


model_params = {
"num_termites": {
"type": "SliderInt",
"value": 100,
"label": "No. of Termites",
"min": 10,
"max": 500,
"step": 1,
},
"wood_chip_density": {
"type": "SliderFloat",
"value": 0.1,
"label": "Wood Chip Density",
"min": 0.01,
"max": 1,
"step": 0.1,
},
"width": 60,
"height": 60,
}

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",
play_interval=1,
render_interval=10,
)
Empty file.
119 changes: 119 additions & 0 deletions examples/termites/termites/agents.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
from mesa.discrete_space import CellAgent


class Termite(CellAgent):
"""
A Termite agent that has ability to carry woodchip.

Attributes:
hasWoodChip(bool): True if the agent is carrying a wood chip.

The termite will:
1. Search for a cell with a wood chip (search_for_chip).
2. Once it picks up the chip, search for a pile (find_new_pile).
3. Put the chip down on an empty cell (put_down_chip), then move away (get_away).
"""

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.hasWoodChip = False

def wiggle(self):
# Move the agent to a random neighboring cell.
self.cell = self.cell.get_neighborhood(radius=3).select_random_cell()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Simplify it to cell = self.model.random.choice(self.model.grid.all_cells.cells)

Copy link
Member

Choose a reason for hiding this comment

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

If I mag ask, why? Isn’t it more logical they choose a cell nearby? Is it performance?

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 initially tried the same (radius=1), but the model struggled to form even medium-sized piles, and there was no chance of combining these piles even after 10k+ steps. Also, performance was the bottleneck.

Copy link
Collaborator

Choose a reason for hiding this comment

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

If I mag ask, why? Isn’t it more logical they choose a cell nearby? Is it performance?

I also should add to it, termites are meant to search for woodchips, and its not mentioned anywhere that they should search their vicinity, therefore this approach just makes that search faster.

Copy link
Member

Choose a reason for hiding this comment

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

Sounds valid. @Spartan-71 could you add this in one or two lines of documentation/comment?


def search_for_chip(self):
"""
If the current cell has a wood chip, pick it up and move forward.
Otherwise, wiggle and continue searching.
"""
# Check if current cell has a wood chip
if self.cell.woodcell:
# Pick up the wood chip
self.cell.woodcell = False
self.hasWoodChip = True

# Move forward
for _ in range(30):
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 current cell is empty (no wood chip), drop the chip.
Otherwise, move forward, then try again.
"""
if not self.hasWoodChip:
return True # Nothing to put down

if not self.cell.woodcell:
# Drop the chip
self.cell.woodcell = True
self.hasWoodChip = False

# Get away from the pile
self.get_away()
return True
else:
# Move to a random neighbor
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):
# Move 20 steps forward randomly untill on a cell with no wood chip.
empty_neighbors = [
c for c in self.cell.get_neighborhood(radius=3) if c.is_empty
]
if empty_neighbors:
self.cell = self.model.random.choice(empty_neighbors)

for _ in range(20):
new_cell = self.cell.neighborhood.select_random_cell()
if new_cell.is_empty:
self.cell = new_cell
# If still on a wood chip, keep going
if self.cell.woodcell:
return self.get_away()
break

def step(self):
"""
Protocol which termite 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.hasWoodChip:
# Keep searching until termite find a chip
while not self.search_for_chip():
pass

# Keep looking for a pile until termite find one
while not self.find_new_pile():
pass

# Keep trying to put down the chip until successful
while not self.put_down_chip():
pass
49 changes: 49 additions & 0 deletions examples/termites/termites/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import numpy as np
from mesa import Model
from mesa.discrete_space import OrthogonalMooreGrid, PropertyLayer

from .agents import Termite


class TermiteModel(Model):
"""
A simulation that depicts behavior of termite agents gathering wood chips into piles.
"""

def __init__(
self, num_termites=100, width=60, height=60, wood_chip_density=0.1, seed=None
):
"""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
)
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)

Termite.create_agents(
self,
self.num_termites,
self.random.sample(self.grid.all_cells.cells, k=self.num_termites),
)

def step(self):
self.agents.shuffle_do("step")