Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
76 changes: 76 additions & 0 deletions examples/painted_desert/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@

# Painted Desert Challenge Model

![Painted Desert Screenshot](painted_desert_screenshot.png)

This is a Mesa implementation of the NetLogo model [Painted Desert Challenge](https://www.netlogoweb.org/launch#https://www.netlogoweb.org/assets/modelslib/Sample%20Models/Computer%20Science/Painted%20Desert%20Challenge.nlogox), which is an extension of the classic Termites model. The model demonstrates how simple agent behaviors can lead to emergent complex patterns, specifically the sorting of colored wood chips into separate piles.

## What is it?

In the basic Termites model, agents follow simple rules that result in them moving all wood chips into a single pile. The Painted Desert Challenge adds the dimension of multiple types (colors) of wood chips, with the goal of getting the agents to sort each chip type into its own pile.

## How it Works

Each chip collector agent (representing a termite) follows these simple rules:

1. **Search for a chip**: Agents wander randomly until they find a wood chip of their assigned color.
2. **Pick up the chip**: When an agent finds a matching chip, it picks it up and moves away from that spot.
3. **Find a similar chip**: The agent continues wandering until it finds another chip of the same color.
4. **Deposit the chip**: When near a similar chip, the agent looks for an empty space nearby and deposits its chip there.

With these simple rules, the wood chips eventually become sorted into distinct piles by color, demonstrating emergent order from decentralized decision-making.

## Model Parameters

The model includes several adjustable parameters:

- **Grid width**: Width of the simulation grid (20-100)
- **Grid height**: Height of the simulation grid (20-100)
- **Chip density**: Initial density of wood chips as a percentage (10-90%)
- **Number of agents**: Number of chip collector agents (50-250)
- **Number of chip colors**: Number of different chip colors (2-14)

## Visualization

The Solara-based visualization shows:
- **Empty patches**: Black squares
- **Wood chips**: Colored squares based on their type
- **Agents**:
- White when not carrying a chip
- Colored to match the chip they're carrying when transporting one

## How to Run

1. Install dependencies:
```bash
pip install mesa[rec]
```

2. Run the app:
```bash
solara run app.py
```

## Code Structure

The model is organized into the following files:

- **painted_desert/model.py**: Contains the `ChipCollectorModel` class that sets up the grid, initializes patches with chips, and manages the simulation
- **painted_desert/agents.py**: Contains the `ChipCollector` class that defines agent behavior
- **app.py**: Sets up the Solara visualization interface

## Things to Notice

- **Emergent sorting**: Watch as the randomly distributed colored chips gradually organize into distinct colored piles
- **Pile dynamics**: Piles are not "protected" - agents will sometimes take chips from existing piles, which helps merge smaller piles into larger ones
- **Round piles**: The final piles tend to be roughly round in shape
- **Decreasing pile count**: Over time, the number of piles generally decreases as smaller piles are merged into larger ones

## Key Concepts Demonstrated

- **Emergent behavior**: Complex patterns arising from simple individual rules
- **Self-organization**: Agents organizing their environment without central coordination
- **Decentralized systems**: No single agent is in charge, yet the system as a whole accomplishes a complex task
- **Agent-based modeling**: Simulating individual behaviors to study system-level outcomes

This model is an excellent example of how simple local interactions can lead to sophisticated global patterns, a common theme in complex adaptive systems.
114 changes: 114 additions & 0 deletions examples/painted_desert/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
from matplotlib.colors import ListedColormap
from mesa.visualization import SolaraViz, make_space_component
from mesa.visualization.components import AgentPortrayalStyle, PropertyLayerStyle
from painted_desert.model import ChipCollectorModel

# Centralized color mapping for easy modification
CHIP_COLOR_MAP = {
0: "black", # Background/empty patches
-1: "white", # Agents not carrying chips
1: "grey",
2: "red",
3: "orange",
4: "brown",
5: "yellow",
6: "green",
7: "lime",
8: "turquoise",
9: "cyan",
10: "navy",
11: "blue",
12: "violet",
13: "magenta",
14: "pink",
}


def agent_portrayal(agent):
"""Define how chip collector agents appear in the visualization."""
if agent is None:
return None

# visual_color can be -1 (int) when not carrying, or chip_color (int) when carrying
visual_color = agent.visual_color
color = CHIP_COLOR_MAP.get(visual_color, "black")

return AgentPortrayalStyle(color=color, size=50, edgecolors="black")


def property_portrayal(layer):
"""Define how property layers (patch colors) appear in the visualization."""
if layer.name != "pcolor":
return None

color_list = [CHIP_COLOR_MAP.get(i, "gray") for i in range(15)]
cmap = ListedColormap(color_list)

return PropertyLayerStyle(
colormap=cmap,
vmin=0,
vmax=14, # Max expected value
alpha=1.0,
colorbar=False,
)


model_params = {
"width": {
"type": "SliderInt",
"value": 40,
"label": "Grid width:",
"min": 20,
"max": 100,
"step": 5,
},
"height": {
"type": "SliderInt",
"value": 40,
"label": "Grid height:",
"min": 20,
"max": 100,
"step": 5,
},
"density": {
"type": "SliderFloat",
"value": 45,
"label": "Chip density (%):",
"min": 10,
"max": 90,
"step": 5,
},
"number": {
"type": "SliderInt",
"value": 150,
"label": "Number of agents:",
"min": 50,
"max": 250,
"step": 10,
},
"colors": {
"type": "SliderInt",
"value": 4,
"label": "Number of chip colors:",
"min": 2,
"max": 14,
"step": 1,
},
}

# Create initial model instance
woodchip_model = ChipCollectorModel(
width=40, height=40, density=45, number=150, colors=4, seed=42
)

# Create visualization page
page = SolaraViz(
woodchip_model,
components=[
make_space_component(
agent_portrayal=agent_portrayal, propertylayer_portrayal=property_portrayal
)
],
model_params=model_params,
name="Painted Desert Challenge Model",
)
108 changes: 108 additions & 0 deletions examples/painted_desert/painted_desert/agents.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
from mesa.discrete_space import Grid2DMovingAgent


class ChipCollector(Grid2DMovingAgent):
"""Represents a termite that collects and organizes colored wood chips."""

def __init__(self, model, cell, color):
"""
Initialize the chip collector agent.

Args:
model: The associated ChipCollectorModel instance.
cell: Initial cell.
color: Initial chip color.
"""
super().__init__(model)
self.cell = cell
self.chip_color = color
self.chip = False
self.visual_color = -1 # White when not carrying

def wiggle(self):
"""Move forward 1 step, then turn random angle (-50 to +50 degrees).
In discrete grid: move to a random neighbor (simulating forward + random turn)."""
neighbors = list(self.cell.neighborhood)
if neighbors:
self.cell = self.random.choice(neighbors)

def get_away(self):
"""Turn random direction, move back 10 steps, stop if on empty (black) cell.
Executes recursively (iteratively) until on empty cell."""
# Keep moving back until on empty cell
while True:
# Turn random direction, move back 10 steps
for _ in range(10):
neighbors = list(self.cell.neighborhood)
if neighbors:
self.cell = self.random.choice(neighbors)
# Stop if on empty (black) cell
if self.cell.pcolor == 0: # black = empty
return # stop

def find_chip(self):
"""Find a wood chip and pick it up.
Executes recursively (iteratively) until chip is found and picked up."""
# Keep searching until chip is found
while True:
if self.cell.pcolor == self.chip_color: # if wood-chip is my color
# Pick up the chip
self.cell.pcolor = 0 # Set patch to empty (black)
self.chip = True
self.visual_color = self.chip_color
# Get away from this spot
self.get_away()
return # stop
# Wiggle and continue searching
self.wiggle()

def find_new_pile(self):
"""Find another wood chip of the same color.
Executes recursively (iteratively) until matching chip is found."""
# Keep searching until matching chip is found
while True:
if self.cell.pcolor == self.chip_color: # Found matching chip
return # stop
# Move forward 10 steps (move 10 times to random neighbors)
for _ in range(10):
neighbors = list(self.cell.neighborhood)
if neighbors:
self.cell = self.random.choice(neighbors)
# Wiggle
self.wiggle()

def find_empty_spot(self):
"""Find a place to put down wood chip.
Executes recursively (iteratively) until empty spot is found and chip is put down."""
# Keep searching until empty spot is found
while True:
if (
self.cell.pcolor == 0
): # if find a patch without a wood chip (black = empty)
# Put down the chip
self.cell.pcolor = self.chip_color
self.chip = False
self.visual_color = -1 # white
# Move forward 20 steps
for _ in range(20):
neighbors = list(self.cell.neighborhood)
if neighbors:
self.cell = self.random.choice(neighbors)
return # stop
# Turn random direction (wiggle) and move forward 1
self.wiggle()

def step(self):
"""Execute agent behavior in one time step.

Main procedure (go):
- If not carrying: find-chip
- If carrying: find-new-pile, then find-empty-spot
"""
if not self.chip:
# Not carrying a chip: find-chip
self.find_chip()
else:
# Carrying a chip: find-new-pile, then find-empty-spot
self.find_new_pile()
self.find_empty_spot()
65 changes: 65 additions & 0 deletions examples/painted_desert/painted_desert/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import mesa
from mesa.discrete_space import OrthogonalMooreGrid

from .agents import ChipCollector


class ChipCollectorModel(mesa.Model):
"""Model simulating chip collectors organizing wood chips on patches."""

def __init__(
self, width=50, height=50, density=40, number=150, colors=4, seed=None
):
"""Initialize the model.

Args:
width: Width of the grid.
height: Height of the grid.
density: Probability (0-100) that a patch will have a colored chip initially.
number: Number of chip collector agents.
colors: Number of different chip colors.
seed: Random seed for reproducibility.
"""
super().__init__(seed=seed)
self.width = width
self.height = height
self.density = density
self.number = number
self.colors = colors

# Create grid
self.grid = OrthogonalMooreGrid((width, height), random=self.random)

# Create property layer for patch colors (pcolor)
# 0 = black (empty), other values = colored chips
self.grid.create_property_layer("pcolor", default_value=0, dtype=int)

# Initialize patches with colored chips based on density
self._setup_patches()

# Create chip collector agents
self._setup_agents()

def _setup_patches(self):
"""Initialize patches with colored chips based on density parameter."""
for cell in self.grid.all_cells:
# If random number < density, set a colored chip
if self.random.random() * 100 < self.density:
chip_color = self.random.randint(1, self.colors)
cell.pcolor = chip_color

def _setup_agents(self):
"""Create chip collector agents at random positions."""
# Generate random positions for agents
all_cells = list(self.grid.all_cells.cells)
selected_cells = self.random.choices(all_cells, k=self.number)

# Create agents (they will pick up any chip they find)
for cell in selected_cells:
ChipCollector(self, cell, self.random.randint(1, self.colors))
# Agent is already initialized with cell, so we don't need to move it

def step(self):
"""Execute one time step of the model."""
# Shuffle and execute all agents' step methods
self.agents.shuffle_do("step")
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.