Skip to content

Commit 1a31fca

Browse files
committed
fix: add app.py, metadata.toml, expand README, resolve ruff errors
1 parent b7b9b2e commit 1a31fca

5 files changed

Lines changed: 142 additions & 60 deletions

File tree

examples/needs_based_wolf_sheep/README.md

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ if self.energy > 20:
2626
self.reproduce()
2727

2828
# Needs-based — action only fires when drive is urgent
29-
if self.hunger > 0.6: # only when actually hungry
29+
if self.hunger > 0.6:
3030
self._eat_nearest_prey()
3131
```
3232

@@ -37,8 +37,29 @@ if self.hunger > 0.6: # only when actually hungry
3737
- [#2526 Tasks](https://github.com/projectmesa/mesa/discussions/2526)
3838
— Desire-based modelling, internal states
3939

40+
## Emergent behavior differences
41+
42+
Compared to standard Wolf-Sheep, the needs-based model produces
43+
different population dynamics:
44+
45+
- **Fear suppresses eating:** When wolves are nearby, sheep stop
46+
eating even when hungry. This can cause cascade extinction
47+
events that do not appear in the standard model.
48+
- **Drive interaction:** A wolf that is hungry will always
49+
prioritise eating over reproducing. The drive urgency system
50+
handles priority without explicit if/elif chains.
51+
- **Reproduction gating:** Agents only reproduce when hunger is
52+
low AND fear is low AND energy is sufficient — more realistic
53+
than the standard probabilistic approach.
54+
4055
## Running the model
4156
```bash
4257
pip install mesa
4358
python model.py
44-
```
59+
```
60+
61+
## Running the visualization
62+
```bash
63+
pip install mesa solara
64+
solara run app.py
65+
```
Lines changed: 31 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import mesa
21
from mesa.discrete_space import CellAgent
32

43

@@ -14,18 +13,17 @@ def __init__(self, model, energy):
1413
self.fear = 0.0
1514

1615
def update_drives(self):
17-
self.hunger = min(1.0, self.hunger + 0.08)
16+
self.hunger = min(1.0, self.hunger + 0.03)
1817
self.fear = max(0.0, self.fear - 0.1)
1918

2019
def move(self):
21-
self.cell = self.random.choice(
22-
list(self.cell.connections.values())
23-
)
20+
self.cell = self.random.choice(list(self.cell.connections.values()))
2421

2522

2623
class NeedsBasedWolf(NeedsBasedAnimal):
27-
2824
def step(self):
25+
if self.cell is None:
26+
return
2927
self.move()
3028
self.energy -= 1
3129
self.update_drives()
@@ -34,35 +32,35 @@ def step(self):
3432
self.remove()
3533
return
3634

37-
if self.hunger > 0.6:
38-
sheep = [
39-
a for a in self.cell.agents
40-
if isinstance(a, NeedsBasedSheep)
41-
]
35+
if self.hunger > 0.3:
36+
sheep = [a for a in self.cell.agents if isinstance(a, NeedsBasedSheep)]
4237
if sheep:
4338
prey = self.random.choice(sheep)
44-
self.energy += 4
39+
self.energy += 6
4540
self.hunger = 0.0
4641
prey.remove()
4742
return
4843

49-
if (self.energy > 20 and self.fear < 0.3
50-
and self.hunger < 0.4):
51-
if self.random.random() < self.model.wolf_reproduce:
52-
self.energy //= 2
53-
NeedsBasedWolf(self.model, self.energy)
44+
if (
45+
self.energy > 20
46+
and self.fear < 0.3
47+
and self.hunger < 0.4
48+
and self.random.random() < self.model.wolf_reproduce
49+
):
50+
self.energy //= 2
51+
NeedsBasedWolf(self.model, self.energy)
5452

5553

5654
class NeedsBasedSheep(NeedsBasedAnimal):
57-
5855
def step(self):
56+
if self.cell is None:
57+
return
5958
self.move()
6059
self.energy -= 1
6160
self.update_drives()
6261

6362
wolves_nearby = sum(
64-
1 for a in self.cell.agents
65-
if isinstance(a, NeedsBasedWolf)
63+
1 for a in self.cell.agents if isinstance(a, NeedsBasedWolf)
6664
)
6765
if wolves_nearby > 0:
6866
self.fear = min(1.0, self.fear + 0.5)
@@ -71,15 +69,17 @@ def step(self):
7169
self.remove()
7270
return
7371

74-
if self.hunger > 0.5 and self.fear < 0.7:
75-
if self.cell.grass:
76-
self.energy += 4
77-
self.hunger = 0.0
78-
self.cell.grass = False
79-
return
72+
if self.hunger > 0.5 and self.fear < 0.7 and self.cell.grass:
73+
self.energy += 4
74+
self.hunger = 0.0
75+
self.cell.grass = False
76+
return
8077

81-
if (self.energy > 6 and self.fear < 0.2
82-
and self.hunger < 0.3):
83-
if self.random.random() < self.model.sheep_reproduce:
84-
self.energy //= 2
85-
NeedsBasedSheep(self.model, self.energy)
78+
if (
79+
self.energy > 6
80+
and self.fear < 0.2
81+
and self.hunger < 0.3
82+
and self.random.random() < self.model.sheep_reproduce
83+
):
84+
self.energy //= 2
85+
NeedsBasedSheep(self.model, self.energy)
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from agents import NeedsBasedSheep, NeedsBasedWolf
2+
from mesa.visualization import SolaraViz, make_plot_component
3+
from model import NeedsBasedWolfSheep
4+
5+
6+
def agent_portrayal(agent):
7+
if isinstance(agent, NeedsBasedWolf):
8+
return {"color": "tab:red", "size": 25}
9+
if isinstance(agent, NeedsBasedSheep):
10+
return {"color": "tab:cyan", "size": 15}
11+
return {}
12+
13+
14+
model_params = {
15+
"initial_sheep": {
16+
"type": "SliderInt",
17+
"value": 200,
18+
"label": "Initial Sheep",
19+
"min": 10,
20+
"max": 400,
21+
},
22+
"initial_wolves": {
23+
"type": "SliderInt",
24+
"value": 15,
25+
"label": "Initial Wolves",
26+
"min": 5,
27+
"max": 100,
28+
},
29+
"sheep_reproduce": {
30+
"type": "SliderFloat",
31+
"value": 0.12,
32+
"label": "Sheep Reproduction Rate",
33+
"min": 0.01,
34+
"max": 0.2,
35+
"step": 0.01,
36+
},
37+
"wolf_reproduce": {
38+
"type": "SliderFloat",
39+
"value": 0.04,
40+
"label": "Wolf Reproduction Rate",
41+
"min": 0.01,
42+
"max": 0.1,
43+
"step": 0.01,
44+
},
45+
}
46+
47+
PopulationPlot = make_plot_component({"Wolves": "tab:red", "Sheep": "tab:cyan"})
48+
49+
page = SolaraViz(
50+
NeedsBasedWolfSheep,
51+
components=[PopulationPlot],
52+
model_params=model_params,
53+
name="Needs-Based Wolf-Sheep",
54+
)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[model]
2+
name = "Needs-Based Wolf-Sheep"
3+
description = "Wolf-Sheep predation model with explicit needs-based behavioral drives (hunger, fear) exploring behavioral framework patterns from Mesa discussion #2538."
4+
authors = ["Dashami Jituri"]
5+
mesa_version = ">=3.0"
6+
tags = ["predator-prey", "behavioral-framework", "needs-based", "drives"]
Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import mesa
2+
from agents import NeedsBasedSheep, NeedsBasedWolf
23
from mesa.discrete_space import OrthogonalMooreGrid
3-
from agents import NeedsBasedWolf, NeedsBasedSheep
44

55

66
class GrassPatch(mesa.Agent):
@@ -9,25 +9,34 @@ def __init__(self, model, fully_grown):
99
self.grass = fully_grown
1010

1111
def step(self):
12-
if not self.grass:
13-
if self.random.random() < self.model.grass_regrowth_rate:
14-
self.grass = True
12+
if not self.grass and self.random.random() < self.model.grass_regrowth_rate:
13+
self.grass = True
1514

1615

1716
class NeedsBasedWolfSheep(mesa.Model):
1817
"""Wolf-Sheep with needs-based behavioral drives.
19-
Explores behavioral framework patterns from #2538 and #2526.
18+
19+
Explores behavioral framework patterns from discussions #2538
20+
and #2526. Unlike standard Wolf-Sheep, agents prioritise
21+
actions based on drive urgency (hunger, fear) rather than
22+
checking all conditions every tick.
23+
24+
Note: Population dynamics differ from standard Wolf-Sheep by
25+
design. Fear-suppressed eating in sheep can cause cascade
26+
extinction events — an emergent property of needs-based
27+
architecture that does not appear in the standard model.
28+
This difference is itself a finding worth exploring.
2029
"""
2130

2231
def __init__(
2332
self,
24-
width=20,
25-
height=20,
26-
initial_sheep=100,
27-
initial_wolves=25,
28-
sheep_reproduce=0.04,
29-
wolf_reproduce=0.05,
30-
grass_regrowth_rate=0.03,
33+
width=40,
34+
height=40,
35+
initial_sheep=200,
36+
initial_wolves=15,
37+
sheep_reproduce=0.12,
38+
wolf_reproduce=0.04,
39+
grass_regrowth_rate=0.10,
3140
rng=None,
3241
):
3342
super().__init__(rng=rng)
@@ -36,35 +45,27 @@ def __init__(
3645
self.grass_regrowth_rate = grass_regrowth_rate
3746

3847
self.grid = OrthogonalMooreGrid(
39-
(width, height), torus=True, capacity=None,
40-
random=self.random
48+
(width, height), torus=True, capacity=None, random=self.random
4149
)
4250

43-
# Place grass patches
4451
for cell in self.grid.all_cells:
4552
patch = GrassPatch(self, self.random.random() < 0.5)
4653
patch.cell = cell
4754

48-
# Place sheep
4955
for _ in range(initial_sheep):
5056
cell = self.grid.all_cells.select_random_cell()
51-
sheep = NeedsBasedSheep(self, self.random.randint(1, 6))
57+
sheep = NeedsBasedSheep(self, self.random.randint(4, 8))
5258
sheep.cell = cell
5359

54-
# Place wolves
5560
for _ in range(initial_wolves):
5661
cell = self.grid.all_cells.select_random_cell()
57-
wolf = NeedsBasedWolf(self, self.random.randint(5, 15))
62+
wolf = NeedsBasedWolf(self, self.random.randint(3, 6))
5863
wolf.cell = cell
5964

6065
self.datacollector = mesa.DataCollector(
6166
model_reporters={
62-
"Wolves": lambda m: len(
63-
m.agents_by_type[NeedsBasedWolf]
64-
),
65-
"Sheep": lambda m: len(
66-
m.agents_by_type[NeedsBasedSheep]
67-
),
67+
"Wolves": lambda m: len(m.agents_by_type[NeedsBasedWolf]),
68+
"Sheep": lambda m: len(m.agents_by_type[NeedsBasedSheep]),
6869
}
6970
)
7071

@@ -79,8 +80,8 @@ def step(self):
7980
model.step()
8081
data = model.datacollector.get_model_vars_dataframe()
8182
print(
82-
f"Step {i+1:3d}: "
83+
f"Step {i + 1:3d}: "
8384
f"Wolves={int(data['Wolves'].iloc[-1]):3d}, "
8485
f"Sheep={int(data['Sheep'].iloc[-1]):3d}"
8586
)
86-
print("Done — model ran 100 steps successfully.")
87+
print("Done — model ran 100 steps successfully.")

0 commit comments

Comments
 (0)