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
65 changes: 65 additions & 0 deletions examples/needs_based_wolf_sheep/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Needs-Based Wolf-Sheep

An extension of the classic Wolf-Sheep predation model that
explores **needs-based behavioral architecture** from Mesa
discussion [#2538](https://github.com/projectmesa/mesa/discussions/2538).

## What's different from standard Wolf-Sheep

In the standard model, every agent checks all conditions every
tick — energy thresholds, reproduction probability, nearby prey —
regardless of whether anything changed.

This model restructures that logic using **explicit internal drive
states** that degrade over time and determine action priority:

| Drive | Agent | Behaviour when urgent |
|-------|-------|-----------------------|
| `hunger` | Wolf, Sheep | Prioritise eating |
| `fear` | Sheep | Suppress eating, flee |

## Key behavioral difference
```python
# Standard Wolf-Sheep — all checked every tick
if self.energy > 20:
if random() < p_reproduce:
self.reproduce()

# Needs-based — action only fires when drive is urgent
if self.hunger > 0.6:
self._eat_nearest_prey()
```

## Connection to Mesa discussions

- [#2538 Behavioral Framework](https://github.com/projectmesa/mesa/discussions/2538)
— State system, drive-based decision making
- [#2526 Tasks](https://github.com/projectmesa/mesa/discussions/2526)
— Desire-based modelling, internal states

## Emergent behavior differences

Compared to standard Wolf-Sheep, the needs-based model produces
different population dynamics:

- **Fear suppresses eating:** When wolves are nearby, sheep stop
eating even when hungry. This can cause cascade extinction
events that do not appear in the standard model.
- **Drive interaction:** A wolf that is hungry will always
prioritise eating over reproducing. The drive urgency system
handles priority without explicit if/elif chains.
- **Reproduction gating:** Agents only reproduce when hunger is
low AND fear is low AND energy is sufficient — more realistic
than the standard probabilistic approach.

## Running the model
```bash
pip install mesa
python model.py
```

## Running the visualization
```bash
pip install mesa solara
solara run app.py
```
85 changes: 85 additions & 0 deletions examples/needs_based_wolf_sheep/agents.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
from mesa.discrete_space import CellAgent


class NeedsBasedAnimal(CellAgent):
"""Base class with explicit internal drive states.
Connects to Mesa discussion #2538 State Management component.
"""

def __init__(self, model, energy):
super().__init__(model)
self.energy = energy
self.hunger = 0.0
self.fear = 0.0

def update_drives(self):
self.hunger = min(1.0, self.hunger + 0.03)
self.fear = max(0.0, self.fear - 0.1)

def move(self):
self.cell = self.random.choice(list(self.cell.connections.values()))


class NeedsBasedWolf(NeedsBasedAnimal):
def step(self):
if self.cell is None:
return
self.move()
self.energy -= 1
self.update_drives()

if self.energy <= 0:
self.remove()
return

if self.hunger > 0.3:
sheep = [a for a in self.cell.agents if isinstance(a, NeedsBasedSheep)]
if sheep:
prey = self.random.choice(sheep)
self.energy += 6
self.hunger = 0.0
prey.remove()
return

if (
self.energy > 20
and self.fear < 0.3
and self.hunger < 0.4
and self.random.random() < self.model.wolf_reproduce
):
self.energy //= 2
NeedsBasedWolf(self.model, self.energy)


class NeedsBasedSheep(NeedsBasedAnimal):
def step(self):
if self.cell is None:
return
self.move()
self.energy -= 1
self.update_drives()

wolves_nearby = sum(
1 for a in self.cell.agents if isinstance(a, NeedsBasedWolf)
)
if wolves_nearby > 0:
self.fear = min(1.0, self.fear + 0.5)

if self.energy <= 0:
self.remove()
return

if self.hunger > 0.5 and self.fear < 0.7 and self.cell.grass:
self.energy += 4
self.hunger = 0.0
self.cell.grass = False
return

if (
self.energy > 6
and self.fear < 0.2
and self.hunger < 0.3
and self.random.random() < self.model.sheep_reproduce
):
self.energy //= 2
NeedsBasedSheep(self.model, self.energy)
54 changes: 54 additions & 0 deletions examples/needs_based_wolf_sheep/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from agents import NeedsBasedSheep, NeedsBasedWolf
from mesa.visualization import SolaraViz, make_plot_component
from model import NeedsBasedWolfSheep


def agent_portrayal(agent):
if isinstance(agent, NeedsBasedWolf):
return {"color": "tab:red", "size": 25}
if isinstance(agent, NeedsBasedSheep):
return {"color": "tab:cyan", "size": 15}
return {}


model_params = {
"initial_sheep": {
"type": "SliderInt",
"value": 200,
"label": "Initial Sheep",
"min": 10,
"max": 400,
},
"initial_wolves": {
"type": "SliderInt",
"value": 15,
"label": "Initial Wolves",
"min": 5,
"max": 100,
},
"sheep_reproduce": {
"type": "SliderFloat",
"value": 0.12,
"label": "Sheep Reproduction Rate",
"min": 0.01,
"max": 0.2,
"step": 0.01,
},
"wolf_reproduce": {
"type": "SliderFloat",
"value": 0.04,
"label": "Wolf Reproduction Rate",
"min": 0.01,
"max": 0.1,
"step": 0.01,
},
}

PopulationPlot = make_plot_component({"Wolves": "tab:red", "Sheep": "tab:cyan"})

page = SolaraViz(
NeedsBasedWolfSheep,
components=[PopulationPlot],
model_params=model_params,
name="Needs-Based Wolf-Sheep",
)
6 changes: 6 additions & 0 deletions examples/needs_based_wolf_sheep/metadata.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[model]
name = "Needs-Based Wolf-Sheep"
description = "Wolf-Sheep predation model with explicit needs-based behavioral drives (hunger, fear) exploring behavioral framework patterns from Mesa discussion #2538."
authors = ["Dashami Jituri"]
mesa_version = ">=3.0"
tags = ["predator-prey", "behavioral-framework", "needs-based", "drives"]
87 changes: 87 additions & 0 deletions examples/needs_based_wolf_sheep/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import mesa
from agents import NeedsBasedSheep, NeedsBasedWolf
from mesa.discrete_space import OrthogonalMooreGrid


class GrassPatch(mesa.Agent):
def __init__(self, model, fully_grown):
super().__init__(model)
self.grass = fully_grown

def step(self):
if not self.grass and self.random.random() < self.model.grass_regrowth_rate:
self.grass = True


class NeedsBasedWolfSheep(mesa.Model):
"""Wolf-Sheep with needs-based behavioral drives.

Explores behavioral framework patterns from discussions #2538
and #2526. Unlike standard Wolf-Sheep, agents prioritise
actions based on drive urgency (hunger, fear) rather than
checking all conditions every tick.

Note: Population dynamics differ from standard Wolf-Sheep by
design. Fear-suppressed eating in sheep can cause cascade
extinction events — an emergent property of needs-based
architecture that does not appear in the standard model.
This difference is itself a finding worth exploring.
"""

def __init__(
self,
width=40,
height=40,
initial_sheep=200,
initial_wolves=15,
sheep_reproduce=0.12,
wolf_reproduce=0.04,
grass_regrowth_rate=0.10,
rng=None,
):
super().__init__(rng=rng)
self.sheep_reproduce = sheep_reproduce
self.wolf_reproduce = wolf_reproduce
self.grass_regrowth_rate = grass_regrowth_rate

self.grid = OrthogonalMooreGrid(
(width, height), torus=True, capacity=None, random=self.random
)

for cell in self.grid.all_cells:
patch = GrassPatch(self, self.random.random() < 0.5)
patch.cell = cell

for _ in range(initial_sheep):
cell = self.grid.all_cells.select_random_cell()
sheep = NeedsBasedSheep(self, self.random.randint(4, 8))
sheep.cell = cell

for _ in range(initial_wolves):
cell = self.grid.all_cells.select_random_cell()
wolf = NeedsBasedWolf(self, self.random.randint(3, 6))
wolf.cell = cell

self.datacollector = mesa.DataCollector(
model_reporters={
"Wolves": lambda m: len(m.agents_by_type[NeedsBasedWolf]),
"Sheep": lambda m: len(m.agents_by_type[NeedsBasedSheep]),
}
)

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


if __name__ == "__main__":
model = NeedsBasedWolfSheep()
for i in range(100):
model.step()
data = model.datacollector.get_model_vars_dataframe()
print(
f"Step {i + 1:3d}: "
f"Wolves={int(data['Wolves'].iloc[-1]):3d}, "
f"Sheep={int(data['Sheep'].iloc[-1]):3d}"
)
print("Done — model ran 100 steps successfully.")
1 change: 1 addition & 0 deletions examples/needs_based_wolf_sheep/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
mesa