Proposal: Unified Time and Event Scheduling API #2921
Replies: 12 comments 76 replies
-
An alternative API could be using a decorator: class MyModel(Model):
...
@recurring
def step(self):
self.agents.shuffle_do("step")
@recurring(interval=7)
def weekly_update(self):
self.collect_stats()
def helper_method(self): # Not recurring, no decorator
passExisting models only have to add |
Beta Was this translation helpful? Give feedback.
-
|
Thanks for this. I have been thinking about cleaning up the event scheduling and integrating it better into MESA as well, but you have already done most of the work. Broadly speaking, I endorse the ideas outlined. At long last, it adds proper control for running models to MESA (i.e., run for x steps, run until a given time, etc.). Just a few quick reactions.
At the moment, DEVSSimulator has the following methods
Next to the methods, there is also a check to ensure that valid time units are used. DEVS allows floats and integers; ABM only integers. Lastly, the def __init__(
self,
*args: Any,
seed: float | None = None,
rng: RNGLike | SeedLike | None = None,
simulator:Simulator = ABMSimulator,
**kwargs: Any,
) -> None:This is compatible with the API sketched here, while giving users full control if they want to use full DEVS or just ABM style fixed time advancement. |
Beta Was this translation helpful? Give feedback.
-
|
I am not convinced by your points regarding methods versus keywords. Autocomplete at the method/function level shows up before you dive into keywords and thus, at least for me, works better to find what I need. Moreover, On the recurring point, you might be on to something. So yes, there is indeed a difference between what you call global processes and events.
|
Beta Was this translation helpful? Give feedback.
-
|
Doing some pathfinding in: |
Beta Was this translation helpful? Give feedback.
-
|
With #3155 providing the basics, this new architecture allows us to really flexibly schedule events. We already have the Underlying, there should be a really powerful I propose the following API for Core methodmodel.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 = no limit, defaults to 1 for one-off)
end_at=None, # Absolute time to stop
end_after=None, # Duration to run (from first execution)
end_when=None, # Condition to stop when true
priority=Priority.DEFAULT,
args=None,
kwargs=None,
) -> SimulationEventConstraints:
Convenience Functionsmodel.schedule_at(callback, time, priority=DEFAULT, args=None, kwargs=None)
# Equivalent to: model.schedule(callback, start_at=time)
model.schedule_after(callback, delay, priority=DEFAULT, args=None, kwargs=None)
# Equivalent to: model.schedule(callback, start_after=delay)
model.cancel_event(event)Decorator@scheduled # interval=1, count=None (infinite)
@scheduled(interval=7)
@scheduled(interval=7, start_at=10, count=5)
@scheduled(interval=1, end_when=lambda m: not m.running)
@scheduled(interval=1, only_if=lambda m: m.market_open)ExamplesOne-off Events# Immediate
model.schedule(self.initialize_agents)
# Scheduled disaster
model.schedule(self.earthquake, start_at=50)
# Delayed policy change
model.schedule(self.introduce_carbon_tax, start_after=100)Simple Recurring# Daily step
@scheduled
def step(self):
self.agents.do("step")
# Weekly reports
@scheduled(interval=7)
def weekly_report(self):
self.collect_statistics()
# Quarterly earnings (4 times)
model.schedule(self.publish_earnings, interval=90, start_at=90, count=4)Conditional Execution (Pausing)# Work week (Mon-Fri, 9am-5pm in simulation time)
@scheduled(
interval=1,
only_if=lambda m: m.time % 168 < 120 and 9 <= m.time % 24 < 17
)
def market_trading(self):
self.process_trades()
# Seasonal breeding (spring/summer only)
@scheduled(
interval=1,
only_if=lambda m: 90 <= m.time % 365 < 270
)
def breeding_season(self):
self.agents.select(lambda a: a.can_breed).do("reproduce")
# Crisis response (only during crisis)
@scheduled(interval=1, only_if=lambda m: m.in_crisis)
def crisis_response(self):
self.handle_crisis()Time-Bounded Recurring# Limited intervention
model.schedule(self.stimulus_payment, interval=30, count=3) # 3 payments
# Campaign that runs for 90 days
model.schedule(
self.run_campaign,
interval=1,
start_at=100,
end_after=90
)Variable & Stochastic Intervals# Activity rate changes over time
model.schedule(
self.dynamic_event,
interval=lambda m: m.current_activity_rate
)
# Poisson arrivals (customers, infections, etc.)
model.schedule(
self.customer_arrival,
interval=lambda m: m.random.expovariate(2.0), # rate = 2.0/time unit
end_at=1000
)
# Grass regrowth with variation
model.schedule(
self.grass_regrow,
interval=lambda m: m.random.uniform(20, 40),
start_after=self.eaten_at
)Combining only_if with other constraints# Trading system: only during market hours, for first 100 days
@scheduled(
interval=1,
only_if=lambda m: 9 <= m.time % 24 < 17 and m.time % 7 < 5,
end_at=100
)
def trading_step(self):
self.process_orders()
# Seasonal migration: only in fall, for 10 years
model.schedule(
self.migration_event,
interval=7,
only_if=lambda m: 270 <= m.time % 365 < 330, # Fall months
count=520 # ~10 years of weekly migrations
)Agent Self-Schedulingclass Prisoner(Agent):
def get_arrested(self, sentence):
self.in_jail = True
self.model.schedule(self.release, start_after=sentence)
def release(self):
self.in_jail = False
class GrassPatch(Agent):
def get_eaten(self):
self.fully_grown = False
self.model.schedule(
self.regrow,
start_after=self.regrowth_time
)
def regrow(self):
self.fully_grown = TrueEvent Chaining (User Pattern)# Poisson process implemented by user
class QueueModel(Model):
def __init__(self):
super().__init__()
self._schedule_next_arrival()
def _schedule_next_arrival(self):
delay = self.random.expovariate(self.arrival_rate)
self.schedule(self.customer_arrival, start_after=delay)
def customer_arrival(self):
Customer(self)
self._schedule_next_arrival() # Chain to nextTime Controlmodel.run_for(100) # Run for 100 time units
model.run_until(500) # Run until time 500
model.run_while(lambda m: m.running) # Run while condition true
model.run_next_event() # Execute next event (debugging)Comparison: only_if vs end_when# end_when: Stops scheduling permanently when condition becomes true
@scheduled(interval=1, end_when=lambda m: m.crisis_resolved)
def monitor(self):
self.check_status()
# Once crisis_resolved=True, monitoring stops forever
# only_if: Pauses/resumes based on condition
@scheduled(interval=1, only_if=lambda m: m.in_crisis)
def crisis_handler(self):
self.handle_crisis()
# Skips when in_crisis=False, resumes when in_crisis=TrueMigration from Mesa 3.x# Old (Mesa 3.x)
for _ in range(100):
model.step()
# New (Mesa 3.5+)
@scheduled
def step(self):
self.agents.do("step")
model.run_for(100) |
Beta Was this translation helpful? Give feedback.
-
|
This proposal helped something click for me: scheduling and run control really are separate concerns, but recurring behavior sits right at the boundary between them. What feels most powerful here isn’t so much run_for vs run_until, but the idea of having one expressive scheduling primitive that can naturally represent one-off events, fixed or variable recurrence, conditional execution (only_if), and bounded lifetimes (count, end_at). From that angle, step() stops being a special case and just becomes a recurring scheduled event with interval=1, which honestly feels like the right mental model. The only thing I’d be cautious about is API surface vs learnability. A single powerful schedule() with a small number of explicit convenience wrappers like schedule_at and schedule_after feels like a good balance before adding decorators or more syntactic sugar. Overall, this direction feels like it genuinely unifies ABM and DEVS semantics, rather than just layering one on top of the other. |
Beta Was this translation helpful? Give feedback.
-
|
We learn as we go. When scheduling things in a simulation, there are really two different concepts:
These deserve different names and different return types. An event is ephemeral. A pattern is persistent. # One-off events
event = model.event(callback, at=50)
event = model.event(callback, after=10)
event.cancel()
# Recurring patterns
pattern = model.pattern(callback, interval=1)
pattern = model.pattern(callback, interval=lambda m: m.random.expovariate(2.0))
pattern.pause()
pattern.resume()
pattern.stop()The method names signal what you’re creating. The return types give you appropriate controls. Why not a single
|
| Concept | API | Returns |
|---|---|---|
| One-off | model.event(callback, at=..., after=...) |
Event |
| Recurring | model.pattern(callback, interval=...) |
Pattern |
| Run | model.run_for(), model.run_until(), model.run_while() |
- |
| Step | Auto-scheduled, stored in model.step_pattern |
Pattern |
The whole system is: EventList on Model, Event and Pattern classes that register themselves, run methods that pop and execute.
A no fuss API.
Beta Was this translation helpful? Give feedback.
-
Does Mesa need a "heartbeat"?With the new Event/Pattern architecture, Use cases that might benefit from a heartbeat:
In pure discrete-event simulation, time jumps directly between events: there's no concept of "ticks." But many practical concerns (UI updates, data sampling, resource management) may benefit from predictable periodic execution. The main questions:
Everyone's opinion welcome, but especially @quaquel and @Sahil-Chhoker. |
Beta Was this translation helpful? Give feedback.
-
|
Now that we can generate recurring events (#3201) and refactored the internals (#3204), we're in a good state to build a public API on this. First, to recap:
There are still quite a few open questions / design decisions that need to be made. Here are the ones I think are now most important. Architectural
My personal proposal is to expose the current stuff though a simple, low-hanging API, with exactly 4 methods:
With the scheduling function, a Event or EventGenerator is returned, which can respectively be cancelled and stopped, restarted or modified otherwi From there we can start updating models, and see what's missing. I think that will help ground our discussions in:
|
Beta Was this translation helpful? Give feedback.
-
Conceptually, you have an event list containing the ordered list of events, a blob of objects that schedule events (also known as the model), and something that controls the time advancement of the event list (a simulator or run control). So, yes, I would favor a separate run-control/simulator object. How to expose the functionality to the user is a separate question. Because of the separation of concerns, it makes sense to cleanly separate these three fundamentally different things into their own separate classes, which can be tested and developed in isolation.
A decorator is, in my view, just syntactic sugar on top of an existing method/function-based API. So, although I like a
I agree with the methods, but note that scheduling is event-list interaction, while running is run-control interaction. They are conceptually really different things, and I would be careful not to conflate them. |
Beta Was this translation helpful? Give feedback.
-
Conditional running and stoppingFollowing up on the proposal for Core Use CasesTime-based stopping is covered by the proposed The model itself may also need to stop from within step logic. For example, detecting that an outbreak is contained mid-simulation and stopping execution. What to do with
|
Beta Was this translation helpful? Give feedback.
-
|
Two updates:
|
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Summary
This proposal redesigns Mesa's time advancement and event scheduling into a simple, unified API. The goal: make simple models simple, while enabling advanced discrete-event simulation for those who need it.
And everything in between.
Key changes:
Simulatorclasses (ABMSimulator,DEVSimulator)Modelrun()andschedule()methodsstep()optional sugar that's automatically scheduledMotivation
The problem with two objects
The current experimental DEVS implementation requires users to manage two separate objects:
This raises immediate questions: Why do I need a simulator? What's the difference between
ABMSimulatorandDEVSimulator? What doessetup()do? When would I callreset()?For most users, these questions shouldn't exist. They have a model, they want to run it.
The simplest API imaginable
Let's try to make it as simple as possible. You get something like:
The simulator is an implementation detail. Users shouldn't need to think about it.
Proposed API
Running models
Scheduling events
Model initialization
Usage progression
The API lets users start simple and incrementally adopt advanced features without rewriting their model. I think this is the coolest part of an unified API.
Level 1: Just define step()
This is how most Mesa models work today, and it continues to work unchanged:
No simulators, no setup, no complexity. Define
step(), callrun().Level 2: Add scheduled events
Now suppose you want a drought to occur at t=50. Just schedule it:
The model still steps normally at t=1, 2, 3..., and the drought fires at t=50. No changes to how you run the model.
Level 3: Agents scheduling events
Agents can schedule their own events. This is useful for agents that need to "wake up" after some time:
This is the pattern from Epstein's civil violence model. Jailed citizens don't need to be activated every tick just to check if they're still in jail.
Level 4: Agents with event-driven behavior
Some agents don't need regular activation at all. Grass in Wolf-Sheep only needs to act when eaten:
Instead of activating thousands of grass patches every tick to decrement a counter, each patch just schedules its own regrowth. This can dramatically improve performance.
Level 5: Multiple time scales
Some models have processes that operate at different speeds:
Weather updates 24 times per day, markets once per day, policy once per month. All coexist naturally.
Level 6: Pure event-driven
For true discrete-event simulation with no regular stepping:
Time jumps directly from event to event. No wasted computation on empty ticks.
Configuration reference
Migration
From current step-based models
No changes required:
From experimental Simulator API
Open questions
recurringclear? Alternatives:tick,auto_schedulemodel.stepsalongsidemodel.time? It's redundant for pure event-driven models, but familiar for step-based models.model.run()with no arguments run untilmodel.running = False? Or can it be defined additionally, together with something else?model.step()still work for manualforloops?Feedback welcome
I would love to hear your thoughts:
recurringmethods (step, etc.)Beta Was this translation helpful? Give feedback.
All reactions