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
108 changes: 108 additions & 0 deletions examples/continuous_predator_prey/prey_predator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Continuous Space Predator-Prey Model

## Summary
This model simulates a predator-prey ecosystem in Mesa's experimental `ContinuousSpace`. It demonstrates realistic biological behaviors including lifespans, mating requirements, and energy-based reproduction.

This model has been updated to **Mesa 4.0 standards**, using the native `agents.shuffle_do()` activation and automatic agent ID management.

## Agents

### Prey
- **Movement:** Random motion through continuous space
- **Lifespan:** Maximum age of 40 steps (die of old age)
- **Mating:** Requires at least 1 mate nearby to reproduce
- **Carrying Capacity:** Will NOT reproduce if 6+ prey are nearby (simulates overcrowding/limited resources)
- **Reproduction:** Asexual cloning when conditions are favorable

### Predators
- **Movement:** Random motion with higher speed than prey
- **Lifespan:** Maximum age of 60 steps (die of old age)
- **Energy System:** Lose 1 energy per step, gain energy from eating prey
- **Starvation:** Die immediately if energy reaches 0
- **Hunting:** Hunt prey within radius of 4.0
- **Reproduction:** Only reproduce when energy > 30 (must be well-fed)

## Biological Features

This model implements realistic Lotka-Volterra population dynamics:

1. **Natural Death:** Both species die of old age, preventing immortality
2. **Mating Constraints:** Prey need mates nearby, preventing infinite cloning
3. **Overcrowding:** Prey reproduction limited by local population density
4. **Energy-Based Reproduction:** Predators only reproduce after successful hunting
5. **Starting Energy:** Predators spawn with random energy to survive initial hunting

## How to Run

### Interactive Visualization (SolaraViz)
To launch the interactive web dashboard with sliders and real-time visualization:

```bash
solara run app.py
```

This will open a web browser with:
- **Spatial Map:** Blue circles (Prey) and red triangles (Predators) moving in continuous space
- **Population Chart:** Real-time line graph showing prey and predator population cycles
- **Interactive Sliders:** Adjust initial populations and reproduction rates

### Headless Mode
To run a headless benchmark of the model for 50 steps:

```bash
python run.py
```

## Installation

Make sure you have Mesa installed:

```bash
pip install -r requirements.txt
```

For the interactive visualization, also ensure `solara` is installed:

```bash
pip install solara
```

## Model Parameters

| Parameter | Default | Description |
|-----------|---------|-------------|
| `width` | 100 | Width of the continuous space |
| `height` | 100 | Height of the continuous space |
| `initial_prey` | 100 | Starting prey population |
| `initial_predators` | 20 | Starting predator population |
| `prey_reproduce` | 0.04 | Probability of prey reproduction per step |
| `predator_reproduce` | 0.05 | Probability of predator reproduction per step |
| `predator_gain_from_food` | 20 | Energy gained by predator when eating prey |

## Expected Behavior

When running the simulation, you should observe **Lotka-Volterra population cycles**:

1. Prey population grows rapidly (blue line rises)
2. Predator population follows as food becomes abundant (red line rises)
3. Prey population crashes due to predation (blue line falls)
4. Predator population crashes due to starvation (red line falls)
5. Cycle repeats as prey recover

The population chart should show the classic "chasing waves" pattern where the red predator line follows the blue prey line with a time delay.

## Code Formatting

Mesa strictly enforces the PEP8 coding standard. Run this command in your terminal to automatically format your files:

```bash
ruff check . --fix
ruff format .
```

## Files

- `model.py` - Main model class with Mesa 4.0 native activation
- `agents.py` - Prey and Predator agent classes with biological behaviors
- `app.py` - SolaraViz interactive visualization
- `run.py` - Headless simulation runner
136 changes: 136 additions & 0 deletions examples/continuous_predator_prey/prey_predator/agents.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import math

from mesa.experimental.continuous_space import ContinuousSpaceAgent


class Prey(ContinuousSpaceAgent):
# a prey agent which has random motion in continuous space

def __init__(self, space, model, pos, speed=1.0, max_age=40):
# unique_id is handled automatically by super()
super().__init__(space, model)
self.position = pos
self.speed = speed

# we need give them random starting age so they don't all die on the exact same step!
self.age = self.random.randint(0, max_age)
self.max_age = max_age # it's maximum lifespan of prey agent

# now we need make it motion random so we need move agent to random position based on it's speed
def random_move(self):
# random angle between 0 and 2pi would taken
angle = self.random.uniform(0, 2 * math.pi)
# change in x and y by trigonometry
dx = math.cos(angle) * self.speed
dy = math.sin(angle) * self.speed
# NEW POSITION calculated
new_x = self.position[0] + dx
new_y = self.position[1] + dy

new_pos = (new_x, new_y)

# to make sure it doesn't go off the map; use the new helper
if self.model.space.torus:
Copy link
Collaborator

Choose a reason for hiding this comment

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

You need not to do it, its already handled via position.setter

new_pos = self.model.space.torus_correct(new_pos)

# update the stored position
self.position = new_pos

def step(self):
# it calls the random move function to move the agent in each step of the simulation

# 1. DYING OLD: increase age, and die if too old
self.age += 1 # age increasing each step
if self.age > self.max_age:
self.remove() # agent removed when too old
return

self.random_move()

# check surroundings for mating and overcrowding
neighbors, _ = self.get_neighbors_in_radius(
radius=2.5
) # it get nearby agents within radius
prey_neighbors = [
n for n in neighbors if isinstance(n, Prey)
] # it filter to find only prey

# 2. MATING & CROWDING: must have at least 1 mate nearby (>0),
# but won't reproduce if it's too overcrowded (<6)
# it check not too lonely and not too crowded
if (
0 < len(prey_neighbors) < 6
and self.random.random() < self.model.prey_reproduce
):
# create new prey at the exact same position (asexual reproduction)
Prey(self.model.space, self.model, self.position, self.speed, self.max_age)


class Predator(ContinuousSpaceAgent):
# a predator agent which moves randomly but also hunts nearby prey

def __init__(self, space, model, pos, speed=1.5, energy=0, max_age=60):
# unique_id is handled automatically by super()
super().__init__(space, model)
self.position = pos
self.speed = speed
self.energy = energy # energy level of the predator; it need for survival and reproduction

# dying old - we need give them random starting age
self.age = self.random.randint(0, max_age)
self.max_age = max_age # it's maximum lifespan of predator agent

def random_move(self):
# here we make it hunt the prey if it is nearby
# we will same random walk logic used in prey agent
angle = self.random.uniform(0, 2 * math.pi)
dx = math.cos(angle) * self.speed
dy = math.sin(angle) * self.speed

new_pos = (self.position[0] + dx, self.position[1] + dy)
if self.model.space.torus:
new_pos = self.model.space.torus_correct(new_pos)
self.position = new_pos

def step(self):
# 1. DYING OLD
self.age += 1 # age increasing each step
if self.age > self.max_age:
self.remove() # agent removed when too old
return

self.random_move()
self.energy -= 1 # predator lose energy each step; it's cost of living

# get nearby agents within hunting radius (increased from 2.0 so they can find food easier)
neighbors, _ = self.get_neighbors_in_radius(radius=4.0)
prey_neighbors = [
n for n in neighbors if isinstance(n, Prey)
] # it filter the nearby agents to find the prey

if prey_neighbors:
prey_to_eat = self.random.choice(
prey_neighbors
) # choose random prey to eat
self.energy += self.model.predator_gain_from_food # gain energy from eating

# Modern Mesa 4.0 removal (replaces all the contextlib fallbacks)
prey_to_eat.remove() # prey agent removed from simulation

# Starvation - if energy reaches 0, predator dies
if self.energy <= 0:
self.remove() # predator removed when starved
return

# 3. ENERGY REPRODUCTION: they MUST have high energy (>30) to reproduce now!
if self.energy > 30 and self.random.random() < self.model.predator_reproduce:
self.energy /= 2 # reproduction costs energy; parent loses half
# create new predator with half of parent's energy
Predator(
self.model.space,
self.model,
self.position,
self.speed,
int(self.energy),
self.max_age,
) # new predator starts with half parent's energy
85 changes: 85 additions & 0 deletions examples/continuous_predator_prey/prey_predator/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
from agents import Predator, Prey
from mesa.visualization import Slider, SolaraViz, SpaceRenderer, make_plot_component
from mesa.visualization.components import AgentPortrayalStyle
from model import PredatorPreyModel


def agent_draw(agent):
"""Define how the agents look in the UI."""
if isinstance(agent, Prey):
return AgentPortrayalStyle(
color="blue",
size=15,
marker="o", # Circle for prey
)
elif isinstance(agent, Predator):
return AgentPortrayalStyle(
color="red",
size=30,
marker="^", # Triangle for predator
)


# Interactive sliders for the Solara UI
model_params = {
"initial_prey": Slider(
label="Initial Prey",
value=100,
min=10,
max=300,
step=10,
),
"initial_predators": Slider(
label="Initial Predators",
value=20,
min=1,
max=100,
step=5,
),
"prey_reproduce": Slider(
label="Prey Reproduction Rate",
value=0.04,
min=0.01,
max=0.2,
step=0.01,
),
"predator_reproduce": Slider(
label="Predator Reproduction Rate",
value=0.05,
min=0.01,
max=0.2,
step=0.01,
),
"width": 100,
"height": 100,
}


# Initialize the model
model = PredatorPreyModel()


# Set up the continuous space renderer
renderer = (
SpaceRenderer(
model,
backend="matplotlib",
)
.setup_agents(agent_draw)
.render()
)


# Set up the Population Line Chart
population_chart = make_plot_component({"Prey": "blue", "Predators": "red"})


# Create the Solara webpage
page = SolaraViz(
model,
renderer, # Pass renderer here as the second argument!
components=[population_chart], # ONLY the chart goes in the list!
model_params=model_params,
name="Continuous Space Predator-Prey",
)
page # noqa
Loading
Loading