Skip to content

Commit

Permalink
Bugfixes to DEVS (#2478)
Browse files Browse the repository at this point in the history
  • Loading branch information
quaquel authored Nov 9, 2024
1 parent 88a8d89 commit c078690
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 20 deletions.
1 change: 0 additions & 1 deletion benchmarks/global_benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
3 changes: 2 additions & 1 deletion mesa/examples/advanced/wolf_sheep/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
73 changes: 59 additions & 14 deletions mesa/experimental/devs/simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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()
Expand All @@ -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.
Expand All @@ -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)

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -277,14 +323,24 @@ 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()
except IndexError:
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:
Expand All @@ -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.
Expand Down
44 changes: 42 additions & 2 deletions tests/test_devs.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Tests for experimental Simulator classes."""

from unittest.mock import MagicMock
from unittest.mock import MagicMock, Mock

import pytest

Expand Down Expand Up @@ -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)
Expand All @@ -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."""
Expand All @@ -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():
Expand Down
3 changes: 1 addition & 2 deletions tests/test_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

0 comments on commit c078690

Please sign in to comment.