Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions mesa/experimental/devs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@
"""

from .eventlist import Priority, SimulationEvent
from .simulator import ABMSimulator, DEVSimulator
from .simulator import ABMSimulator, DEVSimulator, Simulator

__all__ = ["ABMSimulator", "DEVSimulator", "Priority", "SimulationEvent"]
__all__ = ["ABMSimulator", "DEVSimulator", "Priority", "SimulationEvent", "Simulator"]
77 changes: 44 additions & 33 deletions mesa/experimental/devs/simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from __future__ import annotations

import numbers
import warnings
from collections.abc import Callable
from typing import TYPE_CHECKING, Any

Expand Down Expand Up @@ -61,9 +62,17 @@ def __init__(self, time_unit: type, start_time: int | float):
self.event_list = EventList()
self.start_time = start_time
self.time_unit = time_unit

self.time = self.start_time
self.model = None
self.model: Model | None = None

@property
def time(self) -> float:
"""Simulator time (deprecated)."""
warnings.warn(
"simulator.time is deprecated, use model.time instead",
DeprecationWarning,
stacklevel=2,
)
return self.model.time

def check_time_unit(self, time: int | float) -> bool: ... # noqa: D102

Expand All @@ -78,22 +87,24 @@ def setup(self, model: Model) -> None:
Exception if event list is not empty

"""
if self.time != self.start_time:
if model.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?"
f"Model time ({model.time}) does not match simulator start_time ({self.start_time}). "
"Has the model already been run?"
)
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"
)
raise ValueError("Events already scheduled. Call setup before scheduling.")

self.model = model
model._simulator = self # Register simulator with model

def reset(self):
"""Reset the simulator by clearing the event list and removing the model to simulate."""
"""Reset the simulator."""
self.event_list.clear()
self.model = None
self.time = self.start_time
if self.model is not None:
self.model._simulator = None
self.model.time = self.start_time
self.model = None

def run_until(self, end_time: int | float) -> None:
"""Run the simulator until the end time.
Expand All @@ -106,23 +117,23 @@ def run_until(self, end_time: int | float) -> None:

"""
if self.model is None:
raise Exception(
"simulator has not been setup, call simulator.setup(model) first"
raise RuntimeError(
"Simulator not set up. Call simulator.setup(model) first."
)

while True:
try:
event = self.event_list.pop_event()
except IndexError: # event list is empty
self.time = end_time
except IndexError:
self.model.time = end_time
break

if event.time <= end_time:
self.time = event.time
self.model.time = event.time
event.execute()
else:
self.time = end_time
self._schedule_event(event) # reschedule event
self.model.time = end_time
self._schedule_event(event)
break

def run_next_event(self):
Expand All @@ -133,17 +144,17 @@ def run_next_event(self):

"""
if self.model is None:
raise Exception(
"simulator has not been setup, call simulator.setup(model) first"
raise RuntimeError(
"Simulator not set up. Call simulator.setup(model) first."
)

try:
event = self.event_list.pop_event()
except IndexError: # event list is empty
except IndexError:
return
else:
self.time = event.time
event.execute()

self.model.time = event.time
event.execute()

def run_for(self, time_delta: int | float):
"""Run the simulator for the specified time delta.
Expand All @@ -154,7 +165,7 @@ def run_for(self, time_delta: int | float):

"""
# fixme, raise initialization error or something like it if model.setup has not been called
end_time = self.time + time_delta
end_time = self.model.time + time_delta
self.run_until(end_time)

def schedule_event_now(
Expand Down Expand Up @@ -240,7 +251,7 @@ def schedule_event_relative(

"""
event = SimulationEvent(
self.time + time_delta,
self.model.time + time_delta,
function,
priority=priority,
function_args=function_args,
Expand Down Expand Up @@ -344,29 +355,29 @@ def run_until(self, end_time: int) -> None:

"""
if self.model is None:
raise Exception(
"simulator has not been setup, call simulator.setup(model) first"
raise RuntimeError(
"Simulator not set up. Call simulator.setup(model) first."
)

while True:
try:
event = self.event_list.pop_event()
except IndexError:
self.time = end_time
self.model.time = float(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
self.model.time = float(event.time)

# Reschedule model.step for next tick if this is a step event
if event.fn() == self.model.step:
self.schedule_event_next_tick(
self.model.step, priority=Priority.HIGH
)

event.execute()
else:
self.time = end_time
self.model.time = float(end_time)
self._schedule_event(event)
break

Expand Down
18 changes: 16 additions & 2 deletions mesa/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import numpy as np

from mesa.agent import Agent, AgentSet
from mesa.experimental.devs import Simulator
from mesa.mesa_logging import create_module_logger, method_logger

SeedLike = int | np.integer | Sequence[int] | np.random.SeedSequence
Expand Down Expand Up @@ -52,6 +53,7 @@ def __init__(
*args: Any,
seed: float | None = None,
rng: RNGLike | SeedLike | None = None,
step_duration: float = 1.0,
**kwargs: Any,
) -> None:
"""Create a new model.
Expand All @@ -65,15 +67,21 @@ def __init__(
rng : Pseudorandom number generator state. When `rng` is None, a new `numpy.random.Generator` is created
using entropy from the operating system. Types other than `numpy.random.Generator` are passed to
`numpy.random.default_rng` to instantiate a `Generator`.
step_duration: How much time advances each step (default 1.0)
kwargs: keyword arguments to pass onto super

Notes:
you have to pass either seed or rng, but not both.

"""
super().__init__(*args, **kwargs)
self.running = True
self.running: bool = True
self.steps: int = 0
self.time: float = 0.0
self._step_duration: float = step_duration

# Track if a simulator is controlling time
self._simulator: Simulator | None = None

if (seed is not None) and (rng is not None):
raise ValueError("you have to pass either rng or seed, not both")
Expand Down Expand Up @@ -117,7 +125,13 @@ def _wrapped_step(self, *args: Any, **kwargs: Any) -> None:
"""Automatically increments time and steps after calling the user's step method."""
# Automatically increment time and step counters
self.steps += 1
_mesa_logger.info(f"calling model.step for timestep {self.steps} ")
# Only auto-increment time if no simulator is controlling it
if self._simulator is None:
self.time += self._step_duration

_mesa_logger.info(
f"calling model.step for step {self.steps} at time {self.time}"
)
# Call the original user-defined step method
self._user_step(*args, **kwargs)

Expand Down
46 changes: 28 additions & 18 deletions tests/test_devs.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ def test_devs_simulator():
simulator = DEVSimulator()

# setup
model = MagicMock(spec=Model)
model = Model()
simulator.setup(model)

assert len(simulator.event_list) == 0
assert simulator.model == model
assert simulator.time == 0
assert model.time == 0.0

# schedule_event_now
fn1 = MagicMock()
Expand All @@ -43,38 +43,38 @@ def test_devs_simulator():
simulator.run_for(0.8)
fn1.assert_called_once()
fn3.assert_called_once()
assert simulator.time == 0.8
assert model.time == 0.8

simulator.run_for(0.2)
fn2.assert_called_once()
assert simulator.time == 1.0
assert model.time == 1.0

simulator.run_for(0.2)
assert simulator.time == 1.2
assert model.time == 1.2

with pytest.raises(ValueError):
simulator.schedule_event_absolute(fn2, 0.5)

# step
simulator = DEVSimulator()
model = MagicMock(spec=Model)
model = 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
assert model.time == 1.0
simulator.run_next_event()
assert simulator.time == 1.0
assert model.time == 1.0

simulator = DEVSimulator()
with pytest.raises(Exception):
simulator.run_next_event()

# cancel_event
simulator = DEVSimulator()
model = MagicMock(spec=Model)
model = Model()
simulator.setup(model)
fn = MagicMock()
event = simulator.schedule_event_relative(fn, 0.5)
Expand All @@ -85,7 +85,6 @@ def test_devs_simulator():
simulator.reset()
assert len(simulator.event_list) == 0
assert simulator.model is None
assert simulator.time == 0.0

# run without setup
simulator = DEVSimulator()
Expand All @@ -94,15 +93,16 @@ def test_devs_simulator():

# setup with time advanced
simulator = DEVSimulator()
simulator.time = simulator.start_time + 1
model = MagicMock(spec=Model)
with pytest.raises(Exception):
model = Model()
model.time = 1.0 # Advance time before setup
with pytest.raises(ValueError):
simulator.setup(model)

# setup with event scheduled
simulator = DEVSimulator()
simulator.schedule_event_now(Mock())
with pytest.raises(Exception):
model = Model()
simulator.event_list.add_event(SimulationEvent(1.0, Mock(), Priority.DEFAULT))
with pytest.raises(ValueError):
simulator.setup(model)


Expand All @@ -111,7 +111,7 @@ def test_abm_simulator():
simulator = ABMSimulator()

# setup
model = MagicMock(spec=Model)
model = Model()
simulator.setup(model)

# schedule_event_next_tick
Expand All @@ -120,15 +120,25 @@ def test_abm_simulator():
assert len(simulator.event_list) == 2

simulator.run_for(3)
assert model.step.call_count == 3
assert simulator.time == 3
assert model.steps == 3
assert model.time == 3.0

# run without setup
simulator = ABMSimulator()
with pytest.raises(Exception):
simulator.run_until(10)


def test_simulator_time_deprecation():
"""Test that simulator.time emits deprecation warning."""
simulator = DEVSimulator()
model = Model()
simulator.setup(model)

with pytest.warns(DeprecationWarning, match="simulator.time is deprecated"):
_ = simulator.time


def test_simulation_event():
"""Tests for SimulationEvent class."""
some_test_function = MagicMock()
Expand Down
Loading