Skip to content

Add public event scheduling and time advancement methods#3266

Merged
EwoutH merged 3 commits intomesa:mainfrom
EwoutH:schedule_run
Feb 11, 2026
Merged

Add public event scheduling and time advancement methods#3266
EwoutH merged 3 commits intomesa:mainfrom
EwoutH:schedule_run

Conversation

@EwoutH
Copy link
Copy Markdown
Member

@EwoutH EwoutH commented Feb 9, 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."

Implementation

Four new public methods on Model:

  • schedule_event(function, *, at, after, priority): schedule a one-off event at an absolute or relative time
  • schedule_recurring(function, schedule, priority): schedule a recurring event using a Schedule object, returns an EventGenerator
  • run_for(duration): advance time by the given duration, processing all events
  • run_until(end_time): advance time to an absolute point, processing all events

All four are thin wrappers around existing internal machinery (_event_list, _advance_time, EventGenerator).

Model also stores strong references to active EventGenerators in _event_generators to prevent garbage collection — SimulationEvent wraps bound methods in WeakMethod, so without this a fire-and-forget schedule_recurring() call would silently stop working.

Testing

Tests cover run_for, run_until, schedule_event (at/after/cancel/validation), and schedule_recurring (fixed interval/stop/count). Includes an explicit gc.collect() regression test for the WeakMethod lifetime issue.

Usage Examples

Drop-in replacement for model.step():

# Old
for _ in range(100):
    model.step()

# New — functionally identical
for _ in range(100):
    model.run_for(1)

# Or simply
model.run_for(100)

Schedule custom events alongside steps:

class MyModel(mesa.Model):
    def __init__(self):
        super().__init__()
        # Schedule a one-off event
        self.schedule_event(self.market_crash, at=50.0)

        # Schedule recurring data export every 10 time units
        self.schedule_recurring(
            self.export_data,
            Schedule(interval=10.0, start=10.0),
        )

    def step(self):
        self.agents.shuffle_do("step")

Run to a specific time:

model = MyModel()
model.run_until(500.0)  # run until t=500, executing all scheduled events

Additional Notes

  • No breaking changes, model.step() continues to work exactly as before
  • run_for(1) and step() produce identical results for standard ABMs, since both advance time by 1 unit and trigger the same scheduled step event
  • schedule_event and schedule_recurring return their event/generator objects, allowing cancellation via .cancel() / .stop()
  • These methods work both with and without a Simulator attached

One we agree on the API I will add tests. In separate PRs we can update examples and tutorials and deprecate old API surfaces.

@EwoutH EwoutH added the feature Release notes label label Feb 9, 2026
@EwoutH EwoutH mentioned this pull request Feb 9, 2026
42 tasks
@github-actions
Copy link
Copy Markdown

github-actions bot commented Feb 9, 2026

Performance benchmarks:

Model Size Init time [95% CI] Run time [95% CI]
BoltzmannWealth small 🔵 +0.6% [-0.1%, +1.2%] 🔵 -0.4% [-0.5%, -0.2%]
BoltzmannWealth large 🔵 -0.9% [-2.5%, +0.3%] 🔵 -2.5% [-7.2%, +1.5%]
Schelling small 🔵 +0.8% [+0.4%, +1.2%] 🔵 +1.2% [+1.0%, +1.4%]
Schelling large 🔵 +0.3% [-1.0%, +1.7%] 🔵 +0.2% [-2.6%, +3.0%]
WolfSheep small 🔵 -2.1% [-2.6%, -1.6%] 🔵 -0.3% [-0.4%, -0.1%]
WolfSheep large 🔵 +0.1% [-1.5%, +1.8%] 🔵 +1.1% [-2.3%, +4.9%]
BoidFlockers small 🔵 +1.7% [+1.3%, +2.3%] 🔵 +0.2% [-0.1%, +0.4%]
BoidFlockers large 🔵 +1.7% [+1.3%, +2.0%] 🔵 -0.0% [-0.2%, +0.1%]

agent.remove()

### Event scheduling and time progression methods ###
def schedule_event(
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 can accept this design, although you know my preference to separate them into schedule_event_at and schedule_event_after.

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.

The coin fall to your liking with run_for and run_until ;)

On a bit more serious note, I went for this way because schedule_event(something, at=2.5, after=1) can make some conceptual sense (in this example firing two events), while run(for=1, untill=2.5) is by definition mutually exclusive (this regardless from if we want to enable it or not).

But I'm happy to follow the majority here.

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.

This is a tough one. I like schedule_event ever so slightly better, because it feels more self contained.

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.

API wise this looks good to me.

@EwoutH
Copy link
Copy Markdown
Member Author

EwoutH commented Feb 10, 2026

Found and fixed a subtle bug while writing these tests: schedule_recurring returns an EventGenerator, but if the user doesn't save it (which is completely natural, fire-and-forget recurring events), it gets garbage collected. This happens because SimulationEvent wraps bound methods in WeakMethod, and once the generator is GC'd the weak reference dies silently.

Fix is two lines: Model stores generators in self._event_generators. Added a test that forces gc.collect() after a fire-and-forget call to prevent regression.

@EwoutH
Copy link
Copy Markdown
Member Author

EwoutH commented Feb 10, 2026

@mesa/maintainers please critically review the API and functionality. This is an API we're going to use a lot!

@EwoutH
Copy link
Copy Markdown
Member Author

EwoutH commented Feb 10, 2026

If no objections, I would like to merge this tomorrow and move forward.

@jackiekazil
Copy link
Copy Markdown
Member

I like these updates.

@jackiekazil
Copy link
Copy Markdown
Member

I will add one thing -- I think we need a more formal process for proposing such changes -- like a write up.
This worked out well, but I don't think everyone tracks all progress all the time -- a formal process would allow for a period of feedback.

@EwoutH
Copy link
Copy Markdown
Member Author

EwoutH commented Feb 11, 2026

Introduce event scheduling and time-progression APIs on Model. Adds schedule_event (one-off events with at/after validation), schedule_recurring (creates and starts an EventGenerator from a Schedule), run_for (advance by duration) and run_until (advance to absolute time). Update imports to include Callable and additional eventlist types (EventGenerator, Schedule, SimulationEvent, Priority). These helpers expose existing event-list functionality through a simpler public interface.
schedule_recurring() returns an EventGenerator whose _execute_and_reschedule method is wrapped in a WeakMethod by SimulationEvent. If the user doesn't save the returned generator (fire-and-forget), it gets garbage collected and the WeakMethod silently resolves to None — recurring events never fire.

The fix: Model holds strong references to generators in _event_generators so they stay alive for the duration of the simulation.
Covers run_for, run_until, schedule_event, and schedule_recurring.

Includes explicit GC test (test_fire_and_forget_survives_gc) to prevent regression on the WeakMethod lifetime bug.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature Release notes label

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants