diff --git a/benchmarks/global_benchmark.py b/benchmarks/global_benchmark.py index c3f0c0716b2..ec46c019afe 100644 --- a/benchmarks/global_benchmark.py +++ b/benchmarks/global_benchmark.py @@ -33,7 +33,6 @@ def run_model(model_class, seed, parameters): if model_class.__name__ in uses_simulator: simulator = ABMSimulator() model = model_class(simulator=simulator, seed=seed, **parameters) - simulator.setup(model) else: model = model_class(seed=seed, **parameters) diff --git a/mesa/examples/advanced/wolf_sheep/model.py b/mesa/examples/advanced/wolf_sheep/model.py index 982fcc2809f..6f8887d0491 100644 --- a/mesa/examples/advanced/wolf_sheep/model.py +++ b/mesa/examples/advanced/wolf_sheep/model.py @@ -61,12 +61,13 @@ def __init__( simulator: ABMSimulator instance for event scheduling """ super().__init__(seed=seed) + self.simulator = simulator + self.simulator.setup(self) # Initialize model parameters self.height = height self.width = width self.grass = grass - self.simulator = simulator # Create grid using experimental cell space self.grid = OrthogonalVonNeumannGrid( diff --git a/mesa/experimental/devs/simulator.py b/mesa/experimental/devs/simulator.py index 8967c19ef8e..57749f038f4 100644 --- a/mesa/experimental/devs/simulator.py +++ b/mesa/experimental/devs/simulator.py @@ -57,8 +57,20 @@ def setup(self, model: Model) -> None: Args: model (Model): The model to simulate + Raises: + Exception if simulator.time is not equal to simulator.starttime + Exception if event list is not empty + """ - self.event_list.clear() + if self.time != self.start_time: + raise ValueError( + "trying to setup model, but current time is not equal to start_time, Has the simulator been reset or freshly initialized?" + ) + if not self.event_list.is_empty(): + raise ValueError( + "trying to setup model, but events have already been scheduled. Call simulator.setup before any scheduling" + ) + self.model = model def reset(self): @@ -68,7 +80,20 @@ def reset(self): self.time = self.start_time def run_until(self, end_time: int | float) -> None: - """Run the simulator until the end time.""" + """Run the simulator until the end time. + + Args: + end_time (int | float): The end time for stopping the simulator + + Raises: + Exception if simulator.setup() has not yet been called + + """ + if self.model is None: + raise Exception( + "simulator has not been setup, call simulator.setup(model) first" + ) + while True: try: event = self.event_list.pop_event() @@ -84,6 +109,26 @@ def run_until(self, end_time: int | float) -> None: self._schedule_event(event) # reschedule event break + def run_next_event(self): + """Execute the next event. + + Raises: + Exception if simulator.setup() has not yet been called + + """ + if self.model is None: + raise Exception( + "simulator has not been setup, call simulator.setup(model) first" + ) + + try: + event = self.event_list.pop_event() + except IndexError: # event list is empty + return + else: + self.time = event.time + event.execute() + def run_for(self, time_delta: int | float): """Run the simulator for the specified time delta. @@ -92,6 +137,7 @@ def run_for(self, time_delta: int | float): plus the time delta """ + # fixme, raise initialization error or something like it if model.setup has not been called end_time = self.time + time_delta self.run_until(end_time) @@ -228,7 +274,7 @@ def setup(self, model): """ super().setup(model) - self.schedule_event_now(self.model.step, priority=Priority.HIGH) + self.schedule_event_next_tick(self.model.step, priority=Priority.HIGH) def check_time_unit(self, time) -> bool: """Check whether the time is of the correct unit. @@ -277,7 +323,15 @@ def run_until(self, end_time: int) -> None: Args: end_time (float| int): The end_time delta. The simulator is until the specified end time + Raises: + Exception if simulator.setup() has not yet been called + """ + if self.model is None: + raise Exception( + "simulator has not been setup, call simulator.setup(model) first" + ) + while True: try: event = self.event_list.pop_event() @@ -285,6 +339,8 @@ def run_until(self, end_time: int) -> None: self.time = end_time break + # fixme: the alternative would be to wrap model.step with an annotation which + # handles this scheduling. if event.time <= end_time: self.time = event.time if event.fn() == self.model.step: @@ -298,17 +354,6 @@ def run_until(self, end_time: int) -> None: self._schedule_event(event) break - def run_for(self, time_delta: int): - """Run the simulator for the specified time delta. - - Args: - time_delta (float| int): The time delta. The simulator is run from the current time to the current time - plus the time delta - - """ - end_time = self.time + time_delta - 1 - self.run_until(end_time) - class DEVSimulator(Simulator): """A simulator where the unit of time is a float. diff --git a/tests/test_devs.py b/tests/test_devs.py index 8f1dd9373fd..883a74d1375 100644 --- a/tests/test_devs.py +++ b/tests/test_devs.py @@ -1,6 +1,6 @@ """Tests for experimental Simulator classes.""" -from unittest.mock import MagicMock +from unittest.mock import MagicMock, Mock import pytest @@ -55,6 +55,23 @@ def test_devs_simulator(): with pytest.raises(ValueError): simulator.schedule_event_absolute(fn2, 0.5) + # step + simulator = DEVSimulator() + model = MagicMock(spec=Model) + simulator.setup(model) + + fn = MagicMock() + simulator.schedule_event_absolute(fn, 1.0) + simulator.run_next_event() + fn.assert_called_once() + assert simulator.time == 1.0 + simulator.run_next_event() + assert simulator.time == 1.0 + + simulator = DEVSimulator() + with pytest.raises(Exception): + simulator.run_next_event() + # cancel_event simulator = DEVSimulator() model = MagicMock(spec=Model) @@ -70,6 +87,24 @@ def test_devs_simulator(): assert simulator.model is None assert simulator.time == 0.0 + # run without setup + simulator = DEVSimulator() + with pytest.raises(Exception): + simulator.run_until(10) + + # setup with time advanced + simulator = DEVSimulator() + simulator.time = simulator.start_time + 1 + model = MagicMock(spec=Model) + with pytest.raises(Exception): + simulator.setup(model) + + # setup with event scheduled + simulator = DEVSimulator() + simulator.schedule_event_now(Mock()) + with pytest.raises(Exception): + simulator.setup(model) + def test_abm_simulator(): """Tests abm simulator.""" @@ -86,7 +121,12 @@ def test_abm_simulator(): simulator.run_for(3) assert model.step.call_count == 3 - assert simulator.time == 2 + assert simulator.time == 3 + + # run without setup + simulator = ABMSimulator() + with pytest.raises(Exception): + simulator.run_until(10) def test_simulation_event(): diff --git a/tests/test_examples.py b/tests/test_examples.py index 436bd805f70..98d1d5809ee 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -69,6 +69,5 @@ def test_wolf_sheep(): # noqa: D103 from mesa.experimental.devs import ABMSimulator simulator = ABMSimulator() - model = WolfSheep(seed=42, simulator=simulator) - simulator.setup(model) + WolfSheep(seed=42, simulator=simulator) simulator.run_for(10)