Skip to content

Update wolf-sheep example to use new event scheduling API#3278

Merged
EwoutH merged 4 commits intomesa:mainfrom
EwoutH:wolf_sheep_time_api
Feb 11, 2026
Merged

Update wolf-sheep example to use new event scheduling API#3278
EwoutH merged 4 commits intomesa:mainfrom
EwoutH:wolf_sheep_time_api

Conversation

@EwoutH
Copy link
Copy Markdown
Member

@EwoutH EwoutH commented Feb 11, 2026

Summary

Updates the wolf-sheep example to use the new public event scheduling API (model.schedule_event()) instead of the deprecated ABMSimulator, and refactors GrassPatch for cleaner, more Pythonic code.

Implementation

The main change is migrating from simulator.schedule_event_relative() to model.schedule_event(after=...), and removing the old simulator objects.

agents.py also received some refactoring. The old GrassPatch implementation used a property setter with side effects—setting fully_grown = False would automatically schedule a regrowth event using setattr as a callback. This was clever but opaque. The new implementation adds explicit regrow() and eat() methods that make the grass lifecycle clear: when eaten, grass schedules its own regrowth. The Sheep.feed() method now calls grass_patch.eat() instead of directly assigning to the property.

Grass is now regrowing:
Screenshot_787

Additional Notes

Updates the wolf-sheep example to use the new public event scheduling API (`model.schedule_event()`) instead of the deprecated `ABMSimulator`.

- **agents.py**: Replace `simulator.schedule_event_relative()` with `model.schedule_event(after=...)`
- **model.py**: Remove `simulator` parameter and setup
- **app.py**: Remove simulator initialization and passing

The model behavior remains functionally identical - grass regrowth events are still scheduled at the same times, just using the new API.
instead of trying to be clever with property setters, just make explicit methods for the two state transitions (eating and regrowing).
@EwoutH EwoutH requested a review from quaquel February 11, 2026 13:20
@EwoutH EwoutH added the example Changes the examples or adds to them. label Feb 11, 2026
"""Regrow the grass."""
self.fully_grown = True

def eat(self):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

A pit of a nitpick, but this naming is not ideal. it suggest the grass eats, rather than the grass being eaten.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Shall I rename to get_eaten()?

@EwoutH
Copy link
Copy Markdown
Member Author

EwoutH commented Feb 11, 2026

Not for this PR, but I don't know if the current example displays desired behavior. There's a grass growth spike around t=30 caused by synchronization: while initial grass patches get random regrowth countdowns (0-30), all grass eaten by sheep regrows after exactly 30 time units.

This creates synchronized waves: the initial random grass all finishes around t=30, sheep eat it, then it all regrows together 30 steps later at t=60, creating cascading population spikes. If smoother dynamics are desired, the get_eaten() method should use self.model.rng.integers(1, self.grass_regrowth_time + 1) instead of the fixed self.grass_regrowth_time to maintain randomized regrowth throughout the simulation.

Screenshot_787

@quaquel
Copy link
Copy Markdown
Member

quaquel commented Feb 11, 2026

This creates synchronized waves: the initial random grass all finishes around t=30, sheep eat it, then it all regrows together 30 steps later at t=60, creating cascading population spikes. If smoother dynamics are desired, the get_eaten() method should use self.model.rng.integers(1, self.grass_regrowth_time + 1) instead of the fixed self.grass_regrowth_time to maintain randomized regrowth throughout the simulation.

It makes sense to use some distribution for this. I would not use integers, because that's uniform. But a triangular distribution or so, and flooring the number to an integer should work and create nicer dynamics.

@EwoutH
Copy link
Copy Markdown
Member Author

EwoutH commented Feb 11, 2026

The exact same happens on main:

image

The random initialization spreads out the first regrowth, but doesn't prevent synchronization from emerging through the deterministic 30-step regrowth after eating.

@EwoutH
Copy link
Copy Markdown
Member Author

EwoutH commented Feb 11, 2026

Really nice how this PR also cleans up our test and benchmark code.

@EwoutH EwoutH merged commit 4375948 into mesa:main Feb 11, 2026
19 of 20 checks passed
@quaquel
Copy link
Copy Markdown
Member

quaquel commented Feb 11, 2026

The random initialization spreads out the first regrowth, but doesn't prevent synchronization from emerging through the deterministic 30-step regrowth after eating.

I initially misread your comment and then updated it afterwards.

@EwoutH EwoutH mentioned this pull request Feb 11, 2026
42 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

example Changes the examples or adds to them.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants