Skip to content

Conversation

@EwoutH
Copy link
Member

@EwoutH EwoutH commented Dec 5, 2025

This is a pathfinding PR for #2921, under active development. The goal is to explore the structure, design and API, and gather feedback before finalizing. Breaking changes may occur as we iterate.

Summary

This PR introduces a unified API for time advancement and event scheduling, integrating these capabilities directly into the Model class. It represents the first implementation step toward the design outlined in #2921.

Motive

Currently, Mesa's experimental discrete event simulation requires users to manage two separate objects:

simulator = ABMSimulator()
model = MyModel()
simulator.setup(model)
simulator.schedule_event_absolute(callback, time=50)
simulator.run_until(100)

This raises unnecessary questions: Why do I need a simulator? What's the difference between ABMSimulator and DEVSimulator? What does setup() do? For most users building standard ABMs, these questions shouldn't exist.

The deeper motivation is enabling a smooth progression from simple step-based models to full discrete-event simulation without rewriting code. A user should be able to:

  1. Start with just step() and run(steps=100)
  2. Add scheduled events when needed (model.schedule(drought, at=50))
  3. Have agents schedule their own events (self.model.schedule(self.release, after=sentence))
  4. Eventually build pure event-driven models with no regular stepping

All using the same unified API, without switching between different simulator classes or paradigms.

Implementation

The implementation uses composition to keep concerns separated:

  • Scheduler (new class in scheduler.py): Owns the EventList and implements all scheduling/execution logic
  • Model: Adds thin delegation methods (schedule(), run(), cancel()) that forward to the internal Scheduler
  • Simulator classes: Deprecated but still functional, now delegate to Model's API

This design keeps Model focused (~50 new lines) while the Scheduler contains the complexity (~150 lines). The Scheduler can be tested independently and potentially subclassed for custom behavior.

Usage Examples

API is non-final, we're currently exploring both keyword argument and separate method APIs. This implemenation uses keyword arguments.

Simple step-based model (unchanged):

class MyModel(Model):
    def step(self):
        self.agents.shuffle_do("step")

model = MyModel()
model.run(steps=100)

Adding scheduled events:

class DroughtModel(Model):
    def __init__(self):
        super().__init__()
        self.schedule(self.drought, at=50.0)  # One-off event
    
    def step(self):
        self.agents.shuffle_do("step")
    
    def drought(self):
        self.grass_level *= 0.5

model = DroughtModel()
model.run(until=100)

Migration from old API:

# Before
simulator = ABMSimulator()
model = WolfSheep()
simulator.setup(model)
simulator.schedule_event_absolute(callback, time=50)
simulator.run_until(100)

# After
model = WolfSheep()
model.schedule(callback, at=50)
model.run(until=100)

Additional Notes

What this PR does NOT include (yet):

  • The recurring parameter for automatic method scheduling (needs more design discussion)
  • Decorator-based scheduling (@scheduled)
  • Changes to DataCollector or visualization integration

Open questions for reviewers:

  1. Is the composition approach (Model → Scheduler) the right separation?
  2. Should _scheduler be public (scheduler) for advanced users?
  3. How far do we want to go in providing backwards compatibility?

Related:

@github-actions
Copy link

github-actions bot commented Dec 5, 2025

Performance benchmarks:

Model Size Init time [95% CI] Run time [95% CI]
BoltzmannWealth small 🟢 -7.4% [-8.7%, -5.5%] 🟢 -3.5% [-3.6%, -3.4%]
BoltzmannWealth large 🟢 -48.6% [-59.8%, -37.0%] 🟢 -39.5% [-48.4%, -30.0%]
Schelling small 🔵 -2.5% [-2.7%, -2.4%] 🟢 -4.0% [-4.2%, -3.8%]
Schelling large 🟢 -14.3% [-22.0%, -10.1%] 🟢 -7.8% [-13.4%, -3.4%]
WolfSheep small 🟢 -5.4% [-5.6%, -5.1%] 🔵 -0.5% [-0.7%, -0.4%]
WolfSheep large 🟢 -81.7% [-92.9%, -66.0%] 🟢 -42.1% [-56.2%, -25.6%]
BoidFlockers small 🟢 -5.7% [-6.3%, -5.0%] 🔵 -2.0% [-2.1%, -1.9%]
BoidFlockers large 🔵 +352.8% [-5.5%, +898.1%] 🔵 -2.1% [-2.3%, -1.8%]

This commit introduces a unified time and event scheduling API directly into the Model class, eliminating the need for separate Simulator objects.

The core insight is that users shouldn't need to manage two objects (Model and Simulator) to run a simulation. By integrating scheduling into Model, we get a cleaner API: model.run(until=100) instead of creating a simulator, calling setup(), then run_until().

Implementation uses composition: a new Scheduler class handles all event scheduling and execution logic, while Model provides thin delegation methods. This keeps Model focused on agents and state while Scheduler handles time.

Key changes:
- Add Scheduler class in mesa/experimental/devs/scheduler.py
- Add Model.schedule() for event scheduling (at= or after= syntax)
- Add Model.run() with multiple termination options
- Add Model.cancel() for canceling scheduled events
- Deprecate ABMSimulator/DEVSimulator (still functional with warnings)

Part of the unified time/event scheduling proposal (projectmesa#2921).
Introduce `@scheduled` decorator to explicitly mark methods for recurring execution during model runs. This replaces the implicit auto-scheduling of `step()` methods.

Key changes:
- Add `@scheduled(interval=1.0)` decorator in `scheduler.py`
- Model scans for decorated methods after `__init__` completes via `__new__` wrapper
- Undecorated `step()` methods still work but emit `FutureWarning`
- Deprecate `run_model()` in favor of `model.run(condition=...)`
- Deprecate `model.steps` counter (use `model.time` instead)
- Remove `_step_duration` attribute (unused, never released)
@EwoutH
Copy link
Member Author

EwoutH commented Dec 5, 2025

model.step() and model.steps is deeply engrained in things like batch_run and the DataCollector. This might be a 4.0 change.

A strategy might be moving the dependence to model.time before phasing out model.steps.

@EwoutH
Copy link
Member Author

EwoutH commented Dec 5, 2025

Cleaner attempt at implementing @scheduled:

Explicitly kept two code paths: One for where model.step() doesn't have a decorator, and one for where it does.

Still not perfect, but I think more in the right direction.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant