Skip to content

Add event scheduling and time progression API v2#3188

Closed
EwoutH wants to merge 21 commits intomesa:mainfrom
EwoutH:timeflow_schedule
Closed

Add event scheduling and time progression API v2#3188
EwoutH wants to merge 21 commits intomesa:mainfrom
EwoutH:timeflow_schedule

Conversation

@EwoutH
Copy link
Copy Markdown
Member

@EwoutH EwoutH commented Jan 20, 2026

Based on the discussions and accumulating insights in #3155 and #2921 (comment), a new implementation that implements the single schedule() method.

Builds on #3155. Can either be directly merged or after #3155 (I recommend former).

What's quite cool is that it schedules a model.step() event every 1 time. So like a ABMSimulator by default. Like a hearth beat. Can easily be turned off for full DEVS.

That also makes it super super backwards compatible.

Design decisions

I started from the following design decisions:

  • We want a single, powerful schedule function. Utility functions come second and syntactic suger like decorators third.
  • Same goes for run: Powerful run function, then utility functions.
  • The RecurringEvent is a very useful concept. It should have methods to pause, resume and stop.
  • The step is going to be the hearth beat of Mesa. This keeps backwards compatibility and a familiar way for basic models, with the only difference for them being to run a model with run() instead of repeated step calls. Internally, it's just a scheduled function with interval 1, and can be overwritten.
  • The model.schedule() method returns the Event or RecurringEvent object directly, which in case of Recurring has methods like pause(), resume(), and cancel() for user control.
  • Users who need to modify or cancel scheduled events simply keep a reference to the returned object (e.g., self.trading = model.schedule(self.trade, interval=1)), while one-off events that don't need later control can ignore the return value.
  • The default step method is automatically scheduled as a RecurringEvent with interval=1 and stored in self.step_event, making it easy to introspect or modify the model's heartbeat.
  • We're not implementing a central registry of central events for now, users manage their own event references as needed.

Methods

Some methods

# Schedule method
model.schedule(
    callback,
    start_at=None,      # Absolute time to start
    start_after=None,   # Delay before starting  
    interval=None,      # Time between recurrences (None = one-off, number or callable)
    count=None,         # Max executions (None = infinite for recurring, 1 for one-off)
    end_at=None,        # Absolute time to stop
    end_after=None,     # Duration to run (from first execution)
    priority=Priority.DEFAULT,
    args=None,
    kwargs=None,
) -> SimulationEvent | RecurringEvent

Return Values

  • One-off events return SimulationEvent
  • Recurring events return RecurringEvent with pause(), resume(), stop() methods

Default Step Behavior

  • Model.__init__ automatically schedules self.step as a RecurringEvent with interval=1
  • Stored in self.step_event for user access/modification
  • Users override step() method, the scheduling is automatic

Convenience Methods

  • model.schedule_at(callback, time, ...) → wraps schedule(start_at=time)
  • model.schedule_after(callback, delay, ...) → wraps schedule(start_after=delay)

Run Methods

  • model.run_for(duration) - run for N time units
  • model.run_until(end_time) - run until absolute time
  • model.run_while(condition) - run while condition is true
  • model.run_next_event() - single event execution (debugging)

TODO

  • Fix recurring event pausing
  • Update PR description
  • Update migration guide

Follow-up PRs:

  • Deprecate simulators
  • Update example models which use simulators
  • Update tutorial

(please do not update, rebase or touch my branch in any way)

@EwoutH EwoutH added the feature Release notes label label Jan 20, 2026
EwoutH added 21 commits January 20, 2026 22:53
Introduces clean scheduling and run control methods directly on Model, providing a unified interface for both traditional time-step advancement and discrete event simulation.

New Model methods:
- schedule_at(callback, time): Schedule event at absolute time
- schedule_after(callback, delay): Schedule event after delay
- cancel_event(event): Cancel a scheduled event
- run_until(end_time): Run until specific time
- run_for(duration): Run for specific duration
- run_while(condition): Run while condition is true
- run_next_event(): Execute next scheduled event

Implementation:
- Creates timeflow.py with Scheduler and RunControl classes
- Model delegates to internal _scheduler and _run_control instances
- Modifies _wrapped_step to use run_for(1) internally
- Non-breaking: existing step-based models work unchanged

This lays the foundation for more advanced features like recurring methods and agent self-scheduling while maintaining full backward compatibility.
Introduces a @scheduled decorator to mark model methods for automatic recurring scheduling. The Scheduler now detects and schedules these methods at specified intervals and priorities, simplifying periodic event management in Mesa models.
Refactored the @scheduled decorator to support usage with and without parentheses. Updated Model initialization to check for scheduled step methods and avoid double-wrapping. Modified Scheduler to detect scheduled methods from the class definition and properly schedule bound instance methods. Enhanced recurring event scheduling and rescheduling logic in RunControl and Scheduler for more robust simulation event handling.
Introduces a new test suite for the unified time and event scheduling API, covering Scheduler, RunControl, the @scheduled decorator, Model integration, and various edge cases. These tests verify correct event scheduling, priorities, cancellation, model run methods, agent self-scheduling, and error handling.
Introduces the RecurringEvent class to handle events that automatically reschedule themselves after execution. Updates the Scheduler and RunControl logic to use RecurringEvent for scheduled methods, simplifying recurring event management and removing manual rescheduling code.
Added a PendingDeprecationWarning to discourage calling model.step() directly in a loop, guiding users to use the new @scheduled decorator and run methods introduced in Mesa 3.5. Updated the migration guide with detailed instructions and examples for adopting the unified time API.
Replaces direct import of Scheduler with a conditional import under TYPE_CHECKING to avoid runtime dependencies and improve type hinting.
ModelController and SimulatorController now detect if the model uses the new @scheduled API and call run_for(1) instead of step(). This ensures compatibility with both traditional and unified model stepping methods.
Reorganized and consolidated import statements in mesa/experimental/devs/eventlist.py for improved readability and to avoid redundant imports.
Replaces usage of ABMSimulator with direct scheduling via model methods and the @scheduled decorator. Updates agent scheduling and test code to reflect the new approach.
Simplifies the run_model function by removing conditional logic for ABMSimulator and using model.run_for for all models.
We can later decide how to deprecate exactly.
@EwoutH EwoutH force-pushed the timeflow_schedule branch from 110bc54 to 647affb Compare January 20, 2026 21:55
@github-actions
Copy link
Copy Markdown

Performance benchmarks:

Model Size Init time [95% CI] Run time [95% CI]
BoltzmannWealth small 🔴 +5.5% [+4.5%, +6.4%] 🔴 +5.4% [+5.2%, +5.5%]
BoltzmannWealth large 🔵 -4.9% [-11.1%, +0.6%] 🟢 -9.0% [-11.0%, -7.0%]
Schelling small 🔵 +2.7% [+2.4%, +3.1%] 🔵 +0.6% [+0.5%, +0.8%]
Schelling large 🔵 -6.0% [-10.0%, -2.3%] 🔵 -3.9% [-5.6%, -2.1%]
WolfSheep small 🔵 -0.8% [-2.5%, +0.7%] 🔵 +1.8% [-3.8%, +8.4%]
WolfSheep large 🟢 -18.8% [-25.9%, -11.6%] 🔵 +2.9% [+1.8%, +4.0%]
BoidFlockers small 🔵 +1.7% [+1.1%, +2.3%] 🔵 +0.3% [+0.1%, +0.6%]
BoidFlockers large 🔵 +2.5% [+2.1%, +3.1%] 🔵 +0.5% [+0.2%, +0.8%]

@EwoutH
Copy link
Copy Markdown
Member Author

EwoutH commented Jan 20, 2026

If you think, +1800 lines of code, what are you smoking?

  • over half are tests
  • another few hunderd docs
  • a huge slew of backwards compatibility stuff (both for increasing time with step() and the simulators)
  • Functional code is about 500 lines (with a lot of inline docs, maybe too much), which is about the same as simulators we're going to remove.

@quaquel
Copy link
Copy Markdown
Member

quaquel commented Jan 21, 2026

I am sorry, but I don't like this API. Notions such as start_at and end_at only make sense for recurring events. I find them confusing for scheduling individual events where at or after make sense, and the other arguments don't matter. So, to me, it would make sense to at least separate schedule and schedule _repeatedly. Also, picking up on the idea that recurring events are processes, would it not make more sense to have start_at and start_from. This is also better aligned with end_at and end_after.

@EwoutH
Copy link
Copy Markdown
Member Author

EwoutH commented Jan 21, 2026

Thanks for your perspective. It may indeed make sense to split into schedule and schedule_recurring, especially since they return different objects.

@EwoutH
Copy link
Copy Markdown
Member Author

EwoutH commented Jan 21, 2026

Something else: I’m a bit in doubt if RecurringEvent is the right abstraction, or it should be something like a EventGenerator that just schedules normal events.

@quaquel
Copy link
Copy Markdown
Member

quaquel commented Jan 21, 2026

Something else: I’m a bit in doubt if RecurringEvent is the right abstraction, or it should be something like a EventGenerator that just schedules normal events.

Good question. The name is clear, but adding methods like start and stop makes it more than just a recurring event. Generator does not really capture this either. So, I don't know yet.

@EwoutH
Copy link
Copy Markdown
Member Author

EwoutH commented Jan 21, 2026

Thanks for the insights. To be continued in:

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.

2 participants