Skip to content

Add EventGenerator class for recurring event patterns#3201

Merged
EwoutH merged 2 commits intomesa:mainfrom
EwoutH:EventGenerator
Jan 24, 2026
Merged

Add EventGenerator class for recurring event patterns#3201
EwoutH merged 2 commits intomesa:mainfrom
EwoutH:EventGenerator

Conversation

@EwoutH
Copy link
Copy Markdown
Member

@EwoutH EwoutH commented Jan 24, 2026

I was doing too much stuff at the same time. Let's break it down in a few smaller PRs.

This is (in my opinion) a crucial and obvious building block: A EventGenerator class that generates events in a certain pattern.

Summary

Adds an EventGenerator class to the discrete event simulation module, enabling recurring event patterns as a first-class concept separate from one-off events.

Motive

The current event scheduling system only supports single events via SimulationEvent. For recurring behaviors (like data collection every N time units, or Poisson arrivals), users must manually reschedule events after each execution. This PR introduces EventGenerator to handle recurring patterns cleanly.

Implementation

  • Added EventGenerator class to eventlist.py
  • Supports fixed intervals (interval=5.0) or callable intervals (interval=lambda m: m.random.expovariate(2.0))
  • start(at, after) controls when the pattern begins
  • stop(at, after, count) controls when the pattern ends
  • Tracks execution count and active state
  • Uses weak references internally through SimulationEvent
  • All methods return self for chaining: generator.start(at=0).stop(count=10)

Usage Examples

# Fixed interval pattern
gen = EventGenerator(model, callback, interval=5.0)
gen.start(at=0.0).stop(count=100)

# Stochastic arrivals
gen = EventGenerator(model, self.customer_arrival, interval=lambda m: m.random.expovariate(2.0))
gen.start()

# Including arguments and keyword arguments
from functools import partial
EventGenerator(model, partial(fn, "a", k="v"), interval=1.0)

Additional Notes

This is part of a larger effort to improve Mesa's scheduling API (#2921). Future work will integrate EventGenerator further, potentially into the Model class via model.create_pattern() and/or auto-schedule model.step() as a pattern.

Add EventGenerator class for recurring event patterns in the experimental discrete event simulation.

EventGenerator creates recurring events at specified intervals, supporting both fixed and callable intervals for stochastic processes. It provides start(at, after) and stop(at, after, count) methods for flexible control over when patterns begin and end, with method chaining support. This is a first step toward a cleaner scheduling API that distinguishes between one-off events and recurring patterns.
@EwoutH EwoutH requested a review from quaquel January 24, 2026 15:17
@EwoutH EwoutH added feature Release notes label experimental Release notes label labels Jan 24, 2026
@github-actions
Copy link
Copy Markdown

Performance benchmarks:

Model Size Init time [95% CI] Run time [95% CI]
BoltzmannWealth small 🔵 -1.6% [-2.5%, -0.8%] 🔵 +1.1% [+1.0%, +1.3%]
BoltzmannWealth large 🔵 +4.9% [-6.8%, +16.5%] 🔵 -1.8% [-6.4%, +2.0%]
Schelling small 🔵 -0.2% [-0.7%, +0.2%] 🔵 +1.7% [+1.5%, +1.8%]
Schelling large 🔵 +1.7% [-4.4%, +7.4%] 🔵 -0.2% [-2.0%, +1.3%]
WolfSheep small 🔵 +1.5% [-1.3%, +4.6%] 🔵 +1.4% [+1.0%, +1.8%]
WolfSheep large 🔵 +0.3% [-13.8%, +15.9%] 🔵 +0.8% [-1.2%, +3.0%]
BoidFlockers small 🔵 -0.5% [-1.0%, -0.1%] 🔵 -0.1% [-0.3%, +0.1%]
BoidFlockers large 🔵 +2.0% [+1.4%, +2.6%] 🔵 +0.5% [+0.1%, +1.0%]

self,
model: Model,
function: Callable,
interval: float | int | Callable[[Model], float | int],
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.

I don't understand the typing of the callable here. Why would it need a number next to model?

Copy link
Copy Markdown
Member Author

@EwoutH EwoutH Jan 24, 2026

Choose a reason for hiding this comment

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

Sharp eye. This is intended, the Model is passed (and typed) so you can access the random number generator, like for:

# Poisson arrival
EventGenerator(model, fn, interval=lambda m: m.random.expovariate(2.0))

Comment on lines +152 to +153
function_args: list[Any] | None = None,
function_kwargs: dict[str, Any] | None = None,
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.

I understand doing this, although with functools.partial we can also ofload this to a python library.

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.

Thanks for the swift review.

Would the API them become like this?

EventGenerator(model, partial(fn, "a", k="v"), interval=1.0)

Copy link
Copy Markdown
Member

@quaquel quaquel left a comment

Choose a reason for hiding this comment

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

Looks good to me. Clean API, well-documented and tested.

@EwoutH EwoutH merged commit e741084 into mesa:main Jan 24, 2026
21 of 22 checks passed
EwoutH added a commit that referenced this pull request Feb 9, 2026
Replaces the hand-rolled step rescheduling loop in `Model` with an internal `EventGenerator`, simplifying the code while keeping all behavior and API identical.

The current step scheduling uses a manual pattern: `_do_step()` calls `_schedule_step()`, which creates a `SimulationEvent` that calls `_do_step()` again. This is functionally an `EventGenerator` — but reimplemented ad-hoc. Now that `EventGenerator` exists (#3201), we should use it.

This also removes `_schedule_step()` entirely and simplifies both `_do_step()` and `_wrapped_step()`, reducing the amount of scheduling logic in `Model`.
EwoutH added a commit that referenced this pull request Feb 11, 2026
### Summary
Exposes event scheduling and time advancement methods directly on `Model`, giving users a public API for scheduling events and advancing simulation time without needing to interact with the `Simulator` class.

Basically, these methods allow us:
- to replace all existing behavior (in examples, tutorials, etc.)
- start deprecation all current scheduling and stepping APIs
- remove them in Mesa 4.

### Motive
Currently, users advance their models by calling `model.step()`, which is tightly coupled to the concept of a single discrete step. With the event-based architecture taking shape (#3201, #3204), we need public methods that let users think in terms of *time* rather than *steps*.

The key method here is `model.run_for(duration)`. For existing ABM users, `model.run_for(1)` is functionally equivalent to `model.step()`: it advances time by 1 unit, which triggers the scheduled step event. But unlike `step()`, it generalizes naturally: `run_for(0.5)` advances half a time unit, `run_for(100)` advances 100 units processing all scheduled events along the way. This makes `run_for` the foundation for both traditional ABMs and event-driven models.

This is also a stepping stone toward deprecating `model.step()` as the primary way to advance a model. Instead of calling a method that *is* a step, users call a method that *advances time*, and steps happen as scheduled events within that time. The mental model shifts from "execute step N" to "advance time, and whatever is scheduled will run."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

experimental Release notes label feature Release notes label

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants