Skip to content

Commit 3aa468d

Browse files
committed
Merge remote-tracking branch 'upstream/main'
2 parents 50d5551 + e99e37d commit 3aa468d

File tree

3 files changed

+93
-83
lines changed

3 files changed

+93
-83
lines changed

mesa/experimental/devs/simulator.py

Lines changed: 29 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,19 @@ def __init__(self, time_unit: type, start_time: int | float):
5757
time_unit: type of the smulaiton time
5858
start_time: the starttime of the simulator
5959
"""
60-
# should model run in a separate thread,
61-
# and we can then interact with start, stop, run_until, and step?
62-
self.event_list = EventList()
6360
self.start_time = start_time
6461
self.time_unit = time_unit
6562
self.model: Model | None = None
6663

64+
@property
65+
def event_list(self) -> EventList:
66+
"""Return the event list from the model."""
67+
if self.model is None:
68+
raise RuntimeError(
69+
"Simulator not set up. Call simulator.setup(model) first."
70+
)
71+
return self.model._event_list
72+
6773
@property
6874
def time(self) -> float:
6975
"""Simulator time (deprecated)."""
@@ -92,19 +98,18 @@ def setup(self, model: Model) -> None:
9298
f"Model time ({model.time}) does not match simulator start_time ({self.start_time}). "
9399
"Has the model already been run?"
94100
)
95-
if not self.event_list.is_empty():
101+
if not model._event_list.is_empty():
96102
raise ValueError("Events already scheduled. Call setup before scheduling.")
97103

98104
self.model = model
99105
model._simulator = self # Register simulator with model
100106

101107
def reset(self):
102108
"""Reset the simulator."""
103-
self.event_list.clear()
104109
if self.model is not None:
110+
self.event_list.clear()
105111
self.model._simulator = None
106112
self.model.time = self.start_time
107-
self.model = None
108113

109114
def run_until(self, end_time: int | float) -> None:
110115
"""Run the simulator until the end time.
@@ -121,20 +126,7 @@ def run_until(self, end_time: int | float) -> None:
121126
"Simulator not set up. Call simulator.setup(model) first."
122127
)
123128

124-
while True:
125-
try:
126-
event = self.event_list.pop_event()
127-
except IndexError:
128-
self.model.time = end_time
129-
break
130-
131-
if event.time <= end_time:
132-
self.model.time = event.time
133-
event.execute()
134-
else:
135-
self.model.time = end_time
136-
self._schedule_event(event)
137-
break
129+
self.model._advance_time(end_time)
138130

139131
def run_next_event(self):
140132
"""Execute the next event.
@@ -167,14 +159,11 @@ def run_for(self, time_delta: int | float):
167159
Exception if simulator.setup() has not yet been called
168160
169161
"""
170-
try:
171-
end_time = self.model.time + time_delta
172-
except AttributeError as e:
162+
if self.model is None:
173163
raise RuntimeError(
174164
"Simulator not set up. Call simulator.setup(model) first."
175-
) from e
176-
else:
177-
self.run_until(end_time)
165+
)
166+
self.run_until(self.model.time + time_delta)
178167

179168
def schedule_event_now(
180169
self,
@@ -290,7 +279,6 @@ def _schedule_event(self, event: SimulationEvent):
290279
f"time unit mismatch {event.time} is not of time unit {self.time_unit}"
291280
)
292281

293-
# check timeunit of events
294282
self.event_list.add_event(event)
295283

296284

@@ -316,7 +304,8 @@ def setup(self, model):
316304
317305
"""
318306
super().setup(model)
319-
self.schedule_event_next_tick(self.model.step, priority=Priority.HIGH)
307+
# Schedule the first step event
308+
model._schedule_step(1)
320309

321310
def check_time_unit(self, time) -> bool:
322311
"""Check whether the time is of the correct unit.
@@ -359,43 +348,6 @@ def schedule_event_next_tick(
359348
function_kwargs=function_kwargs,
360349
)
361350

362-
def run_until(self, end_time: int) -> None:
363-
"""Run the simulator up to and included the specified end time.
364-
365-
Args:
366-
end_time (float| int): The end_time delta. The simulator is until the specified end time
367-
368-
Raises:
369-
Exception if simulator.setup() has not yet been called
370-
371-
"""
372-
if self.model is None:
373-
raise RuntimeError(
374-
"Simulator not set up. Call simulator.setup(model) first."
375-
)
376-
377-
while True:
378-
try:
379-
event = self.event_list.pop_event()
380-
except IndexError:
381-
self.model.time = float(end_time)
382-
break
383-
384-
if event.time <= end_time:
385-
self.model.time = float(event.time)
386-
387-
# Reschedule model.step for next tick if this is a step event
388-
if event.fn() == self.model.step:
389-
self.schedule_event_next_tick(
390-
self.model.step, priority=Priority.HIGH
391-
)
392-
393-
event.execute()
394-
else:
395-
self.model.time = float(end_time)
396-
self._schedule_event(event)
397-
break
398-
399351

400352
class DEVSimulator(Simulator):
401353
"""A simulator where the unit of time is a float.
@@ -408,6 +360,18 @@ def __init__(self):
408360
"""Initialize a DEVS simulator."""
409361
super().__init__(float, 0.0)
410362

363+
def setup(self, model: Model) -> None:
364+
"""Set up the simulator with the model to simulate.
365+
366+
Args:
367+
model (Model): The model to simulate
368+
369+
"""
370+
# For pure DEVS, we don't want automatic step scheduling
371+
# Clear any pre-scheduled events
372+
model._event_list.clear()
373+
super().setup(model)
374+
411375
def check_time_unit(self, time) -> bool:
412376
"""Check whether the time is of the correct unit.
413377

mesa/model.py

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,15 @@
1212
from collections.abc import Sequence
1313

1414
# mypy
15-
from typing import Any
15+
from typing import TYPE_CHECKING, Any
1616

1717
import numpy as np
1818

19+
if TYPE_CHECKING:
20+
from mesa.experimental.devs import Simulator
21+
1922
from mesa.agent import Agent, AgentSet
20-
from mesa.experimental.devs import Simulator
23+
from mesa.experimental.devs.eventlist import EventList, Priority, SimulationEvent
2124
from mesa.experimental.scenarios import Scenario
2225
from mesa.mesa_logging import create_module_logger, method_logger
2326

@@ -103,6 +106,9 @@ def __init__(
103106
# Track if a simulator is controlling time
104107
self._simulator: Simulator | None = None
105108

109+
# Event list for event-based execution
110+
self._event_list: EventList = EventList()
111+
106112
# check if `scenario` is provided
107113
# and if so, whether rng is the same or not
108114
if scenario is not None:
@@ -167,18 +173,45 @@ def __init__(
167173
) # an agenset with all agents
168174

169175
def _wrapped_step(self, *args: Any, **kwargs: Any) -> None:
170-
"""Automatically increments time and steps after calling the user's step method."""
171-
# Automatically increment time and step counters
172-
self.steps += 1
173-
# Only auto-increment time if no simulator is controlling it
174-
if self._simulator is None:
175-
self.time += 1
176+
"""Advance time by one unit, processing any scheduled events."""
177+
# Schedule step event if not already scheduled (first call or no simulator)
178+
if self._event_list.is_empty():
179+
self._schedule_step(self.time + 1)
180+
self._advance_time(self.time + 1)
176181

177-
_mesa_logger.info(
178-
f"calling model.step for step {self.steps} at time {self.time}"
179-
)
180-
# Call the original user-defined step method
181-
self._user_step(*args, **kwargs)
182+
def _advance_time(self, until: float) -> None:
183+
"""Advance time to the given point, processing events along the way.
184+
185+
Args:
186+
until: The time to advance to
187+
188+
"""
189+
while True:
190+
try:
191+
event = self._event_list.pop_event()
192+
except IndexError:
193+
break
194+
195+
if event.time <= until:
196+
self.time = event.time
197+
event.execute()
198+
else:
199+
self._event_list.add_event(event)
200+
break
201+
202+
self.time = until
203+
204+
def _schedule_step(self, time: float) -> None:
205+
"""Schedule a step event at the given time."""
206+
event = SimulationEvent(time, self._do_step, priority=Priority.HIGH)
207+
self._event_list.add_event(event)
208+
209+
def _do_step(self) -> None:
210+
"""Execute one step and schedule the next."""
211+
self.steps += 1
212+
_mesa_logger.info(f"Step {self.steps} at time {self.time}")
213+
self._user_step()
214+
self._schedule_step(self.time + 1)
182215

183216
@property
184217
def agents(self) -> AgentSet[A]:

tests/experimental/test_devs.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ def test_devs_simulator():
9494
# simulator reset
9595
simulator.reset()
9696
assert len(simulator.event_list) == 0
97-
assert simulator.model is None
97+
assert simulator.model is model
9898

9999
# run_for without setup
100100
simulator = DEVSimulator()
@@ -115,10 +115,8 @@ def test_devs_simulator():
115115

116116
# setup with event scheduled
117117
simulator = DEVSimulator()
118-
model = Model()
119-
simulator.event_list.add_event(SimulationEvent(1.0, Mock(), Priority.DEFAULT))
120-
with pytest.raises(ValueError):
121-
simulator.setup(model)
118+
with pytest.raises(RuntimeError, match="Simulator not set up"):
119+
simulator.event_list.add_event(SimulationEvent(1.0, Mock(), Priority.DEFAULT))
122120

123121

124122
def test_abm_simulator():
@@ -417,6 +415,21 @@ def test_eventlist():
417415
assert len(event_list) == 0
418416

419417

418+
def test_simulator_uses_model_event_list():
419+
"""Test that simulator uses model's internal event list."""
420+
model = Model()
421+
simulator = DEVSimulator()
422+
simulator.setup(model)
423+
424+
# Simulator's event_list property should return model's event list
425+
assert simulator.event_list is model._event_list
426+
427+
# Events scheduled through simulator appear in model's event list
428+
fn = MagicMock()
429+
simulator.schedule_event_absolute(fn, 1.0)
430+
assert len(model._event_list) == 1
431+
432+
420433
@pytest.fixture
421434
def setup():
422435
"""Create a model with simulator and mock function."""

0 commit comments

Comments
 (0)