Skip to content
177 changes: 129 additions & 48 deletions mesa/examples/advanced/wolf_sheep/agents.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,26 @@
from mesa.discrete_space import CellAgent, FixedAgent
from mesa.experimental.mesa_signals import (
Computable,
Computed,
ContinuousObservable,
HasObservables,
Observable,
)


class Animal(CellAgent):
"""The base animal class."""
class Animal(CellAgent, HasObservables):
"""The base animal class with reactive energy management."""

# Energy depletes continuously over time
energy = ContinuousObservable(
initial_value=8.0, rate_func=lambda value, elapsed, agent: -agent.metabolic_rate
)

# Computed property: animal is hungry when energy is low
is_hungry = Computable()

# Computed property: animal can reproduce when energy is sufficient
can_reproduce = Computable()

def __init__(
self, model, energy=8, p_reproduce=0.04, energy_from_food=4, cell=None
Expand All @@ -17,14 +35,40 @@ def __init__(
cell: Cell in which the animal starts
"""
super().__init__(model)

# Set base metabolic rate (energy loss per time unit when idle)
self.metabolic_rate = 0.5

# Initialize energy (triggers continuous depletion)
self.energy = energy
self.p_reproduce = p_reproduce
self.energy_from_food = energy_from_food
self.cell = cell

# Set up computed properties
self.is_hungry = Computed(lambda: self.energy < self.energy_from_food * 2)
self.can_reproduce = Computed(lambda: self.energy > self.energy_from_food * 4)

# Register threshold: die when energy reaches zero
self.add_threshold("energy", 0.0, self._on_energy_depleted)

# Register threshold: become critically hungry at 25% of starting energy
self.add_threshold("energy", energy * 0.25, self._on_critical_hunger)

def _on_energy_depleted(self, signal):
"""Called when energy crosses zero - animal dies."""
if signal.direction == "down": # Only trigger on downward crossing
self.remove()

def _on_critical_hunger(self, signal):
"""Called when energy becomes critically low."""
if signal.direction == "down":
# Increase metabolic efficiency when starving (survival mode)
self.metabolic_rate *= 0.8

def spawn_offspring(self):
Comment on lines +52 to 69
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Avoid removing agents directly from threshold callback to prevent container mutation during iteration

Animal._on_energy_depleted currently calls self.remove() whenever energy crosses zero downward:

def _on_energy_depleted(self, signal):
    """Called when energy crosses zero - animal dies."""
    if signal.direction == "down":  # Only trigger on downward crossing
        self.remove()

Because this callback is fired from inside ContinuousObservable.__get__/__set__, it can run while:

  • Schedulers are iterating agents in shuffle_do("step"), and/or
  • DataCollector reporters are iterating agents_by_type[...] and accessing .energy.

That matches the RuntimeError: dictionary changed size during iteration reported for the Wolf–Sheep example.

A safer pattern is to mark the agent for removal and let step() perform the actual remove() at a controlled point:

 class Animal(CellAgent, HasObservables):
@@
     def __init__(
         self, model, energy=8, p_reproduce=0.04, energy_from_food=4, cell=None
     ):
@@
-        super().__init__(model)
-
-        # Set base metabolic rate (energy loss per time unit when idle)
-        self.metabolic_rate = 0.5
+        super().__init__(model)
+
+        # Track whether this agent should be removed due to energy depletion
+        self._needs_removal = False
+
+        # Set base metabolic rate (energy loss per time unit when idle)
+        self.metabolic_rate = 0.5
@@
-    def _on_energy_depleted(self, signal):
-        """Called when energy crosses zero - animal dies."""
-        if signal.direction == "down":  # Only trigger on downward crossing
-            self.remove()
+    def _on_energy_depleted(self, signal):
+        """Called when energy crosses zero - mark for removal."""
+        if signal.direction == "down":  # Only trigger on downward crossing
+            self._needs_removal = True
@@
     def step(self):
         """Execute one step of the animal's behavior."""
-        # Move to neighboring cell (uses more energy than standing still)
+        # Remove animals that died since last step
+        if getattr(self, "_needs_removal", False):
+            self.remove()
+            return
+
+        # Move to neighboring cell (uses more energy than standing still)
         self.metabolic_rate = 1.0  # Movement costs more energy
@@
-        # Reproduce if conditions are met (using computed property)
-        if self.can_reproduce and self.random.random() < self.p_reproduce:
+        # Reproduce if conditions are met (using computed property)
+        if self.can_reproduce and self.random.random() < self.p_reproduce:
             self.spawn_offspring()

This preserves the threshold‑driven semantics (death when crossing zero) while avoiding structural mutations of the agent containers during iteration.

Also applies to: 84-99


🏁 Script executed:

#!/bin/bash
# Find and display the Animal class implementation around lines 52-69
fd -e py -p 'agents\.py' | xargs cat

Length of output: 47437


🏁 Script executed:

#!/bin/bash
# Search for ContinuousObservable implementation and threshold mechanism
fd -e py | xargs rg -n -A10 'class ContinuousObservable' | head -60

Length of output: 1167


🏁 Script executed:

#!/bin/bash
# Search for add_threshold and how callbacks are triggered
fd -e py | xargs rg -n 'add_threshold|def add_threshold' -A5 | head -80

Length of output: 5342


🏁 Script executed:

#!/bin/bash
# Search for _on_energy_depleted implementation to see current code
fd -e py | xargs rg -n '_on_energy_depleted' -B2 -A5

Length of output: 1284


🏁 Script executed:

#!/bin/bash
# Examine ContinuousObservable.__set__ and threshold firing logic
rg -n 'class ContinuousObservable' -A 100 mesa/experimental/mesa_signals/mesa_signal.py | head -150

Length of output: 4233


🏁 Script executed:

#!/bin/bash
# Search for where threshold callbacks are actually invoked
rg -n 'threshold_crossed|_check_thresholds|def __set__' mesa/experimental/mesa_signals/mesa_signal.py -A 15 | head -200

Length of output: 5006


🏁 Script executed:

#!/bin/bash
# Check scheduler step implementation to see iteration patterns
rg -n 'class.*Scheduler|def shuffle_do|def step' mesa/experimental/engine.py -A 10 | head -100

Length of output: 67


🏁 Script executed:

#!/bin/bash
# Search for DataCollector iteration patterns
rg -n 'class DataCollector|agents_by_type|def collect' mesa/datacollection.py -A 8 | head -120

Length of output: 1278


🏁 Script executed:

#!/bin/bash
# Look for existing tests that show the RuntimeError issue
rg -n 'RuntimeError.*dict.*changed|dictionary changed size' mesa/examples/ tests/ -B 3 -A 3

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Find scheduler implementations
fd -e py mesa/ | xargs rg -l 'class.*Scheduler' | head -5

Length of output: 325


🏁 Script executed:

#!/bin/bash
# Look for model.step() and how agents are iterated
rg -n 'def step' mesa/model.py -A 20 | head -80

Length of output: 768


🏁 Script executed:

#!/bin/bash
# Check wolf_sheep example for model step or any test files
fd -e py mesa/examples/advanced/wolf_sheep/ | xargs cat | grep -A 20 'def step'

Length of output: 441


🏁 Script executed:

#!/bin/bash
# Search for any deferred removal or agent marking patterns already in codebase
rg -n '_needs_removal|_marked_for_removal|pending_removal' mesa/

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check if notify() method actually executes callbacks immediately
rg -n 'def notify' mesa/experimental/mesa_signals/ -A 30 | head -100

Length of output: 2391


🏁 Script executed:

#!/bin/bash
# Get full _mesa_notify implementation and subscriber invocation
rg -n '_mesa_notify|def _mesa_notify' mesa/experimental/mesa_signals/mesa_signal.py -A 50 | head -150

Length of output: 2419


🏁 Script executed:

#!/bin/bash
# Search for where subscribers are iterated and callbacks invoked
rg -n 'subscribers|callback' mesa/experimental/mesa_signals/mesa_signal.py -B 2 -A 5 | head -200

Length of output: 4233


🏁 Script executed:

#!/bin/bash
# Check wolf_sheep model implementation to see how step() is called
fd . mesa/examples/advanced/wolf_sheep/ | xargs cat

Length of output: 19365


🏁 Script executed:

#!/bin/bash
# Look for scheduler.do() or step execution patterns
rg -n 'class.*Scheduler|def do|shuffle_do' mesa/core/agent.py mesa/agent.py -A 15 | head -100

Length of output: 3240


🏁 Script executed:

#!/bin/bash
# Examine if there's a scheduler being used in model step
rg -n 'scheduler|\.step\(\)' mesa/examples/advanced/wolf_sheep/model.py -B 2 -A 5

Length of output: 0


Deferred removal pattern required to prevent container mutation during threshold callback execution

The review's concern is valid and critical.

Threshold callbacks execute synchronously during property access, and ContinuousObservable's __get__ and __set__ methods both invoke threshold callbacks immediately. When DataCollector.collect(self) iterates agents and accesses their .energy property to compute statistics (e.g., sum(w.energy for w in m.agents_by_type[Wolf])), this triggers __get__, which fires the threshold callback. If _on_energy_depleted calls self.remove() while the dictionary is being iterated, this causes RuntimeError: dictionary changed size during iteration.

The proposed deferred removal pattern is the correct fix:

  • Mark removal with a flag when threshold fires (safe to set flag during iteration)
  • Check and execute removal in step() at a controlled point before returning
  • Preserves threshold-driven semantics while preventing structural mutations

Locations requiring fixes:

  • Add _needs_removal = False initialization in Animal.__init__
  • Modify _on_energy_depleted to set flag instead of calling remove()
  • Add removal check at start of Animal.step() before other logic
🤖 Prompt for AI Agents
In mesa/examples/advanced/wolf_sheep/agents.py around lines 52-69, threshold
callbacks can synchronously remove agents during container iteration causing
"dictionary changed size during iteration"; to fix, add a boolean flag (e.g.,
self._needs_removal = False) in Animal.__init__, change _on_energy_depleted to
set this flag instead of calling self.remove(), and at the start of
Animal.step() check the flag and call self.remove() if set (perform removal at a
controlled point before other per-step logic).

"""Create offspring by splitting energy and creating new instance."""
self.energy /= 2
self.energy /= 2 # This updates the continuous observable
self.__class__(
self.model,
self.energy,
Expand All @@ -35,33 +79,39 @@ def spawn_offspring(self):

def feed(self):
"""Abstract method to be implemented by subclasses."""
raise NotImplementedError

def step(self):
"""Execute one step of the animal's behavior."""
# Move to random neighboring cell
# Move to neighboring cell (uses more energy than standing still)
self.metabolic_rate = 1.0 # Movement costs more energy
self.move()

self.energy -= 1

# Try to feed
self.feed()

# Handle death and reproduction
if self.energy < 0:
self.remove()
elif self.random.random() < self.p_reproduce:
# Return to resting metabolic rate
self.metabolic_rate = 0.5

# Reproduce if conditions are met (using computed property)
if self.can_reproduce and self.random.random() < self.p_reproduce:
self.spawn_offspring()


class Sheep(Animal):
"""A sheep that walks around, reproduces (asexually) and gets eaten."""
"""A sheep that walks around, reproduces and gets eaten.

Sheep prefer cells with grass and avoid wolves. They gain energy by
eating grass, which continuously depletes over time.
"""

def feed(self):
"""If possible, eat grass at current location."""
grass_patch = next(
obj for obj in self.cell.agents if isinstance(obj, GrassPatch)
)
if grass_patch.fully_grown:
# Eating gives instant energy boost
self.energy += self.energy_from_food
grass_patch.fully_grown = False

Expand All @@ -70,64 +120,82 @@ def move(self):
cells_without_wolves = self.cell.neighborhood.select(
lambda cell: not any(isinstance(obj, Wolf) for obj in cell.agents)
)
# If all surrounding cells have wolves, stay put

# If all surrounding cells have wolves, stay put (fear overrides hunger)
if len(cells_without_wolves) == 0:
return

# Among safe cells, prefer those with grown grass
cells_with_grass = cells_without_wolves.select(
lambda cell: any(
isinstance(obj, GrassPatch) and obj.fully_grown for obj in cell.agents
# If critically hungry, prioritize grass over safety
if self.is_hungry: # Using computed property
cells_with_grass = cells_without_wolves.select(
lambda cell: any(
isinstance(obj, GrassPatch) and obj.fully_grown
for obj in cell.agents
)
)
)
# Move to a cell with grass if available, otherwise move to any safe cell
target_cells = (
cells_with_grass if len(cells_with_grass) > 0 else cells_without_wolves
)
# Move to grass if available, otherwise any safe cell
target_cells = (
cells_with_grass if len(cells_with_grass) > 0 else cells_without_wolves
)
else:
# Not hungry - just avoid wolves
target_cells = cells_without_wolves

self.cell = target_cells.select_random_cell()


class Wolf(Animal):
"""A wolf that walks around, reproduces (asexually) and eats sheep."""
"""A wolf that walks around, reproduces and eats sheep.

Wolves are more efficient predators, with higher base energy and
metabolic rate. They actively hunt sheep and gain substantial energy
from successful kills.
"""

def __init__(
self, model, energy=20, p_reproduce=0.05, energy_from_food=20, cell=None
):
"""Initialize a wolf with higher energy needs than sheep."""
super().__init__(model, energy, p_reproduce, energy_from_food, cell)
# Wolves have higher metabolic rate (they're larger predators)
self.metabolic_rate = 1.0

def feed(self):
"""If possible, eat a sheep at current location."""
sheep = [obj for obj in self.cell.agents if isinstance(obj, Sheep)]
if sheep: # If there are any sheep present
if sheep: # Successful hunt
sheep_to_eat = self.random.choice(sheep)
# Eating gives instant energy boost
self.energy += self.energy_from_food
sheep_to_eat.remove()

def move(self):
"""Move to a neighboring cell, preferably one with sheep."""
cells_with_sheep = self.cell.neighborhood.select(
lambda cell: any(isinstance(obj, Sheep) for obj in cell.agents)
)
target_cells = (
cells_with_sheep if len(cells_with_sheep) > 0 else self.cell.neighborhood
)
self.cell = target_cells.select_random_cell()
# When hungry, actively hunt for sheep
if self.is_hungry: # Using computed property
cells_with_sheep = self.cell.neighborhood.select(
lambda cell: any(isinstance(obj, Sheep) for obj in cell.agents)
)
target_cells = (
cells_with_sheep
if len(cells_with_sheep) > 0
else self.cell.neighborhood
)
else:
# When not hungry, wander randomly (conserve energy)
target_cells = self.cell.neighborhood

self.cell = target_cells.select_random_cell()

class GrassPatch(FixedAgent):
"""A patch of grass that grows at a fixed rate and can be eaten by sheep."""

@property
def fully_grown(self):
"""Whether the grass patch is fully grown."""
return self._fully_grown
class GrassPatch(FixedAgent, HasObservables):
"""A patch of grass that grows at a fixed rate and can be eaten by sheep.

@fully_grown.setter
def fully_grown(self, value: bool) -> None:
"""Set grass growth state and schedule regrowth if eaten."""
self._fully_grown = value
Grass growth is modeled as a continuous process with a fixed regrowth time.
"""

if not value: # If grass was just eaten
self.model.simulator.schedule_event_relative(
setattr,
self.grass_regrowth_time,
function_args=[self, "fully_grown", True],
)
# Observable: grass growth state
fully_grown = Observable()

def __init__(self, model, countdown, grass_regrowth_time, cell):
"""Create a new patch of grass.
Expand All @@ -139,12 +207,25 @@ def __init__(self, model, countdown, grass_regrowth_time, cell):
cell: Cell to which this grass patch belongs
"""
super().__init__(model)
self._fully_grown = countdown == 0

self.fully_grown = countdown == 0
self.grass_regrowth_time = grass_regrowth_time
self.cell = cell

# Listen for when grass gets eaten, schedule regrowth
self.observe("fully_grown", "change", self._on_growth_change)

# Schedule initial growth if not fully grown
if not self.fully_grown:
self.model.simulator.schedule_event_relative(self._regrow, countdown)

def _on_growth_change(self, signal):
"""React to grass being eaten - schedule regrowth."""
if signal.new is False: # Grass was just eaten
self.model.simulator.schedule_event_relative(
setattr, countdown, function_args=[self, "fully_grown", True]
self._regrow, self.grass_regrowth_time
)

def _regrow(self):
"""Regrow the grass patch."""
self.fully_grown = True
45 changes: 36 additions & 9 deletions mesa/examples/advanced/wolf_sheep/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
Wolf-Sheep Predation Model
================================

Enhanced version with continuous energy depletion and reactive behaviors.

Replication of the model found in NetLogo:
Wilensky, U. (1997). NetLogo Wolf Sheep Predation model.
http://ccl.northwestern.edu/netlogo/models/WolfSheepPredation.
Expand All @@ -21,11 +23,16 @@
class WolfSheep(Model):
"""Wolf-Sheep Predation Model.

A model for simulating wolf and sheep (predator-prey) ecosystem modelling.
A model for simulating wolf and sheep (predator-prey) ecosystem with:
- Continuous energy depletion over time
- Reactive behaviors based on hunger levels
- Threshold-triggered events (death, starvation mode)
- Computed properties for decision making
"""

description = (
"A model for simulating wolf and sheep (predator-prey) ecosystem modelling."
"A model for simulating wolf and sheep (predator-prey) ecosystem modelling "
"with continuous energy dynamics and reactive behaviors."
)

def __init__(
Expand Down Expand Up @@ -55,12 +62,13 @@ def __init__(
wolf_gain_from_food: Energy a wolf gains from eating a sheep
grass: Whether to have the sheep eat grass for energy
grass_regrowth_time: How long it takes for a grass patch to regrow
once it is eaten
sheep_gain_from_food: Energy sheep gain from grass, if enabled
seed: Random seed
simulator: ABMSimulator instance for event scheduling
"""
super().__init__(seed=seed)

# Initialize time-based simulator for continuous energy dynamics
self.simulator = simulator
self.simulator.setup(self)

Expand All @@ -77,19 +85,32 @@ def __init__(
random=self.random,
)

# Set up data collection
# Set up data collection (tracks observable changes automatically)
model_reporters = {
"Wolves": lambda m: len(m.agents_by_type[Wolf]),
"Sheep": lambda m: len(m.agents_by_type[Sheep]),
"Avg Wolf Energy": lambda m: (
sum(w.energy for w in m.agents_by_type[Wolf])
/ len(m.agents_by_type[Wolf])
if len(m.agents_by_type[Wolf]) > 0
else 0
),
"Avg Sheep Energy": lambda m: (
sum(s.energy for s in m.agents_by_type[Sheep])
/ len(m.agents_by_type[Sheep])
if len(m.agents_by_type[Sheep]) > 0
else 0
),
}
Comment on lines +88 to 104
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Investigate RuntimeError: dictionary changed size during iteration in Wolf–Sheep example

The new continuous energy and threshold system interacts with:

  • Model reporters computing averages by iterating m.agents_by_type[Wolf] / m.agents_by_type[Sheep] and accessing w.energy / s.energy.
  • step() iterating activated agents via self.agents_by_type[Sheep].shuffle_do("step") and the corresponding call for Wolf.

Given that Animal._on_energy_depleted currently calls self.remove() from an energy threshold callback (see agents.py), removals can now be triggered:

  • While reporters iterate over agents_by_type[...] inside datacollector.collect(self), or
  • While the scheduler iterates agents in shuffle_do("step") and agent code accesses energy / is_hungry (which may fire thresholds).

Both patterns can cause RuntimeError: dictionary changed size during iteration, matching the pipeline failure at this file around the collect call.

Mitigations to consider (likely in concert with changes in Animal):

  • Ensure energy threshold callbacks do not directly remove agents while schedulers or reporters might be iterating; instead, mark agents for removal and let the scheduler or model perform removal at a safe point (e.g., at the start or end of step).
  • Alternatively, if the agent containers don’t already do this, ensure they iterate over a stable snapshot (list(...)) when performing shuffle_do or DataCollector traversals.

Please verify the exact call stack of the runtime error and align the removal strategy accordingly; it probably requires updating Animal._on_energy_depleted (see agents.py) rather than just this file.

Also applies to: 147-159


I'll investigate the codebase to verify the claims in this review comment about the potential dictionary iteration issue.


---

🏁 Script executed:

```shell
#!/bin/bash
# Find and examine the model.py file
find . -path "*/advanced/wolf_sheep/model.py" -type f | head -1 | xargs -I {} sh -c 'echo "=== File: {} ===" && cat {}'

Length of output: 5820


🏁 Script executed:

#!/bin/bash
# Find and examine agents.py in the same directory
find . -path "*/advanced/wolf_sheep/agents.py" -type f | head -1 | xargs -I {} sh -c 'echo "=== File: {} ===" && cat {}'

Length of output: 8345


🏁 Script executed:

#!/bin/bash
# Search for agents_by_type implementation and iteration methods
rg -n "agents_by_type" --type=py -A 3 -B 1 | head -100

Length of output: 6078


🏁 Script executed:

#!/bin/bash
# Search for _on_energy_depleted callback
rg -n "_on_energy_depleted" --type=py -A 5 -B 2

Length of output: 1149


🏁 Script executed:

#!/bin/bash
# Search for shuffle_do implementation
rg -n "shuffle_do" --type=py -A 5 -B 2

Length of output: 7633


I need to examine the complete shuffle_do implementation and how datacollector.collect() handles model reporters.


---

🏁 Script executed:

```shell
#!/bin/bash
# Get full shuffle_do implementation
rg -n "def shuffle_do" --type=py -A 15

Length of output: 967


🏁 Script executed:

#!/bin/bash
# Check DataCollector.collect() implementation
rg -n "def collect" --type=py -A 20 mesa/datacollection.py

Length of output: 1201


🏁 Script executed:

#!/bin/bash
# Check AgentSet iteration and _agents structure
rg -n "self\._agents" --type=py -B 2 -A 5 mesa/agent.py | head -150

Length of output: 5487


I found the vulnerability! Let me verify the exact timing of when threshold callbacks could fire during reporter iteration.


---

🏁 Script executed:

```shell
#!/bin/bash
# Check the experimental simulator and event scheduling
find . -path "*/experimental/devs*" -name "*.py" -type f | head -5 | xargs -I {} sh -c 'echo "=== File: {} ===" && head -100 {}'

Length of output: 8278


🏁 Script executed:

#!/bin/bash
# Check if there's any evidence of concurrent energy updates during collect()
rg -n "schedule_event" --type=py mesa/examples/advanced/wolf_sheep/ -B 2 -A 2

Length of output: 934


🏁 Script executed:

#!/bin/bash
# Verify AgentSet __iter__ returns keys() directly without snapshot
rg -n "def __iter__" --type=py mesa/agent.py -A 5

Length of output: 481


🏁 Script executed:

#!/bin/bash
# Search for any test reproducing the RuntimeError mentioned in review
rg -n "RuntimeError.*dictionary" --type=py

Length of output: 0


Fix model reporters to safely handle agent removal during iteration

The review comment is correct. The model reporters iterate directly over live AgentSet objects without snapshotting, making them vulnerable to RuntimeError: dictionary changed size during iteration when energy thresholds fire during collection.

The issue occurs at lines 88-104 (and 147-159) where lambdas like sum(w.energy for w in m.agents_by_type[Wolf]) iterate directly over an AgentSet. Since AgentSet.__iter__ returns an un-snapshotted WeakKeyDictionary.keys() iterator, and _on_energy_depleted calls self.remove(), the dictionary is modified mid-iteration.

Solution: Wrap all agent iterations in the model reporters with list() to snapshot the collection before iteration:

model_reporters = {
    "Wolves": lambda m: len(m.agents_by_type[Wolf]),
    "Sheep": lambda m: len(m.agents_by_type[Sheep]),
    "Avg Wolf Energy": lambda m: (
        sum(w.energy for w in list(m.agents_by_type[Wolf]))
        / len(list(m.agents_by_type[Wolf]))
        if len(m.agents_by_type[Wolf]) > 0
        else 0
    ),
    "Avg Sheep Energy": lambda m: (
        sum(s.energy for s in list(m.agents_by_type[Sheep]))
        / len(list(m.agents_by_type[Sheep]))
        if len(m.agents_by_type[Sheep]) > 0
        else 0
    ),
}

Apply the same fix to lines 147–159 and any other direct iterations over agents_by_type.

🤖 Prompt for AI Agents
In mesa/examples/advanced/wolf_sheep/model.py around lines 88–104 (and similarly
147–159), the model_reporters iterate directly over live AgentSet objects which
can be mutated during reporting causing RuntimeError; fix by snapshotting the
agent collections with list(...) before iterating or computing lengths (use
list(m.agents_by_type[Wolf]) and list(m.agents_by_type[Sheep]) in the sum and
len calls) and apply the same change to the reporters at lines 147–159 and any
other places that iterate over m.agents_by_type without first converting to a
list.


if grass:
model_reporters["Grass"] = lambda m: len(
m.agents_by_type[GrassPatch].select(lambda a: a.fully_grown)
)

self.datacollector = DataCollector(model_reporters)

# Create sheep:
# Create sheep with random initial energy
Sheep.create_agents(
self,
initial_sheep,
Expand All @@ -98,7 +119,8 @@ def __init__(
energy_from_food=sheep_gain_from_food,
cell=self.random.choices(self.grid.all_cells.cells, k=initial_sheep),
)
# Create Wolves:

# Create wolves with random initial energy
Wolf.create_agents(
self,
initial_wolves,
Expand All @@ -123,10 +145,15 @@ def __init__(
self.datacollector.collect(self)

def step(self):
"""Execute one step of the model."""
# First activate all sheep, then all wolves, both in random order
"""Execute one step of the model.

Energy continuously depletes between steps via ContinuousObservable.
This step method only triggers agent decisions and actions.
"""
# Activate all sheep, then all wolves, both in random order
# Their energy has been continuously depleting since last step
self.agents_by_type[Sheep].shuffle_do("step")
self.agents_by_type[Wolf].shuffle_do("step")

# Collect data
# Collect data (automatically captures current energy levels)
self.datacollector.collect(self)
10 changes: 9 additions & 1 deletion mesa/experimental/mesa_signals/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,21 @@
when modified.
"""

from .mesa_signal import All, Computable, Computed, HasObservables, Observable
from .mesa_signal import (
All,
Computable,
Computed,
ContinuousObservable,
HasObservables,
Observable,
)
from .observable_collections import ObservableList

__all__ = [
"All",
"Computable",
"Computed",
"ContinuousObservable",
"HasObservables",
"Observable",
"ObservableList",
Expand Down
Loading
Loading