From b7b9b2e0fb9ef1a58d897b512959b518d7c1f2d3 Mon Sep 17 00:00:00 2001 From: DashamiJituri Date: Sun, 22 Mar 2026 21:08:03 +0530 Subject: [PATCH 1/2] examples: Add needs-based Wolf-Sheep for behavioral framework exploration --- examples/needs_based_wolf_sheep/README.md | 44 ++++++++++ examples/needs_based_wolf_sheep/agents.py | 85 ++++++++++++++++++ examples/needs_based_wolf_sheep/model.py | 86 +++++++++++++++++++ .../needs_based_wolf_sheep/requirements.txt | 1 + 4 files changed, 216 insertions(+) create mode 100644 examples/needs_based_wolf_sheep/README.md create mode 100644 examples/needs_based_wolf_sheep/agents.py create mode 100644 examples/needs_based_wolf_sheep/model.py create mode 100644 examples/needs_based_wolf_sheep/requirements.txt diff --git a/examples/needs_based_wolf_sheep/README.md b/examples/needs_based_wolf_sheep/README.md new file mode 100644 index 000000000..7110e8f35 --- /dev/null +++ b/examples/needs_based_wolf_sheep/README.md @@ -0,0 +1,44 @@ +# 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: # only when actually hungry + 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 + +## Running the model +```bash +pip install mesa +python model.py +``` \ No newline at end of file diff --git a/examples/needs_based_wolf_sheep/agents.py b/examples/needs_based_wolf_sheep/agents.py new file mode 100644 index 000000000..da2ca83a4 --- /dev/null +++ b/examples/needs_based_wolf_sheep/agents.py @@ -0,0 +1,85 @@ +import mesa +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.08) + 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): + self.move() + self.energy -= 1 + self.update_drives() + + if self.energy <= 0: + self.remove() + return + + if self.hunger > 0.6: + sheep = [ + a for a in self.cell.agents + if isinstance(a, NeedsBasedSheep) + ] + if sheep: + prey = self.random.choice(sheep) + self.energy += 4 + self.hunger = 0.0 + prey.remove() + return + + if (self.energy > 20 and self.fear < 0.3 + and self.hunger < 0.4): + if self.random.random() < self.model.wolf_reproduce: + self.energy //= 2 + NeedsBasedWolf(self.model, self.energy) + + +class NeedsBasedSheep(NeedsBasedAnimal): + + def step(self): + 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: + if 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): + if self.random.random() < self.model.sheep_reproduce: + self.energy //= 2 + NeedsBasedSheep(self.model, self.energy) \ No newline at end of file diff --git a/examples/needs_based_wolf_sheep/model.py b/examples/needs_based_wolf_sheep/model.py new file mode 100644 index 000000000..469dbbd5f --- /dev/null +++ b/examples/needs_based_wolf_sheep/model.py @@ -0,0 +1,86 @@ +import mesa +from mesa.discrete_space import OrthogonalMooreGrid +from agents import NeedsBasedWolf, NeedsBasedSheep + + +class GrassPatch(mesa.Agent): + def __init__(self, model, fully_grown): + super().__init__(model) + self.grass = fully_grown + + def step(self): + if not self.grass: + if 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 #2538 and #2526. + """ + + def __init__( + self, + width=20, + height=20, + initial_sheep=100, + initial_wolves=25, + sheep_reproduce=0.04, + wolf_reproduce=0.05, + grass_regrowth_rate=0.03, + 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 + ) + + # Place grass patches + for cell in self.grid.all_cells: + patch = GrassPatch(self, self.random.random() < 0.5) + patch.cell = cell + + # Place sheep + for _ in range(initial_sheep): + cell = self.grid.all_cells.select_random_cell() + sheep = NeedsBasedSheep(self, self.random.randint(1, 6)) + sheep.cell = cell + + # Place wolves + for _ in range(initial_wolves): + cell = self.grid.all_cells.select_random_cell() + wolf = NeedsBasedWolf(self, self.random.randint(5, 15)) + 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.") \ No newline at end of file diff --git a/examples/needs_based_wolf_sheep/requirements.txt b/examples/needs_based_wolf_sheep/requirements.txt new file mode 100644 index 000000000..1ad1bbec7 --- /dev/null +++ b/examples/needs_based_wolf_sheep/requirements.txt @@ -0,0 +1 @@ +mesa \ No newline at end of file From 1a31fca7814ed55b6db6badc38e594544be84ab8 Mon Sep 17 00:00:00 2001 From: DashamiJituri Date: Tue, 24 Mar 2026 21:09:53 +0530 Subject: [PATCH 2/2] fix: add app.py, metadata.toml, expand README, resolve ruff errors --- examples/needs_based_wolf_sheep/README.md | 25 +++++++- examples/needs_based_wolf_sheep/agents.py | 62 +++++++++---------- examples/needs_based_wolf_sheep/app.py | 54 ++++++++++++++++ examples/needs_based_wolf_sheep/metadata.toml | 6 ++ examples/needs_based_wolf_sheep/model.py | 55 ++++++++-------- 5 files changed, 142 insertions(+), 60 deletions(-) create mode 100644 examples/needs_based_wolf_sheep/app.py create mode 100644 examples/needs_based_wolf_sheep/metadata.toml diff --git a/examples/needs_based_wolf_sheep/README.md b/examples/needs_based_wolf_sheep/README.md index 7110e8f35..7c5e44991 100644 --- a/examples/needs_based_wolf_sheep/README.md +++ b/examples/needs_based_wolf_sheep/README.md @@ -26,7 +26,7 @@ if self.energy > 20: self.reproduce() # Needs-based — action only fires when drive is urgent -if self.hunger > 0.6: # only when actually hungry +if self.hunger > 0.6: self._eat_nearest_prey() ``` @@ -37,8 +37,29 @@ if self.hunger > 0.6: # only when actually hungry - [#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 -``` \ No newline at end of file +``` + +## Running the visualization +```bash +pip install mesa solara +solara run app.py +``` diff --git a/examples/needs_based_wolf_sheep/agents.py b/examples/needs_based_wolf_sheep/agents.py index da2ca83a4..fcc7fde29 100644 --- a/examples/needs_based_wolf_sheep/agents.py +++ b/examples/needs_based_wolf_sheep/agents.py @@ -1,4 +1,3 @@ -import mesa from mesa.discrete_space import CellAgent @@ -14,18 +13,17 @@ def __init__(self, model, energy): self.fear = 0.0 def update_drives(self): - self.hunger = min(1.0, self.hunger + 0.08) + 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()) - ) + 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() @@ -34,35 +32,35 @@ def step(self): self.remove() return - if self.hunger > 0.6: - sheep = [ - a for a in self.cell.agents - if isinstance(a, NeedsBasedSheep) - ] + 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 += 4 + self.energy += 6 self.hunger = 0.0 prey.remove() return - if (self.energy > 20 and self.fear < 0.3 - and self.hunger < 0.4): - if self.random.random() < self.model.wolf_reproduce: - self.energy //= 2 - NeedsBasedWolf(self.model, self.energy) + 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) + 1 for a in self.cell.agents if isinstance(a, NeedsBasedWolf) ) if wolves_nearby > 0: self.fear = min(1.0, self.fear + 0.5) @@ -71,15 +69,17 @@ def step(self): self.remove() return - if self.hunger > 0.5 and self.fear < 0.7: - if self.cell.grass: - self.energy += 4 - self.hunger = 0.0 - self.cell.grass = False - 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): - if self.random.random() < self.model.sheep_reproduce: - self.energy //= 2 - NeedsBasedSheep(self.model, self.energy) \ No newline at end of file + 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) diff --git a/examples/needs_based_wolf_sheep/app.py b/examples/needs_based_wolf_sheep/app.py new file mode 100644 index 000000000..9bc63b91e --- /dev/null +++ b/examples/needs_based_wolf_sheep/app.py @@ -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", +) diff --git a/examples/needs_based_wolf_sheep/metadata.toml b/examples/needs_based_wolf_sheep/metadata.toml new file mode 100644 index 000000000..c7850386d --- /dev/null +++ b/examples/needs_based_wolf_sheep/metadata.toml @@ -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"] diff --git a/examples/needs_based_wolf_sheep/model.py b/examples/needs_based_wolf_sheep/model.py index 469dbbd5f..f5c4308a7 100644 --- a/examples/needs_based_wolf_sheep/model.py +++ b/examples/needs_based_wolf_sheep/model.py @@ -1,6 +1,6 @@ import mesa +from agents import NeedsBasedSheep, NeedsBasedWolf from mesa.discrete_space import OrthogonalMooreGrid -from agents import NeedsBasedWolf, NeedsBasedSheep class GrassPatch(mesa.Agent): @@ -9,25 +9,34 @@ def __init__(self, model, fully_grown): self.grass = fully_grown def step(self): - if not self.grass: - if self.random.random() < self.model.grass_regrowth_rate: - self.grass = True + 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 #2538 and #2526. + + 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=20, - height=20, - initial_sheep=100, - initial_wolves=25, - sheep_reproduce=0.04, - wolf_reproduce=0.05, - grass_regrowth_rate=0.03, + 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) @@ -36,35 +45,27 @@ def __init__( self.grass_regrowth_rate = grass_regrowth_rate self.grid = OrthogonalMooreGrid( - (width, height), torus=True, capacity=None, - random=self.random + (width, height), torus=True, capacity=None, random=self.random ) - # Place grass patches for cell in self.grid.all_cells: patch = GrassPatch(self, self.random.random() < 0.5) patch.cell = cell - # Place sheep for _ in range(initial_sheep): cell = self.grid.all_cells.select_random_cell() - sheep = NeedsBasedSheep(self, self.random.randint(1, 6)) + sheep = NeedsBasedSheep(self, self.random.randint(4, 8)) sheep.cell = cell - # Place wolves for _ in range(initial_wolves): cell = self.grid.all_cells.select_random_cell() - wolf = NeedsBasedWolf(self, self.random.randint(5, 15)) + 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] - ), + "Wolves": lambda m: len(m.agents_by_type[NeedsBasedWolf]), + "Sheep": lambda m: len(m.agents_by_type[NeedsBasedSheep]), } ) @@ -79,8 +80,8 @@ def step(self): model.step() data = model.datacollector.get_model_vars_dataframe() print( - f"Step {i+1:3d}: " + 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.") \ No newline at end of file + print("Done — model ran 100 steps successfully.")