From 92831dd96cdb0d259b9bf9857db7f6c54537c7b3 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sun, 11 Feb 2024 20:26:55 +0100 Subject: [PATCH 01/44] first commit --- mesa/experimental/devs/__init__.py | 0 mesa/experimental/devs/eventlist.py | 96 +++++++ mesa/experimental/devs/examples/wolf_sheep.py | 246 ++++++++++++++++++ mesa/experimental/devs/simulator.py | 64 +++++ 4 files changed, 406 insertions(+) create mode 100644 mesa/experimental/devs/__init__.py create mode 100644 mesa/experimental/devs/eventlist.py create mode 100644 mesa/experimental/devs/examples/wolf_sheep.py create mode 100644 mesa/experimental/devs/simulator.py diff --git a/mesa/experimental/devs/__init__.py b/mesa/experimental/devs/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/mesa/experimental/devs/eventlist.py b/mesa/experimental/devs/eventlist.py new file mode 100644 index 00000000000..e3265987355 --- /dev/null +++ b/mesa/experimental/devs/eventlist.py @@ -0,0 +1,96 @@ +import itertools +from enum import IntEnum +from heapq import heapify, heappop, heappush + + +class InstanceCounterMeta(type): + """ Metaclass to make instance counter not share count with descendants + + FIXME:: can also be used for agents + """ + + def __init__(cls, name, bases, attrs): + super().__init__(name, bases, attrs) + cls._ids = itertools.count(1) + + +class Priority(IntEnum): + LOW = 1 + HIGH = 10 + DEFAULT = 5 + + +class SimEvent(metaclass=InstanceCounterMeta): + # fixme:: how do we want to handle function? + # should be a callable, possibly on an object + # also we want only weakrefs to agents + + def __init__(self, time, function, priority=Priority.DEFAULT, function_args=None, function_kwargs=None): + super().__init__() + self.time = time + self.priority = priority + self.fn = function + self.unique_id = next(self._ids) + self.function_args = function_args if function_args else [] + self.function_kwargs = function_kwargs if function_kwargs else {} + + if self.fn is None: + raise Exception() + + def execute(self): + self.fn(*self.function_args, **self.function_kwargs) + + def __cmp__(self, other): + if self.time < other.time: + return -1 + if self.time > other.time: + return 1 + + if self.priority > other.priority: + return -1 + if self.priority < other.priority: + return 1 + + if self.unique_id < other.unique_id: + return -1 + if self.unique_id > other.unique_id: + return 1 + + # theoretically this should be impossible unless it is the + # exact same event + return 0 + + +class EventList: + def __init__(self): + super().__init__() + self._event_list: list[SimEvent] = [] + heapify(self._event_list) + + def add_event(self, event: SimEvent): + heappush(self._event_list, (event.time, event.priority, event.unique_id, event)) + + def peek_ahead(self, n: int = 1) -> list[SimEvent]: + # look n events ahead, or delta time ahead + if self.is_empty(): + raise IndexError("event list is empty") + return [entry[3] for entry in self._event_list[0:n]] + + def pop(self) -> SimEvent: + # return next event + return heappop(self._event_list)[3] + + def is_empty(self) -> bool: + return len(self) == 0 + + def __contains__(self, event: SimEvent) -> bool: + return (event.time, event.priority, event.unique_id, event) in self._event_list + + def __len__(self) -> int: + return len(self._event_list) + + def remove(self, event): + self._event_list.remove((event.time, event.priority, event.unique_id, event)) + + def clear(self): + self._event_list.clear() diff --git a/mesa/experimental/devs/examples/wolf_sheep.py b/mesa/experimental/devs/examples/wolf_sheep.py new file mode 100644 index 00000000000..60ed44eabb6 --- /dev/null +++ b/mesa/experimental/devs/examples/wolf_sheep.py @@ -0,0 +1,246 @@ +""" +Wolf-Sheep Predation Model +================================ + +Replication of the model found in NetLogo: + Wilensky, U. (1997). NetLogo Wolf Sheep Predation model. + http://ccl.northwestern.edu/netlogo/models/WolfSheepPredation. + Center for Connected Learning and Computer-Based Modeling, + Northwestern University, Evanston, IL. +""" + +import mesa +from mesa.experimental.devs.simulator import ABMSimulator + + +class Animal(mesa.Agent): + def __init__(self, unique_id, model, moore, energy, p_reproduce, energy_from_food): + super().__init__(unique_id, model) + self.energy = energy + self.p_reproduce = p_reproduce + self.energy_from_food = energy_from_food + self.moore = moore + + def random_move(self): + next_moves = self.model.grid.get_neighborhood(self.pos, self.moore, True) + next_move = self.random.choice(next_moves) + # Now move: + self.model.grid.move_agent(self, next_move) + + def spawn_offspring(self): + self.energy /= 2 + offspring = self.__class__( + self.model.next_id(), + self.model, + self.moore, + self.energy, + self.p_reproduce, + self.energy_from_food, + ) + self.model.grid.place_agent(offspring, self.pos) + + def feed(self): + ... + + def die(self): + self.model.grid.remove_agent(self) + self.remove() + + def step(self): + self.random_move() + self.energy -= 1 + + self.feed() + + if self.energy < 0: + self.die() + elif self.random.random() < self.p_reproduce: + self.spawn_offspring() + + +class Sheep(Animal): + """ + A sheep that walks around, reproduces (asexually) and gets eaten. + + The init is the same as the RandomWalker. + """ + + def feed(self): + # If there is grass available, eat it + agents = self.model.grid.get_cell_list_contents(self.pos) + grass_patch = next(obj for obj in agents if isinstance(obj, GrassPatch)) + if grass_patch.fully_grown: + self.energy += self.energy_from_food + grass_patch.fully_grown = False + + +class Wolf(Animal): + """ + A wolf that walks around, reproduces (asexually) and eats sheep. + """ + + def feed(self): + agents = self.model.grid.get_cell_list_contents(self.pos) + sheep = [obj for obj in agents if isinstance(obj, Sheep)] + if len(sheep) > 0: + sheep_to_eat = self.random.choice(sheep) + self.energy += self.energy + + # Kill the sheep + sheep_to_eat.die() + + +class GrassPatch(mesa.Agent): + """ + A patch of grass that grows at a fixed rate and it is eaten by sheep + """ + + @property + def fully_grown(self): + return self._fully_grown + + @fully_grown.setter + def fully_grown(self, value): + self._fully_grown = value + + if value == False: + self.model.simulator.schedule_event_relative(self.set_fully_grown, self.grass_regrowth_time) + + + def __init__(self, unique_id, model, fully_grown, countdown, grass_regrowth_time): + """ + Creates a new patch of grass + + Args: + grown: (boolean) Whether the patch of grass is fully grown or not + countdown: Time for the patch of grass to be fully grown again + """ + super().__init__(unique_id, model) + self._fully_grown = fully_grown + self.grass_regrowth_time = grass_regrowth_time + + if not self.fully_grown: + self.model.simulator.schedule_event_relative(self.set_fully_grown, countdown) + + def set_fully_grown(self): + self.fully_grown = True + + + +class WolfSheep(mesa.Model): + """ + Wolf-Sheep Predation Model + + A model for simulating wolf and sheep (predator-prey) ecosystem modelling. + """ + + def __init__( + self, + seed, + height, + width, + initial_sheep, + initial_wolves, + sheep_reproduce, + wolf_reproduce, + grass_regrowth_time, + wolf_gain_from_food=13, + sheep_gain_from_food=5, + moore=False, + simulator=None, + ): + """ + Create a new Wolf-Sheep model with the given parameters. + + Args: + initial_sheep: Number of sheep to start with + initial_wolves: Number of wolves to start with + sheep_reproduce: Probability of each sheep reproducing each step + wolf_reproduce: Probability of each wolf reproducing each step + wolf_gain_from_food: Energy a wolf gains from eating a sheep + grass: Whether to have the sheep eat grass for energy + grass_regrowth_time: How long it takes for a grass patch to regrow + once it is eaten + sheep_gain_from_food: Energy sheep gain from grass, if enabled. + moore: + """ + super().__init__(seed=seed) + # Set parameters + self.height = height + self.width = width + self.initial_sheep = initial_sheep + self.initial_wolves = initial_wolves + self.simulator = simulator + + self.sheep_reproduce = sheep_reproduce + self.wolf_reproduce = wolf_reproduce + self.grass_regrowth_time = grass_regrowth_time + self.wolf_gain_from_food = wolf_gain_from_food + self.sheep_gain_from_food = sheep_gain_from_food + self.moore = moore + + def setup(self): + self.grid = mesa.space.MultiGrid(self.height, self.width, torus=False) + + for _ in range(self.initial_sheep): + pos = ( + self.random.randrange(self.width), + self.random.randrange(self.height), + ) + energy = self.random.randrange(2 * self.sheep_gain_from_food) + sheep = Sheep( + self.next_id(), + self, + self.moore, + energy, + self.sheep_reproduce, + self.sheep_gain_from_food, + ) + self.grid.place_agent(sheep, pos) + + # Create wolves + for _ in range(self.initial_wolves): + pos = ( + self.random.randrange(self.width), + self.random.randrange(self.height), + ) + energy = self.random.randrange(2 * self.wolf_gain_from_food) + wolf = Wolf( + self.next_id(), self, self.moore, energy, self.wolf_reproduce, self.wolf_gain_from_food + ) + self.grid.place_agent(wolf, pos) + + # Create grass patches + possibly_fully_grown = [True, False] + for _agent, pos in self.grid.coord_iter(): + fully_grown = self.random.choice(possibly_fully_grown) + if fully_grown: + countdown = self.grass_regrowth_time + else: + countdown = self.random.randrange(self.grass_regrowth_time) + patch = GrassPatch(self.next_id(), self, fully_grown, countdown, self.grass_regrowth_time) + self.grid.place_agent(patch, pos) + + self.simulator.schedule_event_relative(self.step, 1) + + + + def step(self): + self.get_agents_of_type(Sheep).do("step") + self.get_agents_of_type(Wolf).do("step") + self.simulator.schedule_event_relative(self.step, 1) + + +if __name__ == "__main__": + import time + + simulator = ABMSimulator() + + model = WolfSheep(15, 25, 25, 60, 40, 0.2, 0.1, 20, + simulator=simulator) + + simulator.setup(model) + + start_time = time.perf_counter() + simulator.run(until=100) + print("Time:", time.perf_counter() - start_time) \ No newline at end of file diff --git a/mesa/experimental/devs/simulator.py b/mesa/experimental/devs/simulator.py new file mode 100644 index 00000000000..0eb230e9f99 --- /dev/null +++ b/mesa/experimental/devs/simulator.py @@ -0,0 +1,64 @@ +from .eventlist import EventList, SimEvent, Priority + + +class Simulator: + + def __init__(self, time_unit, start_time): + # should model run in a separate thread + # and we can then interact with start, stop, run_until, and step? + self.event_list = EventList() + self.time = start_time + self.time_unit = time_unit # FIXME currently not used + self.model = None + + def setup(self, model): + self.event_list.clear() + self.model = model + model.setup() + + def reset(self): + pass + + def run(self, until=None): + # run indefinitely? or until is reached + + endtime = self.time + until + while self.time < endtime: + self.step() + + def step(self): + event = self.event_list.pop() + self.time = event.time + event.execute() + + def schedule_event_now(self, function, priority=Priority.DEFAULT, function_args=None, function_kwargs=None) -> SimEvent: + event = SimEvent(self.time, function, priority=priority, function_args=function_args, function_kwargs=function_kwargs) + self._schedule_event(event) + return event + + def schedule_event_absolute(self, function, time, priority=Priority.DEFAULT, function_args=None, function_kwargs=None) -> SimEvent: + event = SimEvent(time, function, priority=priority, function_args=function_args, function_kwargs=function_kwargs) + self._schedule_event(event) + return event + + def schedule_event_relative(self, function, time_delta, priority=Priority.DEFAULT, function_args=None, function_kwargs=None) -> SimEvent: + event = SimEvent(self.time + time_delta, function, priority=priority, function_args=function_args, function_kwargs=function_kwargs) + self._schedule_event(event) + return event + + def cancel_event(self, event): + self.event_list.remove(event) + + def _schedule_event(self, event): + # check timeunit of events + self.event_list.add_event(event) + + +class ABMSimulator(Simulator): + def __init__(self): + super().__init__(int, 0) + + +class DEVSimulator(Simulator): + def __init__(self): + super().__init__(float, 0.0) From b7761a86d2578668d85495c111741bb86bef3ef5 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sun, 11 Feb 2024 20:31:09 +0100 Subject: [PATCH 02/44] Update wolf_sheep.py --- mesa/experimental/devs/examples/wolf_sheep.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mesa/experimental/devs/examples/wolf_sheep.py b/mesa/experimental/devs/examples/wolf_sheep.py index 60ed44eabb6..9157f3f9501 100644 --- a/mesa/experimental/devs/examples/wolf_sheep.py +++ b/mesa/experimental/devs/examples/wolf_sheep.py @@ -226,8 +226,8 @@ def setup(self): def step(self): - self.get_agents_of_type(Sheep).do("step") - self.get_agents_of_type(Wolf).do("step") + self.get_agents_of_type(Sheep).shuffle(inplace=True).do("step") + self.get_agents_of_type(Wolf).shuffle(inplace=True).do("step") self.simulator.schedule_event_relative(self.step, 1) From 585766b59d0928a6a09737436c20cc9f4c6a5161 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Tue, 13 Feb 2024 17:36:18 +0100 Subject: [PATCH 03/44] time unit check and step scheduling in ABMSimulator --- mesa/experimental/devs/examples/wolf_sheep.py | 3 -- mesa/experimental/devs/simulator.py | 31 ++++++++++++++++++- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/mesa/experimental/devs/examples/wolf_sheep.py b/mesa/experimental/devs/examples/wolf_sheep.py index 9157f3f9501..f438dab0b2f 100644 --- a/mesa/experimental/devs/examples/wolf_sheep.py +++ b/mesa/experimental/devs/examples/wolf_sheep.py @@ -223,12 +223,9 @@ def setup(self): self.simulator.schedule_event_relative(self.step, 1) - - def step(self): self.get_agents_of_type(Sheep).shuffle(inplace=True).do("step") self.get_agents_of_type(Wolf).shuffle(inplace=True).do("step") - self.simulator.schedule_event_relative(self.step, 1) if __name__ == "__main__": diff --git a/mesa/experimental/devs/simulator.py b/mesa/experimental/devs/simulator.py index 0eb230e9f99..c6b800603b5 100644 --- a/mesa/experimental/devs/simulator.py +++ b/mesa/experimental/devs/simulator.py @@ -1,5 +1,6 @@ from .eventlist import EventList, SimEvent, Priority +import numbers class Simulator: @@ -11,13 +12,16 @@ def __init__(self, time_unit, start_time): self.time_unit = time_unit # FIXME currently not used self.model = None + def check_time_unit(self, time): + ... + def setup(self, model): self.event_list.clear() self.model = model model.setup() def reset(self): - pass + raise NotImplementedError def run(self, until=None): # run indefinitely? or until is reached @@ -50,6 +54,9 @@ def cancel_event(self, event): self.event_list.remove(event) def _schedule_event(self, event): + if not self.check_time_unit(event.time): + raise ValueError(f"time unit mismatch {event.time} is not of time unit {self.time_unit}") + # check timeunit of events self.event_list.add_event(event) @@ -59,6 +66,28 @@ def __init__(self): super().__init__(int, 0) + def check_time_unit(self, time): + if isinstance(time, int): + return True + if isinstance(time, float): + return time.is_integer() + + def schedule_event_next_tick(self, function, priority=Priority.DEFAULT, function_args=None, function_kwargs=None) -> SimEvent: + self.schedule_event_relative(function, 1, priority=priority, function_args=function_args, function_kwargs=function_kwargs) + + def step(self): + event = self.event_list.pop() + + if event.fn == self.model.step: + self.schedule_event_next_tick(self.model.step, priority=Priority.DEFAULT) + + self.time = event.time + event.execute() + class DEVSimulator(Simulator): def __init__(self): super().__init__(float, 0.0) + + def check_time_unit(self, time): + return isinstance(time, numbers.Number) + From 2d9bce82fa4079e65eb84f04ab17b75acb853db7 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Thu, 15 Feb 2024 13:27:54 +0100 Subject: [PATCH 04/44] ongoing work --- mesa/experimental/devs/eventlist.py | 17 +++-- mesa/experimental/devs/examples/wolf_sheep.py | 4 +- mesa/experimental/devs/simulator.py | 70 +++++++++++++------ 3 files changed, 64 insertions(+), 27 deletions(-) diff --git a/mesa/experimental/devs/eventlist.py b/mesa/experimental/devs/eventlist.py index e3265987355..1a800cca8ee 100644 --- a/mesa/experimental/devs/eventlist.py +++ b/mesa/experimental/devs/eventlist.py @@ -1,6 +1,7 @@ import itertools from enum import IntEnum from heapq import heapify, heappop, heappush +from weakref import ref class InstanceCounterMeta(type): @@ -28,7 +29,7 @@ class SimEvent(metaclass=InstanceCounterMeta): def __init__(self, time, function, priority=Priority.DEFAULT, function_args=None, function_kwargs=None): super().__init__() self.time = time - self.priority = priority + self.priority = priority.value self.fn = function self.unique_id = next(self._ids) self.function_args = function_args if function_args else [] @@ -38,6 +39,7 @@ def __init__(self, time, function, priority=Priority.DEFAULT, function_args=None raise Exception() def execute(self): + # FIXME handle None correctly self.fn(*self.function_args, **self.function_kwargs) def __cmp__(self, other): @@ -68,7 +70,7 @@ def __init__(self): heapify(self._event_list) def add_event(self, event: SimEvent): - heappush(self._event_list, (event.time, event.priority, event.unique_id, event)) + heappush(self._event_list, (event.time, -event.priority, event.unique_id, event)) def peek_ahead(self, n: int = 1) -> list[SimEvent]: # look n events ahead, or delta time ahead @@ -78,19 +80,24 @@ def peek_ahead(self, n: int = 1) -> list[SimEvent]: def pop(self) -> SimEvent: # return next event - return heappop(self._event_list)[3] + event = heappop(self._event_list) + try: + return event[3] + except IndexError: + pass + def is_empty(self) -> bool: return len(self) == 0 def __contains__(self, event: SimEvent) -> bool: - return (event.time, event.priority, event.unique_id, event) in self._event_list + return (event.time, -event.priority, event.unique_id, event) in self._event_list def __len__(self) -> int: return len(self._event_list) def remove(self, event): - self._event_list.remove((event.time, event.priority, event.unique_id, event)) + self._event_list.remove((event.time, -event.priority, event.unique_id, event)) def clear(self): self._event_list.clear() diff --git a/mesa/experimental/devs/examples/wolf_sheep.py b/mesa/experimental/devs/examples/wolf_sheep.py index f438dab0b2f..045f867883f 100644 --- a/mesa/experimental/devs/examples/wolf_sheep.py +++ b/mesa/experimental/devs/examples/wolf_sheep.py @@ -104,7 +104,7 @@ def fully_grown(self, value): self._fully_grown = value if value == False: - self.model.simulator.schedule_event_relative(self.set_fully_grown, self.grass_regrowth_time) + self.model.simulator.schedule_event_relative(self.set_fully_grown, self.grass_regrowth_time, function_args=[type(self), self.unique_id]) def __init__(self, unique_id, model, fully_grown, countdown, grass_regrowth_time): @@ -120,7 +120,7 @@ def __init__(self, unique_id, model, fully_grown, countdown, grass_regrowth_time self.grass_regrowth_time = grass_regrowth_time if not self.fully_grown: - self.model.simulator.schedule_event_relative(self.set_fully_grown, countdown) + self.model.simulator.schedule_event_relative(self.set_fully_grown, countdown, function_args=[type(self), self.unique_id]) def set_fully_grown(self): self.fully_grown = True diff --git a/mesa/experimental/devs/simulator.py b/mesa/experimental/devs/simulator.py index c6b800603b5..b05280dea1f 100644 --- a/mesa/experimental/devs/simulator.py +++ b/mesa/experimental/devs/simulator.py @@ -1,15 +1,20 @@ +from typing import List, Any, Dict, Callable + from .eventlist import EventList, SimEvent, Priority import numbers + class Simulator: + # FIXME add replication support + # FIXME add experimentation support - def __init__(self, time_unit, start_time): - # should model run in a separate thread + def __init__(self, time_unit: type, start_time: int | float): + # should model run in a separate thread, # and we can then interact with start, stop, run_until, and step? self.event_list = EventList() self.time = start_time - self.time_unit = time_unit # FIXME currently not used + self.time_unit = time_unit self.model = None def check_time_unit(self, time): @@ -23,11 +28,11 @@ def setup(self, model): def reset(self): raise NotImplementedError - def run(self, until=None): + def run(self, until: int | float | None = None): # run indefinitely? or until is reached - endtime = self.time + until - while self.time < endtime: + end_time = self.time + until + while self.time < end_time: self.step() def step(self): @@ -35,25 +40,38 @@ def step(self): self.time = event.time event.execute() - def schedule_event_now(self, function, priority=Priority.DEFAULT, function_args=None, function_kwargs=None) -> SimEvent: - event = SimEvent(self.time, function, priority=priority, function_args=function_args, function_kwargs=function_kwargs) + def schedule_event_now(self, function: Callable, priority: int = Priority.DEFAULT, + function_args: List[Any] | None = None, + function_kwargs: Dict[str, Any] | None = None) -> SimEvent: + event = SimEvent(self.time, function, priority=priority, function_args=function_args, + function_kwargs=function_kwargs) self._schedule_event(event) return event - def schedule_event_absolute(self, function, time, priority=Priority.DEFAULT, function_args=None, function_kwargs=None) -> SimEvent: - event = SimEvent(time, function, priority=priority, function_args=function_args, function_kwargs=function_kwargs) + def schedule_event_absolute(self, + function: Callable, time: int | float, + priority: int = Priority.DEFAULT, + function_args: List[Any] | None = None, + function_kwargs: Dict[str, Any] | None = None) -> SimEvent: + event = SimEvent(time, function, priority=priority, function_args=function_args, + function_kwargs=function_kwargs) self._schedule_event(event) return event - def schedule_event_relative(self, function, time_delta, priority=Priority.DEFAULT, function_args=None, function_kwargs=None) -> SimEvent: - event = SimEvent(self.time + time_delta, function, priority=priority, function_args=function_args, function_kwargs=function_kwargs) + def schedule_event_relative(self, function: Callable, + time_delta: int | float, + priority=Priority.DEFAULT, + function_args: List[Any] | None = None, + function_kwargs: Dict[str, Any] | None = None) -> SimEvent: + event = SimEvent(self.time + time_delta, function, priority=priority, function_args=function_args, + function_kwargs=function_kwargs) self._schedule_event(event) return event - def cancel_event(self, event): + def cancel_event(self, event: SimEvent) -> None: self.event_list.remove(event) - def _schedule_event(self, event): + def _schedule_event(self, event: SimEvent): if not self.check_time_unit(event.time): raise ValueError(f"time unit mismatch {event.time} is not of time unit {self.time_unit}") @@ -66,28 +84,40 @@ def __init__(self): super().__init__(int, 0) - def check_time_unit(self, time): + def setup(self, model): + super().setup(model) + self.schedule_event_now(self.model.step, priority=Priority.HIGH, function_args=["model.step"]) + + + def check_time_unit(self, time) -> bool: if isinstance(time, int): return True if isinstance(time, float): return time.is_integer() + else: + return False - def schedule_event_next_tick(self, function, priority=Priority.DEFAULT, function_args=None, function_kwargs=None) -> SimEvent: - self.schedule_event_relative(function, 1, priority=priority, function_args=function_args, function_kwargs=function_kwargs) + def schedule_event_next_tick(self, function: Callable, priority: int = Priority.DEFAULT, + function_args: List[Any] | None = None, + function_kwargs: Dict[str, Any] | None = None) -> SimEvent: + return self.schedule_event_relative(function, 1, + priority=priority, + function_args=function_args, + function_kwargs=function_kwargs) def step(self): event = self.event_list.pop() if event.fn == self.model.step: - self.schedule_event_next_tick(self.model.step, priority=Priority.DEFAULT) + self.schedule_event_next_tick(self.model.step, priority=Priority.HIGH, function_args=["model.step"]) self.time = event.time event.execute() + class DEVSimulator(Simulator): def __init__(self): super().__init__(float, 0.0) - def check_time_unit(self, time): + def check_time_unit(self, time) -> bool: return isinstance(time, numbers.Number) - From 08de4ff9a403f558998bb799364d513c094f1b20 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Fri, 16 Feb 2024 11:05:36 +0100 Subject: [PATCH 05/44] various small tweaks --- mesa/experimental/devs/eventlist.py | 48 ++++++++----- mesa/experimental/devs/examples/wolf_sheep.py | 11 +-- mesa/experimental/devs/simulator.py | 70 +++++++++++++------ 3 files changed, 83 insertions(+), 46 deletions(-) diff --git a/mesa/experimental/devs/eventlist.py b/mesa/experimental/devs/eventlist.py index 1a800cca8ee..0e9dc7f0222 100644 --- a/mesa/experimental/devs/eventlist.py +++ b/mesa/experimental/devs/eventlist.py @@ -1,8 +1,9 @@ import itertools from enum import IntEnum from heapq import heapify, heappop, heappush -from weakref import ref +from weakref import ref, WeakMethod +from types import MethodType class InstanceCounterMeta(type): """ Metaclass to make instance counter not share count with descendants @@ -16,20 +17,27 @@ def __init__(cls, name, bases, attrs): class Priority(IntEnum): - LOW = 1 - HIGH = 10 + LOW = 10 DEFAULT = 5 + HIGH = 1 -class SimEvent(metaclass=InstanceCounterMeta): + +class SimulationEvent(metaclass=InstanceCounterMeta): # fixme:: how do we want to handle function? # should be a callable, possibly on an object # also we want only weakrefs to agents - def __init__(self, time, function, priority=Priority.DEFAULT, function_args=None, function_kwargs=None): + def __init__(self, time, function, priority: Priority = Priority.DEFAULT, function_args=None, function_kwargs=None): super().__init__() self.time = time self.priority = priority.value + + if isinstance(function, MethodType): + function = WeakMethod(function) + else: + function = ref(function) + self.fn = function self.unique_id = next(self._ids) self.function_args = function_args if function_args else [] @@ -39,8 +47,9 @@ def __init__(self, time, function, priority=Priority.DEFAULT, function_args=None raise Exception() def execute(self): - # FIXME handle None correctly - self.fn(*self.function_args, **self.function_kwargs) + fn = self.fn() + if fn is not None: + fn(*self.function_args, **self.function_kwargs) def __cmp__(self, other): if self.time < other.time: @@ -62,42 +71,43 @@ def __cmp__(self, other): # exact same event return 0 + def to_tuple(self): + return self.time, self.priority, self.unique_id, self + class EventList: def __init__(self): super().__init__() - self._event_list: list[SimEvent] = [] + self._event_list: list[tuple] = [] heapify(self._event_list) - def add_event(self, event: SimEvent): - heappush(self._event_list, (event.time, -event.priority, event.unique_id, event)) + def add_event(self, event: SimulationEvent): + heappush(self._event_list, event.to_tuple()) - def peek_ahead(self, n: int = 1) -> list[SimEvent]: + def peek_ahead(self, n: int = 1) -> list[SimulationEvent]: # look n events ahead, or delta time ahead if self.is_empty(): raise IndexError("event list is empty") return [entry[3] for entry in self._event_list[0:n]] - def pop(self) -> SimEvent: - # return next event - event = heappop(self._event_list) + def pop(self) -> SimulationEvent: try: - return event[3] + return heappop(self._event_list)[3] except IndexError: - pass + raise Exception("event list is empty") def is_empty(self) -> bool: return len(self) == 0 - def __contains__(self, event: SimEvent) -> bool: - return (event.time, -event.priority, event.unique_id, event) in self._event_list + def __contains__(self, event: SimulationEvent) -> bool: + return event.to_tuple() in self._event_list def __len__(self) -> int: return len(self._event_list) def remove(self, event): - self._event_list.remove((event.time, -event.priority, event.unique_id, event)) + self._event_list.remove(event.to_tuple) def clear(self): self._event_list.clear() diff --git a/mesa/experimental/devs/examples/wolf_sheep.py b/mesa/experimental/devs/examples/wolf_sheep.py index 045f867883f..619be93013b 100644 --- a/mesa/experimental/devs/examples/wolf_sheep.py +++ b/mesa/experimental/devs/examples/wolf_sheep.py @@ -104,7 +104,7 @@ def fully_grown(self, value): self._fully_grown = value if value == False: - self.model.simulator.schedule_event_relative(self.set_fully_grown, self.grass_regrowth_time, function_args=[type(self), self.unique_id]) + self.model.simulator.schedule_event_relative(self.set_fully_grown, self.grass_regrowth_time) def __init__(self, unique_id, model, fully_grown, countdown, grass_regrowth_time): @@ -120,7 +120,7 @@ def __init__(self, unique_id, model, fully_grown, countdown, grass_regrowth_time self.grass_regrowth_time = grass_regrowth_time if not self.fully_grown: - self.model.simulator.schedule_event_relative(self.set_fully_grown, countdown, function_args=[type(self), self.unique_id]) + self.model.simulator.schedule_event_relative(self.set_fully_grown, countdown ) def set_fully_grown(self): self.fully_grown = True @@ -136,7 +136,6 @@ class WolfSheep(mesa.Model): def __init__( self, - seed, height, width, initial_sheep, @@ -148,6 +147,7 @@ def __init__( sheep_gain_from_food=5, moore=False, simulator=None, + seed=None, ): """ Create a new Wolf-Sheep model with the given parameters. @@ -233,11 +233,12 @@ def step(self): simulator = ABMSimulator() - model = WolfSheep(15, 25, 25, 60, 40, 0.2, 0.1, 20, - simulator=simulator) + model = WolfSheep(25, 25, 60, 40, 0.2, 0.1, 20, + simulator=simulator, seed=15) simulator.setup(model) start_time = time.perf_counter() simulator.run(until=100) + print(simulator.time) print("Time:", time.perf_counter() - start_time) \ No newline at end of file diff --git a/mesa/experimental/devs/simulator.py b/mesa/experimental/devs/simulator.py index b05280dea1f..934e1377608 100644 --- a/mesa/experimental/devs/simulator.py +++ b/mesa/experimental/devs/simulator.py @@ -1,6 +1,6 @@ from typing import List, Any, Dict, Callable -from .eventlist import EventList, SimEvent, Priority +from .eventlist import EventList, SimulationEvent, Priority import numbers @@ -40,38 +40,38 @@ def step(self): self.time = event.time event.execute() - def schedule_event_now(self, function: Callable, priority: int = Priority.DEFAULT, + def schedule_event_now(self, function: Callable, priority: Priority = Priority.DEFAULT, function_args: List[Any] | None = None, - function_kwargs: Dict[str, Any] | None = None) -> SimEvent: - event = SimEvent(self.time, function, priority=priority, function_args=function_args, - function_kwargs=function_kwargs) + function_kwargs: Dict[str, Any] | None = None) -> SimulationEvent: + event = SimulationEvent(self.time, function, priority=priority, function_args=function_args, + function_kwargs=function_kwargs) self._schedule_event(event) return event def schedule_event_absolute(self, function: Callable, time: int | float, - priority: int = Priority.DEFAULT, + priority: Priority = Priority.DEFAULT, function_args: List[Any] | None = None, - function_kwargs: Dict[str, Any] | None = None) -> SimEvent: - event = SimEvent(time, function, priority=priority, function_args=function_args, - function_kwargs=function_kwargs) + function_kwargs: Dict[str, Any] | None = None) -> SimulationEvent: + event = SimulationEvent(time, function, priority=priority, function_args=function_args, + function_kwargs=function_kwargs) self._schedule_event(event) return event def schedule_event_relative(self, function: Callable, time_delta: int | float, - priority=Priority.DEFAULT, + priority: Priority = Priority.DEFAULT, function_args: List[Any] | None = None, - function_kwargs: Dict[str, Any] | None = None) -> SimEvent: - event = SimEvent(self.time + time_delta, function, priority=priority, function_args=function_args, - function_kwargs=function_kwargs) + function_kwargs: Dict[str, Any] | None = None) -> SimulationEvent: + event = SimulationEvent(self.time + time_delta, function, priority=priority, function_args=function_args, + function_kwargs=function_kwargs) self._schedule_event(event) return event - def cancel_event(self, event: SimEvent) -> None: + def cancel_event(self, event: SimulationEvent) -> None: self.event_list.remove(event) - def _schedule_event(self, event: SimEvent): + def _schedule_event(self, event: SimulationEvent): if not self.check_time_unit(event.time): raise ValueError(f"time unit mismatch {event.time} is not of time unit {self.time_unit}") @@ -80,14 +80,21 @@ def _schedule_event(self, event: SimEvent): class ABMSimulator(Simulator): + """This simulator uses incremental time progression, while allowing for additional event scheduling. + + The basic time unit of this simulator is an integer. It schedules `model.step` for each tick with the + highest priority. This implies that by default, `model.step` is the first event executed at a specific tick. + In addition, discrete event scheduling, using integers as the time unit is fully supported, paving the way + for hybrid ABM-DEVS simulations. + + """ + def __init__(self): super().__init__(int, 0) - def setup(self, model): super().setup(model) - self.schedule_event_now(self.model.step, priority=Priority.HIGH, function_args=["model.step"]) - + self.schedule_event_now(self.model.step, priority=Priority.HIGH) def check_time_unit(self, time) -> bool: if isinstance(time, int): @@ -97,19 +104,38 @@ def check_time_unit(self, time) -> bool: else: return False - def schedule_event_next_tick(self, function: Callable, priority: int = Priority.DEFAULT, + def schedule_event_next_tick(self, + function: Callable, + priority: Priority = Priority.DEFAULT, function_args: List[Any] | None = None, - function_kwargs: Dict[str, Any] | None = None) -> SimEvent: + function_kwargs: Dict[str, Any] | None = None) -> SimulationEvent: + """Schedule a SimulationEvent for the next tick + + Args + function (Callable): the callable to execute + priority (Priority): the priority of the event + function_args (List[Any]): List of arguments to pass to the callable + function_kwargs (Dict[str, Any]): List of keyword arguments to pass to the callable + + """ return self.schedule_event_relative(function, 1, priority=priority, function_args=function_args, function_kwargs=function_kwargs) def step(self): + """get the next event from the event list and execute it. + + Note + if the event to execute is `model.step`, this method automatically also + schedules a new `model.step` event for the next time tick. This ensures + incremental time progression. + + """ event = self.event_list.pop() - if event.fn == self.model.step: - self.schedule_event_next_tick(self.model.step, priority=Priority.HIGH, function_args=["model.step"]) + if event.fn() == self.model.step: + self.schedule_event_next_tick(self.model.step, priority=Priority.HIGH) self.time = event.time event.execute() From f2155e5510f9596587fe86c86cf0e187302b446a Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sat, 17 Feb 2024 19:53:06 +0100 Subject: [PATCH 06/44] Epstein Civil Violence as hybrid ABM DES --- .../devs/examples/epstein_civil_violence.py | 286 ++++++++++++++++++ tests/test_examples.py | 4 +- 2 files changed, 288 insertions(+), 2 deletions(-) create mode 100644 mesa/experimental/devs/examples/epstein_civil_violence.py diff --git a/mesa/experimental/devs/examples/epstein_civil_violence.py b/mesa/experimental/devs/examples/epstein_civil_violence.py new file mode 100644 index 00000000000..7b815296ce5 --- /dev/null +++ b/mesa/experimental/devs/examples/epstein_civil_violence.py @@ -0,0 +1,286 @@ +import math +import enum + +import mesa.agent + +from mesa.agent import AgentSet +from mesa.space import SingleGrid +from mesa import Agent, Model + +from mesa.experimental.devs.simulator import ABMSimulator + + +class EpsteinAgent(Agent): + def __init__(self, unique_id, model, vision): + super().__init__(unique_id, model) + self.vision = vision + + +class AgentState(enum.IntEnum): + QUIESCENT = enum.auto() + ARRESTED = enum.auto() + ACTIVE = enum.auto() + + +class Citizen(EpsteinAgent): + """ + A member of the general population, may or may not be in active rebellion. + Summary of rule: If grievance - risk > threshold, rebel. + + Attributes: + unique_id: unique int + model : + hardship: Agent's 'perceived hardship (i.e., physical or economic + privation).' Exogenous, drawn from U(0,1). + regime_legitimacy: Agent's perception of regime legitimacy, equal + across agents. Exogenous. + risk_aversion: Exogenous, drawn from U(0,1). + threshold: if (grievance - (risk_aversion * arrest_probability)) > + threshold, go/remain Active + vision: number of cells in each direction (N, S, E and W) that agent + can inspect + condition: Can be "Quiescent" or "Active;" deterministic function of + greivance, perceived risk, and + grievance: deterministic function of hardship and regime_legitimacy; + how aggrieved is agent at the regime? + arrest_probability: agent's assessment of arrest probability, given + rebellion + """ + + @property + def jail_sentence(self): + return self._jail_sentence + + @jail_sentence.setter + def jail_sentence(self, value): + self.model.active_agents.remove(self) + self.model.simulator.schedule_event_relative(self.release_from_jail, value) + self._jail_sentence = value + self.condition = AgentState.ARRESTED + + def release_from_jail(self): + self._jail_sentence = 0 + self.model.active_agents.add(self) + self.condition = AgentState.QUIESCENT + + def __init__( + self, + unique_id, + model, + hardship, + regime_legitimacy, + risk_aversion, + threshold, + vision, + ): + """ + Create a new Citizen. + Args: + unique_id: unique int + model : model instance + hardship: Agent's 'perceived hardship (i.e., physical or economic + privation).' Exogenous, drawn from U(0,1). + regime_legitimacy: Agent's perception of regime legitimacy, equal + across agents. Exogenous. + risk_aversion: Exogenous, drawn from U(0,1). + threshold: if (grievance - (risk_aversion * arrest_probability)) > + threshold, go/remain Active + vision: number of cells in each direction (N, S, E and W) that + agent can inspect. Exogenous. + """ + super().__init__(unique_id, model, vision) + self.hardship = hardship + self.regime_legitimacy = regime_legitimacy + self.risk_aversion = risk_aversion + self.threshold = threshold + self.condition = AgentState.QUIESCENT + self.grievance = self.hardship * (1 - self.regime_legitimacy) + self.arrest_probability = None + self._jail_sentence = 0 + + def step(self): + """ + Decide whether to activate, then move if applicable. + """ + self.update_neighbors() + self.update_estimated_arrest_probability() + net_risk = self.risk_aversion * self.arrest_probability + if self.grievance - net_risk > self.threshold: + self.condition = AgentState.ACTIVE + else: + self.condition = AgentState.QUIESCENT + if self.model.movement and self.empty_neighbors: + new_pos = self.random.choice(self.empty_neighbors) + self.model.grid.move_agent(self, new_pos) + + def update_neighbors(self): + """ + Look around and see who my neighbors are + """ + self.neighborhood = self.model.grid.get_neighborhood( + self.pos, moore=True, radius=self.vision + ) + self.neighbors = self.model.grid.get_cell_list_contents(self.neighborhood) + self.empty_neighbors = [ + c for c in self.neighborhood if self.model.grid.is_cell_empty(c) + ] + + def update_estimated_arrest_probability(self): + """ + Based on the ratio of cops to actives in my neighborhood, estimate the + p(Arrest | I go active). + """ + cops_in_vision = len([c for c in self.neighbors if isinstance(c, Cop)]) + actives_in_vision = 1.0 # citizen counts herself + for c in self.neighbors: + if ( + isinstance(c, Citizen) + and c.condition == AgentState.ACTIVE + ): + actives_in_vision += 1 + self.arrest_probability = 1 - math.exp( + -1 * self.model.arrest_prob_constant * (cops_in_vision / actives_in_vision) + ) + + +class Cop(EpsteinAgent): + """ + A cop for life. No defection. + Summary of rule: Inspect local vision and arrest a random active agent. + + Attributes: + unique_id: unique int + x, y: Grid coordinates + vision: number of cells in each direction (N, S, E and W) that cop is + able to inspect + """ + + def step(self): + """ + Inspect local vision and arrest a random active agent. Move if + applicable. + """ + self.update_neighbors() + active_neighbors = [] + for agent in self.neighbors: + if ( + isinstance(agent, Citizen) + and agent.condition == "Active" + ): + active_neighbors.append(agent) + if active_neighbors: + arrestee = self.random.choice(active_neighbors) + arrestee.jail_sentence = self.random.randint(0, self.model.max_jail_term) + if self.model.movement and self.empty_neighbors: + new_pos = self.random.choice(self.empty_neighbors) + self.model.grid.move_agent(self, new_pos) + + def update_neighbors(self): + """ + Look around and see who my neighbors are. + """ + self.neighborhood = self.model.grid.get_neighborhood( + self.pos, moore=True, radius=self.vision + ) + self.neighbors = self.model.grid.get_cell_list_contents(self.neighborhood) + self.empty_neighbors = [ + c for c in self.neighborhood if self.model.grid.is_cell_empty(c) + ] + + +class EpsteinCivilViolence(Model): + """ + Model 1 from "Modeling civil violence: An agent-based computational + approach," by Joshua Epstein. + http://www.pnas.org/content/99/suppl_3/7243.full + Attributes: + height: grid height + width: grid width + citizen_density: approximate % of cells occupied by citizens. + cop_density: approximate % of cells occupied by cops. + citizen_vision: number of cells in each direction (N, S, E and W) that + citizen can inspect + cop_vision: number of cells in each direction (N, S, E and W) that cop + can inspect + legitimacy: (L) citizens' perception of regime legitimacy, equal + across all citizens + max_jail_term: (J_max) + active_threshold: if (grievance - (risk_aversion * arrest_probability)) + > threshold, citizen rebels + arrest_prob_constant: set to ensure agents make plausible arrest + probability estimates + movement: binary, whether agents try to move at step end + max_iters: model may not have a natural stopping point, so we set a + max. + """ + + def __init__( + self, + width=40, + height=40, + citizen_density=0.7, + cop_density=0.074, + citizen_vision=7, + cop_vision=7, + legitimacy=0.8, + max_jail_term=1000, + active_threshold=0.1, + arrest_prob_constant=2.3, + movement=True, + max_iters=1000, + seed=None + ): + super().__init__(seed) + if cop_density + citizen_density > 1: + raise ValueError("Cop density + citizen density must be less than 1") + + self.width = width + self.height = height + self.citizen_density = citizen_density + self.cop_density = cop_density + self.legitimacy = legitimacy + self.max_jail_term = max_jail_term + self.active_threshold = active_threshold + self.arrest_prob_constant = arrest_prob_constant + self.movement = movement + self.max_iters = max_iters + self.cop_vision = cop_vision + self.citizen_vision = citizen_vision + self.active_agents: AgentSet | None = None + self.grid = None + + + + def setup(self): + self.grid = SingleGrid(self.width, self.height, torus=True) + + for contents, pos in self.grid.coord_iter(): + if self.random.random() < self.cop_density: + agent = Cop(self.next_id(), self, vision=self.cop_vision) + elif self.random.random() < (self.cop_density + self.citizen_density): + agent = Citizen( + self.next_id(), + self, + hardship=self.random.random(), + regime_legitimacy=self.legitimacy, + risk_aversion=self.random.random(), + threshold=self.active_threshold, + vision=self.citizen_vision, + ) + else: + continue + self.grid.place_agent(agent, pos) + + self.active_agents = self.agents + + def step(self): + self.active_agents.shuffle(inplace=True).do("step") + + +if __name__ == '__main__': + model = EpsteinCivilViolence(seed=15) + simulator = ABMSimulator() + + simulator.setup(model) + + simulator.run(until=100) diff --git a/tests/test_examples.py b/tests/test_examples.py index 1c149da4b75..e5c0381f065 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -51,12 +51,12 @@ def test_examples(self): print(f"testing example {example!r}") with self.active_example_dir(example): try: - # model.py at the top level + # epstein_civil_violence.py at the top level mod = importlib.import_module("model") server = importlib.import_module("server") server.server.render_model() except ImportError: - # /model.py + # /epstein_civil_violence.py mod = importlib.import_module(f"{example.replace('-', '_')}.model") server = importlib.import_module( f"{example.replace('-', '_')}.server" From b7457c551b0ce175f0c7ca34a67922003314baa4 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sat, 17 Feb 2024 20:01:42 +0100 Subject: [PATCH 07/44] Update epstein_civil_violence.py --- .../devs/examples/epstein_civil_violence.py | 54 +++++++++---------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/mesa/experimental/devs/examples/epstein_civil_violence.py b/mesa/experimental/devs/examples/epstein_civil_violence.py index 7b815296ce5..be603348c8c 100644 --- a/mesa/experimental/devs/examples/epstein_civil_violence.py +++ b/mesa/experimental/devs/examples/epstein_civil_violence.py @@ -1,19 +1,17 @@ -import math import enum +import math -import mesa.agent - -from mesa.agent import AgentSet -from mesa.space import SingleGrid from mesa import Agent, Model - +from mesa.agent import AgentSet from mesa.experimental.devs.simulator import ABMSimulator +from mesa.space import SingleGrid class EpsteinAgent(Agent): - def __init__(self, unique_id, model, vision): + def __init__(self, unique_id, model, vision, movement): super().__init__(unique_id, model) self.vision = vision + self.movement = movement class AgentState(enum.IntEnum): @@ -47,19 +45,13 @@ class Citizen(EpsteinAgent): rebellion """ - @property - def jail_sentence(self): - return self._jail_sentence - @jail_sentence.setter - def jail_sentence(self, value): + def sent_to_jail(self, value): self.model.active_agents.remove(self) - self.model.simulator.schedule_event_relative(self.release_from_jail, value) - self._jail_sentence = value self.condition = AgentState.ARRESTED + self.model.simulator.schedule_event_relative(self.release_from_jail, value) def release_from_jail(self): - self._jail_sentence = 0 self.model.active_agents.add(self) self.condition = AgentState.QUIESCENT @@ -67,11 +59,13 @@ def __init__( self, unique_id, model, + vision, + movement, hardship, regime_legitimacy, risk_aversion, threshold, - vision, + arrest_prob_constant, ): """ Create a new Citizen. @@ -88,7 +82,7 @@ def __init__( vision: number of cells in each direction (N, S, E and W) that agent can inspect. Exogenous. """ - super().__init__(unique_id, model, vision) + super().__init__(unique_id, model, vision, movement) self.hardship = hardship self.regime_legitimacy = regime_legitimacy self.risk_aversion = risk_aversion @@ -96,7 +90,7 @@ def __init__( self.condition = AgentState.QUIESCENT self.grievance = self.hardship * (1 - self.regime_legitimacy) self.arrest_probability = None - self._jail_sentence = 0 + self.arrest_prob_constant = arrest_prob_constant def step(self): """ @@ -109,7 +103,7 @@ def step(self): self.condition = AgentState.ACTIVE else: self.condition = AgentState.QUIESCENT - if self.model.movement and self.empty_neighbors: + if self.movement and self.empty_neighbors: new_pos = self.random.choice(self.empty_neighbors) self.model.grid.move_agent(self, new_pos) @@ -139,7 +133,7 @@ def update_estimated_arrest_probability(self): ): actives_in_vision += 1 self.arrest_probability = 1 - math.exp( - -1 * self.model.arrest_prob_constant * (cops_in_vision / actives_in_vision) + -1 * self.arrest_prob_constant * (cops_in_vision / actives_in_vision) ) @@ -155,6 +149,10 @@ class Cop(EpsteinAgent): able to inspect """ + def __init__(self, unique_id, model, vision, movement, max_jail_term): + super().__init__(unique_id, model, vision, movement) + self.max_jail_term = max_jail_term + def step(self): """ Inspect local vision and arrest a random active agent. Move if @@ -170,8 +168,8 @@ def step(self): active_neighbors.append(agent) if active_neighbors: arrestee = self.random.choice(active_neighbors) - arrestee.jail_sentence = self.random.randint(0, self.model.max_jail_term) - if self.model.movement and self.empty_neighbors: + arrestee.sent_to_jail(self.random.randint(0, self.max_jail_term)) + if self.movement and self.empty_neighbors: new_pos = self.random.choice(self.empty_neighbors) self.model.grid.move_agent(self, new_pos) @@ -249,23 +247,23 @@ def __init__( self.active_agents: AgentSet | None = None self.grid = None - - def setup(self): self.grid = SingleGrid(self.width, self.height, torus=True) - for contents, pos in self.grid.coord_iter(): + for _, pos in self.grid.coord_iter(): if self.random.random() < self.cop_density: - agent = Cop(self.next_id(), self, vision=self.cop_vision) + agent = Cop(self.next_id(), self, self.cop_vision, self.movement, self.max_jail_term) elif self.random.random() < (self.cop_density + self.citizen_density): agent = Citizen( self.next_id(), self, + self.citizen_vision, + self.movement, hardship=self.random.random(), regime_legitimacy=self.legitimacy, risk_aversion=self.random.random(), threshold=self.active_threshold, - vision=self.citizen_vision, + arrest_prob_constant=self.arrest_prob_constant ) else: continue @@ -277,7 +275,7 @@ def step(self): self.active_agents.shuffle(inplace=True).do("step") -if __name__ == '__main__': +if __name__ == "__main__": model = EpsteinCivilViolence(seed=15) simulator = ABMSimulator() From cf9efc1817dc18b9249321694bb3dc817b95f555 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sat, 24 Feb 2024 09:40:13 +0100 Subject: [PATCH 08/44] docstrings --- mesa/experimental/devs/__init__.py | 8 +++ mesa/experimental/devs/eventlist.py | 47 ++++++++++++-- mesa/experimental/devs/simulator.py | 97 ++++++++++++++++++++++++++--- 3 files changed, 137 insertions(+), 15 deletions(-) diff --git a/mesa/experimental/devs/__init__.py b/mesa/experimental/devs/__init__.py index e69de29bb2d..48a6d500552 100644 --- a/mesa/experimental/devs/__init__.py +++ b/mesa/experimental/devs/__init__.py @@ -0,0 +1,8 @@ +from simulator import ABMSimulator, DEVSimulator +from eventlist import SimulationEvent, Priority + + +__all__ = ["ABMSimulator", + "DEVSimulator", + "SimulationEvent", + "Priority"] \ No newline at end of file diff --git a/mesa/experimental/devs/eventlist.py b/mesa/experimental/devs/eventlist.py index 0e9dc7f0222..f9b5f02f57b 100644 --- a/mesa/experimental/devs/eventlist.py +++ b/mesa/experimental/devs/eventlist.py @@ -8,7 +8,7 @@ class InstanceCounterMeta(type): """ Metaclass to make instance counter not share count with descendants - FIXME:: can also be used for agents + TODO:: can also be used for agents.unique_id """ def __init__(cls, name, bases, attrs): @@ -22,11 +22,18 @@ class Priority(IntEnum): HIGH = 1 - class SimulationEvent(metaclass=InstanceCounterMeta): - # fixme:: how do we want to handle function? - # should be a callable, possibly on an object - # also we want only weakrefs to agents + """A simulation event + + Attributes: + time (float): The simulation time of the event + priority (Priority): The priority of the event + fn (Callable): The function to execute for this event + unique_id (int) the unique identifier of the event + function_args (list[Any]): Argument for the function + function_kwargs (Dict[str, Any]): Keyword arguments for the function + + """ def __init__(self, time, function, priority: Priority = Priority.DEFAULT, function_args=None, function_kwargs=None): super().__init__() @@ -76,21 +83,48 @@ def to_tuple(self): class EventList: + """An event list + + This is a heap queue sorted list of events. Events are allways removed from the left. The events are sorted + based on their time stamp, their priority, and their unique_id, guaranteeing a complete ordering. + + """ + def __init__(self): super().__init__() self._event_list: list[tuple] = [] heapify(self._event_list) def add_event(self, event: SimulationEvent): + """Add the event to the event list + + Args: + event (SimulationEvent): The event to be added + + """ + heappush(self._event_list, event.to_tuple()) def peek_ahead(self, n: int = 1) -> list[SimulationEvent]: - # look n events ahead, or delta time ahead + """Look at the first n event in the event list + + Args: + n (int): The number of events to look ahead + + Returns: + list[SimulationEvent] + + """ + # look n events ahead if self.is_empty(): raise IndexError("event list is empty") return [entry[3] for entry in self._event_list[0:n]] def pop(self) -> SimulationEvent: + """pop the first element from the event list + + """ + try: return heappop(self._event_list)[3] except IndexError: @@ -107,6 +141,7 @@ def __len__(self) -> int: return len(self._event_list) def remove(self, event): + """remove an event from the event list""" self._event_list.remove(event.to_tuple) def clear(self): diff --git a/mesa/experimental/devs/simulator.py b/mesa/experimental/devs/simulator.py index 934e1377608..a95449ab531 100644 --- a/mesa/experimental/devs/simulator.py +++ b/mesa/experimental/devs/simulator.py @@ -6,8 +6,21 @@ class Simulator: - # FIXME add replication support - # FIXME add experimentation support + """The Simulator controls the time advancement of the model. + + The simulator uses next event time progression to advance the simulation time, and execute the next event + + Attributes: + event_list (EventList): The list of events to execute + time (float | int): The current simulation time + time_unit (type) : The unit of the simulation time + model (Model): The model to simulate + + + """ + + # TODO: add replication support + # TODO: add experimentation support def __init__(self, time_unit: type, start_time: int | float): # should model run in a separate thread, @@ -17,21 +30,40 @@ def __init__(self, time_unit: type, start_time: int | float): self.time_unit = time_unit self.model = None - def check_time_unit(self, time): + def check_time_unit(self, time: int | float) -> bool: ... - def setup(self, model): + def setup(self, model: "Model") -> None: + """Setup the model to simulate + + Args: + model (Model): The model to simulate + + Notes: + The basic assumption of the simulator is that a Model has a model.setup method that sets up the + model. + + """ + self.event_list.clear() self.model = model model.setup() def reset(self): - raise NotImplementedError + """Reset the simulator by clearing the event list and removing the model to simulate""" + self.event_list.clear() + self.model = None + + def run(self, time_delta: int | float): + """run the simulator for time delta - def run(self, until: int | float | None = None): - # run indefinitely? or until is reached + 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 + until + end_time = self.time + time_delta while self.time < end_time: self.step() @@ -43,16 +75,43 @@ def step(self): def schedule_event_now(self, function: Callable, priority: Priority = Priority.DEFAULT, function_args: List[Any] | None = None, function_kwargs: Dict[str, Any] | None = None) -> SimulationEvent: + """Schedule event for the current time instant + + Args: + function (Callable): The callable to execute for this event + priority (Priority): the priority of the event, optional + function_args (List[Any]): list of arguments for function + function_kwargs (Dict[str, Any]): dict of keyword arguments for function + + Returns: + SimulationEvent: the simulation event that is scheduled + + """ + event = SimulationEvent(self.time, function, priority=priority, function_args=function_args, function_kwargs=function_kwargs) self._schedule_event(event) return event def schedule_event_absolute(self, - function: Callable, time: int | float, + function: Callable, + time: int | float, priority: Priority = Priority.DEFAULT, function_args: List[Any] | None = None, function_kwargs: Dict[str, Any] | None = None) -> SimulationEvent: + """Schedule event for the specified time instant + + Args: + function (Callable): The callable to execute for this event + time (int | float): the time for which to schedule the event + priority (Priority): the priority of the event, optional + function_args (List[Any]): list of arguments for function + function_kwargs (Dict[str, Any]): dict of keyword arguments for function + + Returns: + SimulationEvent: the simulation event that is scheduled + + """ event = SimulationEvent(time, function, priority=priority, function_args=function_args, function_kwargs=function_kwargs) self._schedule_event(event) @@ -63,12 +122,32 @@ def schedule_event_relative(self, function: Callable, priority: Priority = Priority.DEFAULT, function_args: List[Any] | None = None, function_kwargs: Dict[str, Any] | None = None) -> SimulationEvent: + """Schedule event for the current time plus the time delta + + Args: + function (Callable): The callable to execute for this event + time_delta (int | float): the time delta + priority (Priority): the priority of the event, optional + function_args (List[Any]): list of arguments for function + function_kwargs (Dict[str, Any]): dict of keyword arguments for function + + Returns: + SimulationEvent: the simulation event that is scheduled + + """ event = SimulationEvent(self.time + time_delta, function, priority=priority, function_args=function_args, function_kwargs=function_kwargs) self._schedule_event(event) return event def cancel_event(self, event: SimulationEvent) -> None: + """remove the event from the event list + + Args: + event (SimulationEvent): The simulation event to remove + + """ + self.event_list.remove(event) def _schedule_event(self, event: SimulationEvent): From 790c1e680ae9be8f0f103e28f07aa822a94055f6 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sat, 24 Feb 2024 11:58:23 +0100 Subject: [PATCH 09/44] correct handling of canceling events --- mesa/experimental/devs/eventlist.py | 45 +++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/mesa/experimental/devs/eventlist.py b/mesa/experimental/devs/eventlist.py index f9b5f02f57b..a0a12481889 100644 --- a/mesa/experimental/devs/eventlist.py +++ b/mesa/experimental/devs/eventlist.py @@ -5,6 +5,7 @@ from types import MethodType + class InstanceCounterMeta(type): """ Metaclass to make instance counter not share count with descendants @@ -35,6 +36,17 @@ class SimulationEvent(metaclass=InstanceCounterMeta): """ + @property + def CANCELED(self) -> bool: + return self._canceled + + @CANCELED.setter + def CANCELED(self, bool: bool) -> None: + self._canceled = bool + self.fn = None + self.function_args = [] + self.function_kwargs = {} + def __init__(self, time, function, priority: Priority = Priority.DEFAULT, function_args=None, function_kwargs=None): super().__init__() self.time = time @@ -106,7 +118,7 @@ def add_event(self, event: SimulationEvent): heappush(self._event_list, event.to_tuple()) def peek_ahead(self, n: int = 1) -> list[SimulationEvent]: - """Look at the first n event in the event list + """Look at the first n non-canceled event in the event list Args: n (int): The number of events to look ahead @@ -114,11 +126,26 @@ def peek_ahead(self, n: int = 1) -> list[SimulationEvent]: Returns: list[SimulationEvent] + Raises: + IndexError: If the eventlist is empty + + Notes: + this method can return a list shorted then n if the number of non-canceled events on the event list + is less than n. + """ # look n events ahead if self.is_empty(): raise IndexError("event list is empty") - return [entry[3] for entry in self._event_list[0:n]] + + peek: list[SimulationEvent] = [] + for entry in self._event_list: + sim_event: SimulationEvent = entry[3] + if not sim_event.CANCELED: + peek.append(sim_event) + if len(peek) >= n: + return peek + return peek def pop(self) -> SimulationEvent: """pop the first element from the event list @@ -126,11 +153,13 @@ def pop(self) -> SimulationEvent: """ try: - return heappop(self._event_list)[3] + while True: + sim_event = heappop(self._event_list)[3] + if not sim_event.CANCELED: + return sim_event except IndexError: raise Exception("event list is empty") - def is_empty(self) -> bool: return len(self) == 0 @@ -140,9 +169,13 @@ def __contains__(self, event: SimulationEvent) -> bool: def __len__(self) -> int: return len(self._event_list) - def remove(self, event): + def remove(self, event: SimulationEvent) -> None: """remove an event from the event list""" - self._event_list.remove(event.to_tuple) + # we cannot simply remove items from _eventlist because this breaks + # heap structure invariant. So, we use a form of lazy deletion. + # SimEvents have a CANCELED flag that we set to True, while poping and peak_ahead + # silently ignore canceled events + event.CANCELED = True def clear(self): self._event_list.clear() From 9aabc48330d09228e46cc17ec5182be3b6df5cf2 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Thu, 29 Feb 2024 19:51:47 +0100 Subject: [PATCH 10/44] docstrings, update to benchmark minor cleaning of public api --- benchmarks/WolfSheep/wolf_sheep.py | 96 +++++++++++-------- mesa/experimental/devs/__init__.py | 7 +- mesa/experimental/devs/eventlist.py | 44 +++++---- mesa/experimental/devs/examples/wolf_sheep.py | 8 +- mesa/experimental/devs/simulator.py | 35 ++++--- 5 files changed, 109 insertions(+), 81 deletions(-) diff --git a/benchmarks/WolfSheep/wolf_sheep.py b/benchmarks/WolfSheep/wolf_sheep.py index 11e07a9e954..26361a54586 100644 --- a/benchmarks/WolfSheep/wolf_sheep.py +++ b/benchmarks/WolfSheep/wolf_sheep.py @@ -13,7 +13,7 @@ from mesa import Model from mesa.experimental.cell_space import CellAgent, OrthogonalVonNeumannGrid -from mesa.time import RandomActivationByType +from mesa.experimental.devs import ABMSimulator class Animal(CellAgent): @@ -36,7 +36,6 @@ def spawn_offspring(self): self.energy_from_food, ) offspring.move_to(self.cell) - self.model.schedule.add(offspring) def feed(self): ... @@ -94,26 +93,34 @@ class GrassPatch(CellAgent): A patch of grass that grows at a fixed rate and it is eaten by sheep """ - def __init__(self, unique_id, model, fully_grown, countdown): + @property + def fully_grown(self): + return self._fully_grown + + @fully_grown.setter + def fully_grown(self, value: bool) -> None: + self._fully_grown = value + + if not value: + self.model.simulator.schedule_event_relative(self.set_fully_grown, self.grass_regrowth_time) + + def __init__(self, unique_id, model, fully_grown, countdown, grass_regrowth_time): """ Creates a new patch of grass Args: - fully_grown: (boolean) Whether the patch of grass is fully grown or not + grown: (boolean) Whether the patch of grass is fully grown or not countdown: Time for the patch of grass to be fully grown again """ super().__init__(unique_id, model) - self.fully_grown = fully_grown - self.countdown = countdown + self._fully_grown = fully_grown + self.grass_regrowth_time = grass_regrowth_time - def step(self): if not self.fully_grown: - if self.countdown <= 0: - # Set as fully grown - self.fully_grown = True - self.countdown = self.model.grass_regrowth_time - else: - self.countdown -= 1 + self.model.simulator.schedule_event_relative(self.set_fully_grown, countdown) + + def set_fully_grown(self): + self.fully_grown = True class WolfSheep(Model): @@ -124,22 +131,24 @@ class WolfSheep(Model): """ def __init__( - self, - height, - width, - initial_sheep, - initial_wolves, - sheep_reproduce, - wolf_reproduce, - grass_regrowth_time, - wolf_gain_from_food=13, - sheep_gain_from_food=5, - seed=None, + self, + simulator, + height, + width, + initial_sheep, + initial_wolves, + sheep_reproduce, + wolf_reproduce, + grass_regrowth_time, + wolf_gain_from_food=13, + sheep_gain_from_food=5, + seed=None, ): """ Create a new Wolf-Sheep model with the given parameters. Args: + simulator: ABMSimulator instance initial_sheep: Number of sheep to start with initial_wolves: Number of wolves to start with sheep_reproduce: Probability of each sheep reproducing each step @@ -149,18 +158,27 @@ def __init__( grass_regrowth_time: How long it takes for a grass patch to regrow once it is eaten sheep_gain_from_food: Energy sheep gain from grass, if enabled. - moore: - seed + seed : the random seed """ super().__init__(seed=seed) # Set parameters self.height = height self.width = width + self.simulator = simulator + self.initial_sheep = initial_sheep self.initial_wolves = initial_wolves self.grass_regrowth_time = grass_regrowth_time - self.schedule = RandomActivationByType(self) + self.sheep_reproduce = sheep_reproduce + self.wolf_reproduce = wolf_reproduce + self.grass_regrowth_time = grass_regrowth_time + self.wolf_gain_from_food = wolf_gain_from_food + self.sheep_gain_from_food = sheep_gain_from_food + + self.grid = None + + def setup(self): self.grid = OrthogonalVonNeumannGrid( [self.height, self.width], torus=False, @@ -174,12 +192,11 @@ def __init__( self.random.randrange(self.width), self.random.randrange(self.height), ) - energy = self.random.randrange(2 * sheep_gain_from_food) + energy = self.random.randrange(2 * self.sheep_gain_from_food) sheep = Sheep( - self.next_id(), self, energy, sheep_reproduce, sheep_gain_from_food + self.next_id(), self, energy, self.sheep_reproduce, self.sheep_gain_from_food ) sheep.move_to(self.grid[pos]) - self.schedule.add(sheep) # Create wolves for _ in range(self.initial_wolves): @@ -187,12 +204,11 @@ def __init__( self.random.randrange(self.width), self.random.randrange(self.height), ) - energy = self.random.randrange(2 * wolf_gain_from_food) + energy = self.random.randrange(2 * self.wolf_gain_from_food) wolf = Wolf( - self.next_id(), self, energy, wolf_reproduce, wolf_gain_from_food + self.next_id(), self, energy, self.wolf_reproduce, self.wolf_gain_from_food ) wolf.move_to(self.grid[pos]) - self.schedule.add(wolf) # Create grass patches possibly_fully_grown = [True, False] @@ -202,20 +218,22 @@ def __init__( countdown = self.grass_regrowth_time else: countdown = self.random.randrange(self.grass_regrowth_time) - patch = GrassPatch(self.next_id(), self, fully_grown, countdown) + patch = GrassPatch(self.next_id(), self, fully_grown, countdown, self.grass_regrowth_time) patch.move_to(cell) - self.schedule.add(patch) def step(self): - self.schedule.step() + self.get_agents_of_type(Sheep).shuffle(inplace=True).do("step") + self.get_agents_of_type(Wolf).shuffle(inplace=True).do("step") if __name__ == "__main__": import time - model = WolfSheep(25, 25, 60, 40, 0.2, 0.1, 20, seed=15) + simulator = ABMSimulator() + model = WolfSheep(simulator,25, 25, 60, 40, 0.2, 0.1, 20, seed=15,) + + simulator.setup(model) start_time = time.perf_counter() - for _ in range(100): - model.step() + simulator.run(100) print("Time:", time.perf_counter() - start_time) diff --git a/mesa/experimental/devs/__init__.py b/mesa/experimental/devs/__init__.py index 48a6d500552..c25db0e7efa 100644 --- a/mesa/experimental/devs/__init__.py +++ b/mesa/experimental/devs/__init__.py @@ -1,8 +1,7 @@ -from simulator import ABMSimulator, DEVSimulator -from eventlist import SimulationEvent, Priority - +from .eventlist import Priority, SimulationEvent +from .simulator import ABMSimulator, DEVSimulator __all__ = ["ABMSimulator", "DEVSimulator", "SimulationEvent", - "Priority"] \ No newline at end of file + "Priority"] diff --git a/mesa/experimental/devs/eventlist.py b/mesa/experimental/devs/eventlist.py index a0a12481889..9e78f3d0dc2 100644 --- a/mesa/experimental/devs/eventlist.py +++ b/mesa/experimental/devs/eventlist.py @@ -1,9 +1,9 @@ import itertools from enum import IntEnum from heapq import heapify, heappop, heappush -from weakref import ref, WeakMethod - from types import MethodType +from typing import Any, Callable +from weakref import WeakMethod, ref class InstanceCounterMeta(type): @@ -26,10 +26,13 @@ class Priority(IntEnum): class SimulationEvent(metaclass=InstanceCounterMeta): """A simulation event + the callable is wrapped using weakrefs, so there is no need to explicitly cancel event if e.g., an agent + is removed from the simulation. + Attributes: time (float): The simulation time of the event + function (Callable): The function to execute for this event priority (Priority): The priority of the event - fn (Callable): The function to execute for this event unique_id (int) the unique identifier of the event function_args (list[Any]): Argument for the function function_kwargs (Dict[str, Any]): Keyword arguments for the function @@ -40,17 +43,12 @@ class SimulationEvent(metaclass=InstanceCounterMeta): def CANCELED(self) -> bool: return self._canceled - @CANCELED.setter - def CANCELED(self, bool: bool) -> None: - self._canceled = bool - self.fn = None - self.function_args = [] - self.function_kwargs = {} - - def __init__(self, time, function, priority: Priority = Priority.DEFAULT, function_args=None, function_kwargs=None): + def __init__(self, time: int|float, function: Callable, priority: Priority = Priority.DEFAULT, + function_args: list[Any] | None = None, function_kwargs: dict[str, Any] | None =None) -> None: super().__init__() self.time = time self.priority = priority.value + self._canceled = False if isinstance(function, MethodType): function = WeakMethod(function) @@ -66,10 +64,18 @@ def __init__(self, time, function, priority: Priority = Priority.DEFAULT, functi raise Exception() def execute(self): + """execute this event""" fn = self.fn() if fn is not None: fn(*self.function_args, **self.function_kwargs) + def cancel(self) -> None: + """cancel this event""" + self._canceled = True + self.fn = None + self.function_args = [] + self.function_kwargs = {} + def __cmp__(self, other): if self.time < other.time: return -1 @@ -90,7 +96,7 @@ def __cmp__(self, other): # exact same event return 0 - def to_tuple(self): + def _to_tuple(self): return self.time, self.priority, self.unique_id, self @@ -115,7 +121,7 @@ def add_event(self, event: SimulationEvent): """ - heappush(self._event_list, event.to_tuple()) + heappush(self._event_list, event._to_tuple()) def peek_ahead(self, n: int = 1) -> list[SimulationEvent]: """Look at the first n non-canceled event in the event list @@ -148,23 +154,21 @@ def peek_ahead(self, n: int = 1) -> list[SimulationEvent]: return peek def pop(self) -> SimulationEvent: - """pop the first element from the event list - - """ + """pop the first element from the event list""" try: while True: sim_event = heappop(self._event_list)[3] if not sim_event.CANCELED: return sim_event - except IndexError: - raise Exception("event list is empty") + except IndexError as e: + raise Exception("event list is empty") from e def is_empty(self) -> bool: return len(self) == 0 def __contains__(self, event: SimulationEvent) -> bool: - return event.to_tuple() in self._event_list + return event._to_tuple() in self._event_list def __len__(self) -> int: return len(self._event_list) @@ -175,7 +179,7 @@ def remove(self, event: SimulationEvent) -> None: # heap structure invariant. So, we use a form of lazy deletion. # SimEvents have a CANCELED flag that we set to True, while poping and peak_ahead # silently ignore canceled events - event.CANCELED = True + event.cancel() def clear(self): self._event_list.clear() diff --git a/mesa/experimental/devs/examples/wolf_sheep.py b/mesa/experimental/devs/examples/wolf_sheep.py index 619be93013b..3441d76f0d0 100644 --- a/mesa/experimental/devs/examples/wolf_sheep.py +++ b/mesa/experimental/devs/examples/wolf_sheep.py @@ -96,14 +96,14 @@ class GrassPatch(mesa.Agent): """ @property - def fully_grown(self): + def fully_grown(self) -> bool: return self._fully_grown @fully_grown.setter - def fully_grown(self, value): + def fully_grown(self, value: bool): self._fully_grown = value - if value == False: + if not value: self.model.simulator.schedule_event_relative(self.set_fully_grown, self.grass_regrowth_time) @@ -241,4 +241,4 @@ def step(self): start_time = time.perf_counter() simulator.run(until=100) print(simulator.time) - print("Time:", time.perf_counter() - start_time) \ No newline at end of file + print("Time:", time.perf_counter() - start_time) diff --git a/mesa/experimental/devs/simulator.py b/mesa/experimental/devs/simulator.py index a95449ab531..705a2e372d4 100644 --- a/mesa/experimental/devs/simulator.py +++ b/mesa/experimental/devs/simulator.py @@ -1,8 +1,7 @@ -from typing import List, Any, Dict, Callable - -from .eventlist import EventList, SimulationEvent, Priority - import numbers +from typing import Any, Callable + +from .eventlist import EventList, Priority, SimulationEvent class Simulator: @@ -55,7 +54,7 @@ def reset(self): self.model = None def run(self, time_delta: int | float): - """run the simulator for time delta + """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 @@ -73,8 +72,8 @@ def step(self): event.execute() def schedule_event_now(self, function: Callable, priority: Priority = Priority.DEFAULT, - function_args: List[Any] | None = None, - function_kwargs: Dict[str, Any] | None = None) -> SimulationEvent: + function_args: list[Any] | None = None, + function_kwargs: dict[str, Any] | None = None) -> SimulationEvent: """Schedule event for the current time instant Args: @@ -97,8 +96,8 @@ def schedule_event_absolute(self, function: Callable, time: int | float, priority: Priority = Priority.DEFAULT, - function_args: List[Any] | None = None, - function_kwargs: Dict[str, Any] | None = None) -> SimulationEvent: + function_args: list[Any] | None = None, + function_kwargs: dict[str, Any] | None = None) -> SimulationEvent: """Schedule event for the specified time instant Args: @@ -112,6 +111,9 @@ def schedule_event_absolute(self, SimulationEvent: the simulation event that is scheduled """ + if self.time > time: + raise ValueError("trying to schedule an event in the past") + event = SimulationEvent(time, function, priority=priority, function_args=function_args, function_kwargs=function_kwargs) self._schedule_event(event) @@ -120,8 +122,8 @@ def schedule_event_absolute(self, def schedule_event_relative(self, function: Callable, time_delta: int | float, priority: Priority = Priority.DEFAULT, - function_args: List[Any] | None = None, - function_kwargs: Dict[str, Any] | None = None) -> SimulationEvent: + function_args: list[Any] | None = None, + function_kwargs: dict[str, Any] | None = None) -> SimulationEvent: """Schedule event for the current time plus the time delta Args: @@ -163,7 +165,7 @@ class ABMSimulator(Simulator): The basic time unit of this simulator is an integer. It schedules `model.step` for each tick with the highest priority. This implies that by default, `model.step` is the first event executed at a specific tick. - In addition, discrete event scheduling, using integers as the time unit is fully supported, paving the way + In addition, discrete event scheduling, using integer as the time unit is fully supported, paving the way for hybrid ABM-DEVS simulations. """ @@ -186,8 +188,8 @@ def check_time_unit(self, time) -> bool: def schedule_event_next_tick(self, function: Callable, priority: Priority = Priority.DEFAULT, - function_args: List[Any] | None = None, - function_kwargs: Dict[str, Any] | None = None) -> SimulationEvent: + function_args: list[Any] | None = None, + function_kwargs: dict[str, Any] | None = None) -> SimulationEvent: """Schedule a SimulationEvent for the next tick Args @@ -221,6 +223,11 @@ def step(self): class DEVSimulator(Simulator): + """A simulator where the unit of time is a float. Can be used for full-blown discrete event simulating using + event scheduling. + + """ + def __init__(self): super().__init__(float, 0.0) From 76ebfed5de7140af2ce6875300f9099d1334c8e7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 29 Feb 2024 19:08:02 +0000 Subject: [PATCH 11/44] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- benchmarks/WolfSheep/wolf_sheep.py | 60 ++++++++---- mesa/experimental/devs/__init__.py | 5 +- mesa/experimental/devs/eventlist.py | 12 ++- .../devs/examples/epstein_civil_violence.py | 69 +++++++------- mesa/experimental/devs/examples/wolf_sheep.py | 24 +++-- mesa/experimental/devs/simulator.py | 91 +++++++++++++------ 6 files changed, 163 insertions(+), 98 deletions(-) diff --git a/benchmarks/WolfSheep/wolf_sheep.py b/benchmarks/WolfSheep/wolf_sheep.py index 26361a54586..a5dc47fba8f 100644 --- a/benchmarks/WolfSheep/wolf_sheep.py +++ b/benchmarks/WolfSheep/wolf_sheep.py @@ -102,7 +102,9 @@ def fully_grown(self, value: bool) -> None: self._fully_grown = value if not value: - self.model.simulator.schedule_event_relative(self.set_fully_grown, self.grass_regrowth_time) + self.model.simulator.schedule_event_relative( + self.set_fully_grown, self.grass_regrowth_time + ) def __init__(self, unique_id, model, fully_grown, countdown, grass_regrowth_time): """ @@ -117,7 +119,9 @@ def __init__(self, unique_id, model, fully_grown, countdown, grass_regrowth_time self.grass_regrowth_time = grass_regrowth_time if not self.fully_grown: - self.model.simulator.schedule_event_relative(self.set_fully_grown, countdown) + self.model.simulator.schedule_event_relative( + self.set_fully_grown, countdown + ) def set_fully_grown(self): self.fully_grown = True @@ -131,18 +135,18 @@ class WolfSheep(Model): """ def __init__( - self, - simulator, - height, - width, - initial_sheep, - initial_wolves, - sheep_reproduce, - wolf_reproduce, - grass_regrowth_time, - wolf_gain_from_food=13, - sheep_gain_from_food=5, - seed=None, + self, + simulator, + height, + width, + initial_sheep, + initial_wolves, + sheep_reproduce, + wolf_reproduce, + grass_regrowth_time, + wolf_gain_from_food=13, + sheep_gain_from_food=5, + seed=None, ): """ Create a new Wolf-Sheep model with the given parameters. @@ -194,7 +198,11 @@ def setup(self): ) energy = self.random.randrange(2 * self.sheep_gain_from_food) sheep = Sheep( - self.next_id(), self, energy, self.sheep_reproduce, self.sheep_gain_from_food + self.next_id(), + self, + energy, + self.sheep_reproduce, + self.sheep_gain_from_food, ) sheep.move_to(self.grid[pos]) @@ -206,7 +214,11 @@ def setup(self): ) energy = self.random.randrange(2 * self.wolf_gain_from_food) wolf = Wolf( - self.next_id(), self, energy, self.wolf_reproduce, self.wolf_gain_from_food + self.next_id(), + self, + energy, + self.wolf_reproduce, + self.wolf_gain_from_food, ) wolf.move_to(self.grid[pos]) @@ -218,7 +230,9 @@ def setup(self): countdown = self.grass_regrowth_time else: countdown = self.random.randrange(self.grass_regrowth_time) - patch = GrassPatch(self.next_id(), self, fully_grown, countdown, self.grass_regrowth_time) + patch = GrassPatch( + self.next_id(), self, fully_grown, countdown, self.grass_regrowth_time + ) patch.move_to(cell) def step(self): @@ -230,7 +244,17 @@ def step(self): import time simulator = ABMSimulator() - model = WolfSheep(simulator,25, 25, 60, 40, 0.2, 0.1, 20, seed=15,) + model = WolfSheep( + simulator, + 25, + 25, + 60, + 40, + 0.2, + 0.1, + 20, + seed=15, + ) simulator.setup(model) diff --git a/mesa/experimental/devs/__init__.py b/mesa/experimental/devs/__init__.py index c25db0e7efa..b6dca39e29c 100644 --- a/mesa/experimental/devs/__init__.py +++ b/mesa/experimental/devs/__init__.py @@ -1,7 +1,4 @@ from .eventlist import Priority, SimulationEvent from .simulator import ABMSimulator, DEVSimulator -__all__ = ["ABMSimulator", - "DEVSimulator", - "SimulationEvent", - "Priority"] +__all__ = ["ABMSimulator", "DEVSimulator", "SimulationEvent", "Priority"] diff --git a/mesa/experimental/devs/eventlist.py b/mesa/experimental/devs/eventlist.py index 9e78f3d0dc2..19e39f69b1b 100644 --- a/mesa/experimental/devs/eventlist.py +++ b/mesa/experimental/devs/eventlist.py @@ -7,7 +7,7 @@ class InstanceCounterMeta(type): - """ Metaclass to make instance counter not share count with descendants + """Metaclass to make instance counter not share count with descendants TODO:: can also be used for agents.unique_id """ @@ -43,8 +43,14 @@ class SimulationEvent(metaclass=InstanceCounterMeta): def CANCELED(self) -> bool: return self._canceled - def __init__(self, time: int|float, function: Callable, priority: Priority = Priority.DEFAULT, - function_args: list[Any] | None = None, function_kwargs: dict[str, Any] | None =None) -> None: + def __init__( + self, + time: int | float, + function: Callable, + priority: Priority = Priority.DEFAULT, + function_args: list[Any] | None = None, + function_kwargs: dict[str, Any] | None = None, + ) -> None: super().__init__() self.time = time self.priority = priority.value diff --git a/mesa/experimental/devs/examples/epstein_civil_violence.py b/mesa/experimental/devs/examples/epstein_civil_violence.py index be603348c8c..108a3488f5c 100644 --- a/mesa/experimental/devs/examples/epstein_civil_violence.py +++ b/mesa/experimental/devs/examples/epstein_civil_violence.py @@ -45,7 +45,6 @@ class Citizen(EpsteinAgent): rebellion """ - def sent_to_jail(self, value): self.model.active_agents.remove(self) self.condition = AgentState.ARRESTED @@ -56,16 +55,16 @@ def release_from_jail(self): self.condition = AgentState.QUIESCENT def __init__( - self, - unique_id, - model, - vision, - movement, - hardship, - regime_legitimacy, - risk_aversion, - threshold, - arrest_prob_constant, + self, + unique_id, + model, + vision, + movement, + hardship, + regime_legitimacy, + risk_aversion, + threshold, + arrest_prob_constant, ): """ Create a new Citizen. @@ -127,10 +126,7 @@ def update_estimated_arrest_probability(self): cops_in_vision = len([c for c in self.neighbors if isinstance(c, Cop)]) actives_in_vision = 1.0 # citizen counts herself for c in self.neighbors: - if ( - isinstance(c, Citizen) - and c.condition == AgentState.ACTIVE - ): + if isinstance(c, Citizen) and c.condition == AgentState.ACTIVE: actives_in_vision += 1 self.arrest_probability = 1 - math.exp( -1 * self.arrest_prob_constant * (cops_in_vision / actives_in_vision) @@ -161,10 +157,7 @@ def step(self): self.update_neighbors() active_neighbors = [] for agent in self.neighbors: - if ( - isinstance(agent, Citizen) - and agent.condition == "Active" - ): + if isinstance(agent, Citizen) and agent.condition == "Active": active_neighbors.append(agent) if active_neighbors: arrestee = self.random.choice(active_neighbors) @@ -213,20 +206,20 @@ class EpsteinCivilViolence(Model): """ def __init__( - self, - width=40, - height=40, - citizen_density=0.7, - cop_density=0.074, - citizen_vision=7, - cop_vision=7, - legitimacy=0.8, - max_jail_term=1000, - active_threshold=0.1, - arrest_prob_constant=2.3, - movement=True, - max_iters=1000, - seed=None + self, + width=40, + height=40, + citizen_density=0.7, + cop_density=0.074, + citizen_vision=7, + cop_vision=7, + legitimacy=0.8, + max_jail_term=1000, + active_threshold=0.1, + arrest_prob_constant=2.3, + movement=True, + max_iters=1000, + seed=None, ): super().__init__(seed) if cop_density + citizen_density > 1: @@ -252,7 +245,13 @@ def setup(self): for _, pos in self.grid.coord_iter(): if self.random.random() < self.cop_density: - agent = Cop(self.next_id(), self, self.cop_vision, self.movement, self.max_jail_term) + agent = Cop( + self.next_id(), + self, + self.cop_vision, + self.movement, + self.max_jail_term, + ) elif self.random.random() < (self.cop_density + self.citizen_density): agent = Citizen( self.next_id(), @@ -263,7 +262,7 @@ def setup(self): regime_legitimacy=self.legitimacy, risk_aversion=self.random.random(), threshold=self.active_threshold, - arrest_prob_constant=self.arrest_prob_constant + arrest_prob_constant=self.arrest_prob_constant, ) else: continue diff --git a/mesa/experimental/devs/examples/wolf_sheep.py b/mesa/experimental/devs/examples/wolf_sheep.py index 3441d76f0d0..7b3aa6402b6 100644 --- a/mesa/experimental/devs/examples/wolf_sheep.py +++ b/mesa/experimental/devs/examples/wolf_sheep.py @@ -104,8 +104,9 @@ def fully_grown(self, value: bool): self._fully_grown = value if not value: - self.model.simulator.schedule_event_relative(self.set_fully_grown, self.grass_regrowth_time) - + self.model.simulator.schedule_event_relative( + self.set_fully_grown, self.grass_regrowth_time + ) def __init__(self, unique_id, model, fully_grown, countdown, grass_regrowth_time): """ @@ -120,13 +121,14 @@ def __init__(self, unique_id, model, fully_grown, countdown, grass_regrowth_time self.grass_regrowth_time = grass_regrowth_time if not self.fully_grown: - self.model.simulator.schedule_event_relative(self.set_fully_grown, countdown ) + self.model.simulator.schedule_event_relative( + self.set_fully_grown, countdown + ) def set_fully_grown(self): self.fully_grown = True - class WolfSheep(mesa.Model): """ Wolf-Sheep Predation Model @@ -206,7 +208,12 @@ def setup(self): ) energy = self.random.randrange(2 * self.wolf_gain_from_food) wolf = Wolf( - self.next_id(), self, self.moore, energy, self.wolf_reproduce, self.wolf_gain_from_food + self.next_id(), + self, + self.moore, + energy, + self.wolf_reproduce, + self.wolf_gain_from_food, ) self.grid.place_agent(wolf, pos) @@ -218,7 +225,9 @@ def setup(self): countdown = self.grass_regrowth_time else: countdown = self.random.randrange(self.grass_regrowth_time) - patch = GrassPatch(self.next_id(), self, fully_grown, countdown, self.grass_regrowth_time) + patch = GrassPatch( + self.next_id(), self, fully_grown, countdown, self.grass_regrowth_time + ) self.grid.place_agent(patch, pos) self.simulator.schedule_event_relative(self.step, 1) @@ -233,8 +242,7 @@ def step(self): simulator = ABMSimulator() - model = WolfSheep(25, 25, 60, 40, 0.2, 0.1, 20, - simulator=simulator, seed=15) + model = WolfSheep(25, 25, 60, 40, 0.2, 0.1, 20, simulator=simulator, seed=15) simulator.setup(model) diff --git a/mesa/experimental/devs/simulator.py b/mesa/experimental/devs/simulator.py index 705a2e372d4..2261a1d40c0 100644 --- a/mesa/experimental/devs/simulator.py +++ b/mesa/experimental/devs/simulator.py @@ -71,9 +71,13 @@ def step(self): self.time = event.time event.execute() - def schedule_event_now(self, function: Callable, priority: Priority = Priority.DEFAULT, - function_args: list[Any] | None = None, - function_kwargs: dict[str, Any] | None = None) -> SimulationEvent: + def schedule_event_now( + self, + function: Callable, + priority: Priority = Priority.DEFAULT, + function_args: list[Any] | None = None, + function_kwargs: dict[str, Any] | None = None, + ) -> SimulationEvent: """Schedule event for the current time instant Args: @@ -87,17 +91,24 @@ def schedule_event_now(self, function: Callable, priority: Priority = Priority.D """ - event = SimulationEvent(self.time, function, priority=priority, function_args=function_args, - function_kwargs=function_kwargs) + event = SimulationEvent( + self.time, + function, + priority=priority, + function_args=function_args, + function_kwargs=function_kwargs, + ) self._schedule_event(event) return event - def schedule_event_absolute(self, - function: Callable, - time: int | float, - priority: Priority = Priority.DEFAULT, - function_args: list[Any] | None = None, - function_kwargs: dict[str, Any] | None = None) -> SimulationEvent: + def schedule_event_absolute( + self, + function: Callable, + time: int | float, + priority: Priority = Priority.DEFAULT, + function_args: list[Any] | None = None, + function_kwargs: dict[str, Any] | None = None, + ) -> SimulationEvent: """Schedule event for the specified time instant Args: @@ -114,16 +125,24 @@ def schedule_event_absolute(self, if self.time > time: raise ValueError("trying to schedule an event in the past") - event = SimulationEvent(time, function, priority=priority, function_args=function_args, - function_kwargs=function_kwargs) + event = SimulationEvent( + time, + function, + priority=priority, + function_args=function_args, + function_kwargs=function_kwargs, + ) self._schedule_event(event) return event - def schedule_event_relative(self, function: Callable, - time_delta: int | float, - priority: Priority = Priority.DEFAULT, - function_args: list[Any] | None = None, - function_kwargs: dict[str, Any] | None = None) -> SimulationEvent: + def schedule_event_relative( + self, + function: Callable, + time_delta: int | float, + priority: Priority = Priority.DEFAULT, + function_args: list[Any] | None = None, + function_kwargs: dict[str, Any] | None = None, + ) -> SimulationEvent: """Schedule event for the current time plus the time delta Args: @@ -137,8 +156,13 @@ def schedule_event_relative(self, function: Callable, SimulationEvent: the simulation event that is scheduled """ - event = SimulationEvent(self.time + time_delta, function, priority=priority, function_args=function_args, - function_kwargs=function_kwargs) + event = SimulationEvent( + self.time + time_delta, + function, + priority=priority, + function_args=function_args, + function_kwargs=function_kwargs, + ) self._schedule_event(event) return event @@ -154,7 +178,9 @@ def cancel_event(self, event: SimulationEvent) -> None: def _schedule_event(self, event: SimulationEvent): if not self.check_time_unit(event.time): - raise ValueError(f"time unit mismatch {event.time} is not of time unit {self.time_unit}") + raise ValueError( + f"time unit mismatch {event.time} is not of time unit {self.time_unit}" + ) # check timeunit of events self.event_list.add_event(event) @@ -185,11 +211,13 @@ def check_time_unit(self, time) -> bool: else: return False - def schedule_event_next_tick(self, - function: Callable, - priority: Priority = Priority.DEFAULT, - function_args: list[Any] | None = None, - function_kwargs: dict[str, Any] | None = None) -> SimulationEvent: + def schedule_event_next_tick( + self, + function: Callable, + priority: Priority = Priority.DEFAULT, + function_args: list[Any] | None = None, + function_kwargs: dict[str, Any] | None = None, + ) -> SimulationEvent: """Schedule a SimulationEvent for the next tick Args @@ -199,10 +227,13 @@ def schedule_event_next_tick(self, function_kwargs (Dict[str, Any]): List of keyword arguments to pass to the callable """ - return self.schedule_event_relative(function, 1, - priority=priority, - function_args=function_args, - function_kwargs=function_kwargs) + return self.schedule_event_relative( + function, + 1, + priority=priority, + function_args=function_args, + function_kwargs=function_kwargs, + ) def step(self): """get the next event from the event list and execute it. From efd79df6112126d55f6ab7334409f5150f4386c3 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sun, 3 Mar 2024 20:27:15 +0100 Subject: [PATCH 12/44] make benchmarks work --- benchmarks/Flocking/flocking.py | 19 ++++++++++++------- benchmarks/Schelling/schelling.py | 11 +++++++---- benchmarks/WolfSheep/wolf_sheep.py | 6 +++++- benchmarks/global_benchmark.py | 10 ++++++++-- 4 files changed, 32 insertions(+), 14 deletions(-) diff --git a/benchmarks/Flocking/flocking.py b/benchmarks/Flocking/flocking.py index 352cacb84f4..66eb087cca0 100644 --- a/benchmarks/Flocking/flocking.py +++ b/benchmarks/Flocking/flocking.py @@ -124,15 +124,20 @@ def __init__( self.vision = vision self.speed = speed self.separation = separation + self.schedule = None + self.space = None + self.width = width + self.height = height + self.speed = speed + self.cohere = cohere + self.separate = separate + self.match = match + + def setup(self): self.schedule = mesa.time.RandomActivation(self) - self.space = mesa.space.ContinuousSpace(width, height, True) - self.factors = {"cohere": cohere, "separate": separate, "match": match} - self.make_agents() + self.space = mesa.space.ContinuousSpace(self.width, self.height, True) + self.factors = {"cohere": self.cohere, "separate": self.separate, "match": self.match} - def make_agents(self): - """ - Create self.population agents, with random positions and starting directions. - """ for i in range(self.population): x = self.random.random() * self.space.x_max y = self.random.random() * self.space.y_max diff --git a/benchmarks/Schelling/schelling.py b/benchmarks/Schelling/schelling.py index c7dd3bf1deb..249d022e8f1 100644 --- a/benchmarks/Schelling/schelling.py +++ b/benchmarks/Schelling/schelling.py @@ -66,17 +66,20 @@ def __init__( self.width = width self.density = density self.minority_pc = minority_pc + self.radius = radius + self.homophily = homophily + self.happy = 0 + + def setup(self): self.schedule = RandomActivation(self) self.grid = OrthogonalMooreGrid( - [height, width], + [self.height, self.width], torus=True, capacity=1, random=self.random, ) - self.happy = 0 - # Set up agents # We use a grid iterator that returns # the coordinates of a cell as well as @@ -85,7 +88,7 @@ def __init__( if self.random.random() < self.density: agent_type = 1 if self.random.random() < self.minority_pc else 0 agent = SchellingAgent( - self.next_id(), self, agent_type, radius, homophily + self.next_id(), self, agent_type, self.radius, self.homophily ) agent.move_to(cell) self.schedule.add(agent) diff --git a/benchmarks/WolfSheep/wolf_sheep.py b/benchmarks/WolfSheep/wolf_sheep.py index a5dc47fba8f..1b2cf60f312 100644 --- a/benchmarks/WolfSheep/wolf_sheep.py +++ b/benchmarks/WolfSheep/wolf_sheep.py @@ -108,11 +108,15 @@ def fully_grown(self, value: bool) -> None: def __init__(self, unique_id, model, fully_grown, countdown, grass_regrowth_time): """ + TODO:: fully grown can just be an int --> so one less param (i.e. countdown) + Creates a new patch of grass Args: - grown: (boolean) Whether the patch of grass is fully grown or not + fully_grown: (boolean) Whether the patch of grass is fully grown or not countdown: Time for the patch of grass to be fully grown again + grass_regrowth_time : time to fully regrow grass + countdown : Time for the patch of grass to be fully regrown if fully grown is False """ super().__init__(unique_id, model) self._fully_grown = fully_grown diff --git a/benchmarks/global_benchmark.py b/benchmarks/global_benchmark.py index 677d352b5c7..e55ba05b315 100644 --- a/benchmarks/global_benchmark.py +++ b/benchmarks/global_benchmark.py @@ -7,6 +7,8 @@ from configurations import configurations +from mesa.experimental.devs.simulator import ABMSimulator + # making sure we use this version of mesa and not one # also installed in site_packages or so. sys.path.insert(0, os.path.abspath("..")) @@ -19,9 +21,13 @@ def run_model(model_class, seed, parameters): # time.sleep(0.001) end_init_start_run = timeit.default_timer() + simulator = ABMSimulator() + simulator.setup(model) + + simulator.run(until=config["steps"]) - for _ in range(config["steps"]): - model.step() + # for _ in range(config["steps"]): + # model.step() # time.sleep(0.0001) end_run = timeit.default_timer() From d555858595e44aeb27c784451ba8a8d19dbb6687 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 3 Mar 2024 19:28:40 +0000 Subject: [PATCH 13/44] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- benchmarks/Flocking/flocking.py | 6 +++++- benchmarks/Schelling/schelling.py | 1 - 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/benchmarks/Flocking/flocking.py b/benchmarks/Flocking/flocking.py index 66eb087cca0..e75755b4e27 100644 --- a/benchmarks/Flocking/flocking.py +++ b/benchmarks/Flocking/flocking.py @@ -136,7 +136,11 @@ def __init__( def setup(self): self.schedule = mesa.time.RandomActivation(self) self.space = mesa.space.ContinuousSpace(self.width, self.height, True) - self.factors = {"cohere": self.cohere, "separate": self.separate, "match": self.match} + self.factors = { + "cohere": self.cohere, + "separate": self.separate, + "match": self.match, + } for i in range(self.population): x = self.random.random() * self.space.x_max diff --git a/benchmarks/Schelling/schelling.py b/benchmarks/Schelling/schelling.py index 249d022e8f1..eab82462173 100644 --- a/benchmarks/Schelling/schelling.py +++ b/benchmarks/Schelling/schelling.py @@ -70,7 +70,6 @@ def __init__( self.homophily = homophily self.happy = 0 - def setup(self): self.schedule = RandomActivation(self) self.grid = OrthogonalMooreGrid( From 6e43ab573cbda084ceb6db4550dd63c3d20f2fc9 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sun, 3 Mar 2024 20:31:59 +0100 Subject: [PATCH 14/44] fix for benchmarks --- benchmarks/Flocking/flocking.py | 2 ++ benchmarks/Schelling/schelling.py | 2 ++ benchmarks/global_benchmark.py | 7 ++++--- mesa/experimental/devs/examples/wolf_sheep.py | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/benchmarks/Flocking/flocking.py b/benchmarks/Flocking/flocking.py index e75755b4e27..a07ec7074e2 100644 --- a/benchmarks/Flocking/flocking.py +++ b/benchmarks/Flocking/flocking.py @@ -105,6 +105,7 @@ def __init__( cohere=0.03, separate=0.015, match=0.05, + simulator=None ): """ Create a new Flockers model. @@ -132,6 +133,7 @@ def __init__( self.cohere = cohere self.separate = separate self.match = match + self.simulator = simulator def setup(self): self.schedule = mesa.time.RandomActivation(self) diff --git a/benchmarks/Schelling/schelling.py b/benchmarks/Schelling/schelling.py index eab82462173..e4df596a17a 100644 --- a/benchmarks/Schelling/schelling.py +++ b/benchmarks/Schelling/schelling.py @@ -49,6 +49,7 @@ def __init__( density=0.8, minority_pc=0.5, seed=None, + simulator=None, ): """ Create a new Schelling model. @@ -69,6 +70,7 @@ def __init__( self.radius = radius self.homophily = homophily self.happy = 0 + self.simulator=simulator def setup(self): self.schedule = RandomActivation(self) diff --git a/benchmarks/global_benchmark.py b/benchmarks/global_benchmark.py index e55ba05b315..4e5fc853128 100644 --- a/benchmarks/global_benchmark.py +++ b/benchmarks/global_benchmark.py @@ -17,14 +17,15 @@ # Generic function to initialize and run a model def run_model(model_class, seed, parameters): start_init = timeit.default_timer() - model = model_class(seed=seed, **parameters) + simulator = ABMSimulator() + model = model_class(simulator=simulator, seed=seed, **parameters) # time.sleep(0.001) end_init_start_run = timeit.default_timer() - simulator = ABMSimulator() + simulator.setup(model) - simulator.run(until=config["steps"]) + simulator.run(config["steps"]) # for _ in range(config["steps"]): # model.step() diff --git a/mesa/experimental/devs/examples/wolf_sheep.py b/mesa/experimental/devs/examples/wolf_sheep.py index 7b3aa6402b6..58c2f8f7e82 100644 --- a/mesa/experimental/devs/examples/wolf_sheep.py +++ b/mesa/experimental/devs/examples/wolf_sheep.py @@ -247,6 +247,6 @@ def step(self): simulator.setup(model) start_time = time.perf_counter() - simulator.run(until=100) + simulator.run(100) print(simulator.time) print("Time:", time.perf_counter() - start_time) From ec30623093e4d32dc694b3d822fae75831739778 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 3 Mar 2024 19:32:20 +0000 Subject: [PATCH 15/44] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- benchmarks/Flocking/flocking.py | 2 +- benchmarks/Schelling/schelling.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/benchmarks/Flocking/flocking.py b/benchmarks/Flocking/flocking.py index a07ec7074e2..d629529df1f 100644 --- a/benchmarks/Flocking/flocking.py +++ b/benchmarks/Flocking/flocking.py @@ -105,7 +105,7 @@ def __init__( cohere=0.03, separate=0.015, match=0.05, - simulator=None + simulator=None, ): """ Create a new Flockers model. diff --git a/benchmarks/Schelling/schelling.py b/benchmarks/Schelling/schelling.py index e4df596a17a..8fefe783df3 100644 --- a/benchmarks/Schelling/schelling.py +++ b/benchmarks/Schelling/schelling.py @@ -70,7 +70,7 @@ def __init__( self.radius = radius self.homophily = homophily self.happy = 0 - self.simulator=simulator + self.simulator = simulator def setup(self): self.schedule = RandomActivation(self) From b267bb08a2dd4806d02d7781212dc0106977e9d8 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sun, 3 Mar 2024 20:42:34 +0100 Subject: [PATCH 16/44] me being stupid setup initializes the model so should be part of init timing --- benchmarks/global_benchmark.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/benchmarks/global_benchmark.py b/benchmarks/global_benchmark.py index 4e5fc853128..884e56ee168 100644 --- a/benchmarks/global_benchmark.py +++ b/benchmarks/global_benchmark.py @@ -19,12 +19,10 @@ def run_model(model_class, seed, parameters): start_init = timeit.default_timer() simulator = ABMSimulator() model = model_class(simulator=simulator, seed=seed, **parameters) - # time.sleep(0.001) + simulator.setup(model) end_init_start_run = timeit.default_timer() - simulator.setup(model) - simulator.run(config["steps"]) # for _ in range(config["steps"]): From 53e7ae59a9fecb6b1e0d19906a69092a67f9ddb6 Mon Sep 17 00:00:00 2001 From: Corvince Date: Mon, 4 Mar 2024 21:28:52 +0100 Subject: [PATCH 17/44] schedule events correctly --- mesa/experimental/devs/eventlist.py | 8 ++++---- mesa/experimental/devs/simulator.py | 7 +++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/mesa/experimental/devs/eventlist.py b/mesa/experimental/devs/eventlist.py index 19e39f69b1b..a97ed409903 100644 --- a/mesa/experimental/devs/eventlist.py +++ b/mesa/experimental/devs/eventlist.py @@ -56,10 +56,10 @@ def __init__( self.priority = priority.value self._canceled = False - if isinstance(function, MethodType): - function = WeakMethod(function) - else: - function = ref(function) + # if isinstance(function, MethodType): + # function = WeakMethod(function) + # else: + # function = ref(function) self.fn = function self.unique_id = next(self._ids) diff --git a/mesa/experimental/devs/simulator.py b/mesa/experimental/devs/simulator.py index 2261a1d40c0..931072188f1 100644 --- a/mesa/experimental/devs/simulator.py +++ b/mesa/experimental/devs/simulator.py @@ -29,8 +29,7 @@ def __init__(self, time_unit: type, start_time: int | float): self.time_unit = time_unit self.model = None - def check_time_unit(self, time: int | float) -> bool: - ... + def check_time_unit(self, time: int | float) -> bool: ... def setup(self, model: "Model") -> None: """Setup the model to simulate @@ -245,11 +244,11 @@ def step(self): """ event = self.event_list.pop() + self.time = event.time - if event.fn() == self.model.step: + if event.fn == self.model.step: self.schedule_event_next_tick(self.model.step, priority=Priority.HIGH) - self.time = event.time event.execute() From 155517b1d366acd6fc952a6b82ac80c08d5a8d03 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 4 Mar 2024 20:29:21 +0000 Subject: [PATCH 18/44] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mesa/experimental/devs/eventlist.py | 2 -- mesa/experimental/devs/simulator.py | 3 ++- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/mesa/experimental/devs/eventlist.py b/mesa/experimental/devs/eventlist.py index a97ed409903..87098f2125a 100644 --- a/mesa/experimental/devs/eventlist.py +++ b/mesa/experimental/devs/eventlist.py @@ -1,9 +1,7 @@ import itertools from enum import IntEnum from heapq import heapify, heappop, heappush -from types import MethodType from typing import Any, Callable -from weakref import WeakMethod, ref class InstanceCounterMeta(type): diff --git a/mesa/experimental/devs/simulator.py b/mesa/experimental/devs/simulator.py index 931072188f1..1eb9ac7cf23 100644 --- a/mesa/experimental/devs/simulator.py +++ b/mesa/experimental/devs/simulator.py @@ -29,7 +29,8 @@ def __init__(self, time_unit: type, start_time: int | float): self.time_unit = time_unit self.model = None - def check_time_unit(self, time: int | float) -> bool: ... + def check_time_unit(self, time: int | float) -> bool: + ... def setup(self, model: "Model") -> None: """Setup the model to simulate From 79a30a4270c31aade1e71fb45eb8c2b2fc28142a Mon Sep 17 00:00:00 2001 From: Corvince Date: Mon, 4 Mar 2024 22:02:33 +0100 Subject: [PATCH 19/44] simplified eventlist --- mesa/experimental/devs/eventlist.py | 87 ++++++++++------------------- mesa/experimental/devs/simulator.py | 7 +-- 2 files changed, 32 insertions(+), 62 deletions(-) diff --git a/mesa/experimental/devs/eventlist.py b/mesa/experimental/devs/eventlist.py index 87098f2125a..5d714175b96 100644 --- a/mesa/experimental/devs/eventlist.py +++ b/mesa/experimental/devs/eventlist.py @@ -4,24 +4,13 @@ from typing import Any, Callable -class InstanceCounterMeta(type): - """Metaclass to make instance counter not share count with descendants - - TODO:: can also be used for agents.unique_id - """ - - def __init__(cls, name, bases, attrs): - super().__init__(name, bases, attrs) - cls._ids = itertools.count(1) - - class Priority(IntEnum): LOW = 10 DEFAULT = 5 HIGH = 1 -class SimulationEvent(metaclass=InstanceCounterMeta): +class SimulationEvent: """A simulation event the callable is wrapped using weakrefs, so there is no need to explicitly cancel event if e.g., an agent @@ -37,6 +26,8 @@ class SimulationEvent(metaclass=InstanceCounterMeta): """ + _ids = itertools.count(1) + @property def CANCELED(self) -> bool: return self._canceled @@ -54,10 +45,10 @@ def __init__( self.priority = priority.value self._canceled = False - # if isinstance(function, MethodType): - # function = WeakMethod(function) - # else: - # function = ref(function) + if isinstance(function, MethodType): + function = WeakMethod(function) + else: + function = ref(function) self.fn = function self.unique_id = next(self._ids) @@ -80,28 +71,13 @@ def cancel(self) -> None: self.function_args = [] self.function_kwargs = {} - def __cmp__(self, other): - if self.time < other.time: - return -1 - if self.time > other.time: - return 1 - - if self.priority > other.priority: - return -1 - if self.priority < other.priority: - return 1 - - if self.unique_id < other.unique_id: - return -1 - if self.unique_id > other.unique_id: - return 1 - - # theoretically this should be impossible unless it is the - # exact same event - return 0 - - def _to_tuple(self): - return self.time, self.priority, self.unique_id, self + def __lt__(self, other): + # Define a total ordering for events to be used by the heapq + return (self.time, self.priority, self.unique_id) < ( + other.time, + other.priority, + other.unique_id, + ) class EventList: @@ -113,9 +89,8 @@ class EventList: """ def __init__(self): - super().__init__() - self._event_list: list[tuple] = [] - heapify(self._event_list) + self._events: list[SimulationEvent] = [] + heapify(self._events) def add_event(self, event: SimulationEvent): """Add the event to the event list @@ -125,7 +100,7 @@ def add_event(self, event: SimulationEvent): """ - heappush(self._event_list, event._to_tuple()) + heappush(self._events, event) def peek_ahead(self, n: int = 1) -> list[SimulationEvent]: """Look at the first n non-canceled event in the event list @@ -149,33 +124,29 @@ def peek_ahead(self, n: int = 1) -> list[SimulationEvent]: raise IndexError("event list is empty") peek: list[SimulationEvent] = [] - for entry in self._event_list: - sim_event: SimulationEvent = entry[3] - if not sim_event.CANCELED: - peek.append(sim_event) + for event in self._events: + if not event.CANCELED: + peek.append(event) if len(peek) >= n: return peek return peek - def pop(self) -> SimulationEvent: + def pop_event(self) -> SimulationEvent: """pop the first element from the event list""" - - try: - while True: - sim_event = heappop(self._event_list)[3] - if not sim_event.CANCELED: - return sim_event - except IndexError as e: - raise Exception("event list is empty") from e + while self._events: + event = heappop(self._events) + if not event.CANCELED: + return event + raise IndexError("Event list is empty") def is_empty(self) -> bool: return len(self) == 0 def __contains__(self, event: SimulationEvent) -> bool: - return event._to_tuple() in self._event_list + return event in self._events def __len__(self) -> int: - return len(self._event_list) + return len(self._events) def remove(self, event: SimulationEvent) -> None: """remove an event from the event list""" @@ -186,4 +157,4 @@ def remove(self, event: SimulationEvent) -> None: event.cancel() def clear(self): - self._event_list.clear() + self._events.clear() diff --git a/mesa/experimental/devs/simulator.py b/mesa/experimental/devs/simulator.py index 1eb9ac7cf23..8a852ffaf21 100644 --- a/mesa/experimental/devs/simulator.py +++ b/mesa/experimental/devs/simulator.py @@ -67,7 +67,7 @@ def run(self, time_delta: int | float): self.step() def step(self): - event = self.event_list.pop() + event = self.event_list.pop_event() self.time = event.time event.execute() @@ -244,10 +244,9 @@ def step(self): incremental time progression. """ - event = self.event_list.pop() + event = self.event_list.pop_event() self.time = event.time - - if event.fn == self.model.step: + if event.fn() == self.model.step: self.schedule_event_next_tick(self.model.step, priority=Priority.HIGH) event.execute() From 6c843c9879850a2f93aead04c903bf041baa188f Mon Sep 17 00:00:00 2001 From: Corvince Date: Mon, 4 Mar 2024 22:14:34 +0100 Subject: [PATCH 20/44] readded imports --- mesa/experimental/devs/eventlist.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mesa/experimental/devs/eventlist.py b/mesa/experimental/devs/eventlist.py index 5d714175b96..6d8fd793b5b 100644 --- a/mesa/experimental/devs/eventlist.py +++ b/mesa/experimental/devs/eventlist.py @@ -1,7 +1,9 @@ import itertools from enum import IntEnum from heapq import heapify, heappop, heappush +from types import MethodType from typing import Any, Callable +from weakref import WeakMethod, ref class Priority(IntEnum): From 5ba27d8f039a6f8456d2d7528cd76af466b11188 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Mon, 11 Mar 2024 20:10:12 +0100 Subject: [PATCH 21/44] remove need for model.setup --- benchmarks/Flocking/flocking.py | 22 +++-------- benchmarks/Schelling/schelling.py | 13 ++----- benchmarks/WolfSheep/wolf_sheep.py | 29 +++++---------- mesa/experimental/devs/examples/wolf_sheep.py | 37 +++++++++---------- mesa/experimental/devs/simulator.py | 8 +--- 5 files changed, 36 insertions(+), 73 deletions(-) diff --git a/benchmarks/Flocking/flocking.py b/benchmarks/Flocking/flocking.py index d629529df1f..7e2346cdd47 100644 --- a/benchmarks/Flocking/flocking.py +++ b/benchmarks/Flocking/flocking.py @@ -122,26 +122,16 @@ def __init__( """ super().__init__(seed=seed) self.population = population - self.vision = vision - self.speed = speed - self.separation = separation - self.schedule = None - self.space = None self.width = width self.height = height - self.speed = speed - self.cohere = cohere - self.separate = separate - self.match = match self.simulator = simulator - def setup(self): self.schedule = mesa.time.RandomActivation(self) self.space = mesa.space.ContinuousSpace(self.width, self.height, True) self.factors = { - "cohere": self.cohere, - "separate": self.separate, - "match": self.match, + "cohere": cohere, + "separate": separate, + "match": match, } for i in range(self.population): @@ -153,10 +143,10 @@ def setup(self): unique_id=i, model=self, pos=pos, - speed=self.speed, + speed=speed, direction=direction, - vision=self.vision, - separation=self.separation, + vision=vision, + separation=separation, **self.factors, ) self.space.place_agent(boid, pos) diff --git a/benchmarks/Schelling/schelling.py b/benchmarks/Schelling/schelling.py index 8fefe783df3..b7702a84bed 100644 --- a/benchmarks/Schelling/schelling.py +++ b/benchmarks/Schelling/schelling.py @@ -63,19 +63,12 @@ def __init__( seed: Seed for Reproducibility """ super().__init__(seed=seed) - self.height = height - self.width = width - self.density = density self.minority_pc = minority_pc - self.radius = radius - self.homophily = homophily - self.happy = 0 self.simulator = simulator - def setup(self): self.schedule = RandomActivation(self) self.grid = OrthogonalMooreGrid( - [self.height, self.width], + [height, width], torus=True, capacity=1, random=self.random, @@ -86,10 +79,10 @@ def setup(self): # the coordinates of a cell as well as # its contents. (coord_iter) for cell in self.grid: - if self.random.random() < self.density: + if self.random.random() < density: agent_type = 1 if self.random.random() < self.minority_pc else 0 agent = SchellingAgent( - self.next_id(), self, agent_type, self.radius, self.homophily + self.next_id(), self, agent_type, radius, homophily ) agent.move_to(cell) self.schedule.add(agent) diff --git a/benchmarks/WolfSheep/wolf_sheep.py b/benchmarks/WolfSheep/wolf_sheep.py index 1b2cf60f312..89ce0788000 100644 --- a/benchmarks/WolfSheep/wolf_sheep.py +++ b/benchmarks/WolfSheep/wolf_sheep.py @@ -162,7 +162,6 @@ def __init__( sheep_reproduce: Probability of each sheep reproducing each step wolf_reproduce: Probability of each wolf reproducing each step wolf_gain_from_food: Energy a wolf gains from eating a sheep - grass: Whether to have the sheep eat grass for energy grass_regrowth_time: How long it takes for a grass patch to regrow once it is eaten sheep_gain_from_food: Energy sheep gain from grass, if enabled. @@ -176,17 +175,7 @@ def __init__( self.initial_sheep = initial_sheep self.initial_wolves = initial_wolves - self.grass_regrowth_time = grass_regrowth_time - - self.sheep_reproduce = sheep_reproduce - self.wolf_reproduce = wolf_reproduce - self.grass_regrowth_time = grass_regrowth_time - self.wolf_gain_from_food = wolf_gain_from_food - self.sheep_gain_from_food = sheep_gain_from_food - - self.grid = None - def setup(self): self.grid = OrthogonalVonNeumannGrid( [self.height, self.width], torus=False, @@ -200,13 +189,13 @@ def setup(self): self.random.randrange(self.width), self.random.randrange(self.height), ) - energy = self.random.randrange(2 * self.sheep_gain_from_food) + energy = self.random.randrange(2 * sheep_gain_from_food) sheep = Sheep( self.next_id(), self, energy, - self.sheep_reproduce, - self.sheep_gain_from_food, + sheep_reproduce, + sheep_gain_from_food, ) sheep.move_to(self.grid[pos]) @@ -216,13 +205,13 @@ def setup(self): self.random.randrange(self.width), self.random.randrange(self.height), ) - energy = self.random.randrange(2 * self.wolf_gain_from_food) + energy = self.random.randrange(2 * wolf_gain_from_food) wolf = Wolf( self.next_id(), self, energy, - self.wolf_reproduce, - self.wolf_gain_from_food, + wolf_reproduce, + wolf_gain_from_food, ) wolf.move_to(self.grid[pos]) @@ -231,11 +220,11 @@ def setup(self): for cell in self.grid: fully_grown = self.random.choice(possibly_fully_grown) if fully_grown: - countdown = self.grass_regrowth_time + countdown = grass_regrowth_time else: - countdown = self.random.randrange(self.grass_regrowth_time) + countdown = self.random.randrange(grass_regrowth_time) patch = GrassPatch( - self.next_id(), self, fully_grown, countdown, self.grass_regrowth_time + self.next_id(), self, fully_grown, countdown, grass_regrowth_time ) patch.move_to(cell) diff --git a/mesa/experimental/devs/examples/wolf_sheep.py b/mesa/experimental/devs/examples/wolf_sheep.py index 58c2f8f7e82..4f04fdd0bb1 100644 --- a/mesa/experimental/devs/examples/wolf_sheep.py +++ b/mesa/experimental/devs/examples/wolf_sheep.py @@ -174,14 +174,13 @@ def __init__( self.initial_wolves = initial_wolves self.simulator = simulator - self.sheep_reproduce = sheep_reproduce - self.wolf_reproduce = wolf_reproduce - self.grass_regrowth_time = grass_regrowth_time - self.wolf_gain_from_food = wolf_gain_from_food - self.sheep_gain_from_food = sheep_gain_from_food - self.moore = moore + # self.sheep_reproduce = sheep_reproduce + # self.wolf_reproduce = wolf_reproduce + # self.grass_regrowth_time = grass_regrowth_time + # self.wolf_gain_from_food = wolf_gain_from_food + # self.sheep_gain_from_food = sheep_gain_from_food + # self.moore = moore - def setup(self): self.grid = mesa.space.MultiGrid(self.height, self.width, torus=False) for _ in range(self.initial_sheep): @@ -189,14 +188,14 @@ def setup(self): self.random.randrange(self.width), self.random.randrange(self.height), ) - energy = self.random.randrange(2 * self.sheep_gain_from_food) + energy = self.random.randrange(2 * sheep_gain_from_food) sheep = Sheep( self.next_id(), self, - self.moore, + moore, energy, - self.sheep_reproduce, - self.sheep_gain_from_food, + sheep_reproduce, + sheep_gain_from_food, ) self.grid.place_agent(sheep, pos) @@ -206,14 +205,14 @@ def setup(self): self.random.randrange(self.width), self.random.randrange(self.height), ) - energy = self.random.randrange(2 * self.wolf_gain_from_food) + energy = self.random.randrange(2 * wolf_gain_from_food) wolf = Wolf( self.next_id(), self, - self.moore, + moore, energy, - self.wolf_reproduce, - self.wolf_gain_from_food, + wolf_reproduce, + wolf_gain_from_food, ) self.grid.place_agent(wolf, pos) @@ -222,16 +221,14 @@ def setup(self): for _agent, pos in self.grid.coord_iter(): fully_grown = self.random.choice(possibly_fully_grown) if fully_grown: - countdown = self.grass_regrowth_time + countdown = grass_regrowth_time else: - countdown = self.random.randrange(self.grass_regrowth_time) + countdown = self.random.randrange(grass_regrowth_time) patch = GrassPatch( - self.next_id(), self, fully_grown, countdown, self.grass_regrowth_time + self.next_id(), self, fully_grown, countdown, grass_regrowth_time ) self.grid.place_agent(patch, pos) - self.simulator.schedule_event_relative(self.step, 1) - def step(self): self.get_agents_of_type(Sheep).shuffle(inplace=True).do("step") self.get_agents_of_type(Wolf).shuffle(inplace=True).do("step") diff --git a/mesa/experimental/devs/simulator.py b/mesa/experimental/devs/simulator.py index 8a852ffaf21..34e2f86bf3f 100644 --- a/mesa/experimental/devs/simulator.py +++ b/mesa/experimental/devs/simulator.py @@ -33,20 +33,14 @@ def check_time_unit(self, time: int | float) -> bool: ... def setup(self, model: "Model") -> None: - """Setup the model to simulate + """Setup the simulator with the model to simulate Args: model (Model): The model to simulate - Notes: - The basic assumption of the simulator is that a Model has a model.setup method that sets up the - model. - """ - self.event_list.clear() self.model = model - model.setup() def reset(self): """Reset the simulator by clearing the event list and removing the model to simulate""" From f298879d3a7ff3bd01a85bc18da97a7c2b38076b Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Mon, 11 Mar 2024 20:13:46 +0100 Subject: [PATCH 22/44] adds a bit more docstring info on eventlist implementation motivation --- mesa/experimental/devs/eventlist.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mesa/experimental/devs/eventlist.py b/mesa/experimental/devs/eventlist.py index 6d8fd793b5b..079c290f7bf 100644 --- a/mesa/experimental/devs/eventlist.py +++ b/mesa/experimental/devs/eventlist.py @@ -85,8 +85,9 @@ def __lt__(self, other): class EventList: """An event list - This is a heap queue sorted list of events. Events are allways removed from the left. The events are sorted - based on their time stamp, their priority, and their unique_id, guaranteeing a complete ordering. + This is a heap queue sorted list of events. Events are allways removed from the left, so heapq is a performant and + appropriate data structure. Events are sorted based on their time stamp, their priority, and their unique_id + as a tie-breaker, guaranteeing a complete ordering. """ From bae814ed6cfbf616bf8cc2531734741e23788201 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Mon, 11 Mar 2024 20:39:39 +0100 Subject: [PATCH 23/44] cleanup of fully_grown --- benchmarks/WolfSheep/wolf_sheep.py | 7 ++----- mesa/experimental/devs/examples/wolf_sheep.py | 4 ++-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/benchmarks/WolfSheep/wolf_sheep.py b/benchmarks/WolfSheep/wolf_sheep.py index 89ce0788000..bf52f2f12ba 100644 --- a/benchmarks/WolfSheep/wolf_sheep.py +++ b/benchmarks/WolfSheep/wolf_sheep.py @@ -103,7 +103,7 @@ def fully_grown(self, value: bool) -> None: if not value: self.model.simulator.schedule_event_relative( - self.set_fully_grown, self.grass_regrowth_time + setattr, self.grass_regrowth_time, function_args=[self, "fully_grown", True] ) def __init__(self, unique_id, model, fully_grown, countdown, grass_regrowth_time): @@ -124,12 +124,9 @@ def __init__(self, unique_id, model, fully_grown, countdown, grass_regrowth_time if not self.fully_grown: self.model.simulator.schedule_event_relative( - self.set_fully_grown, countdown + setattr, countdown, function_args=[self, "fully_grown", True] ) - def set_fully_grown(self): - self.fully_grown = True - class WolfSheep(Model): """ diff --git a/mesa/experimental/devs/examples/wolf_sheep.py b/mesa/experimental/devs/examples/wolf_sheep.py index 4f04fdd0bb1..79b695409cf 100644 --- a/mesa/experimental/devs/examples/wolf_sheep.py +++ b/mesa/experimental/devs/examples/wolf_sheep.py @@ -105,7 +105,7 @@ def fully_grown(self, value: bool): if not value: self.model.simulator.schedule_event_relative( - self.set_fully_grown, self.grass_regrowth_time + setattr, self.grass_regrowth_time, function_args=[self, "fully_grown", True] ) def __init__(self, unique_id, model, fully_grown, countdown, grass_regrowth_time): @@ -122,7 +122,7 @@ def __init__(self, unique_id, model, fully_grown, countdown, grass_regrowth_time if not self.fully_grown: self.model.simulator.schedule_event_relative( - self.set_fully_grown, countdown + setattr, countdown, function_args=[self, "fully_grown", True] ) def set_fully_grown(self): From 911c919484cb7d7401f01b30e40941b5f906b9c9 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Mon, 11 Mar 2024 20:39:51 +0100 Subject: [PATCH 24/44] removal of setup from epstein example --- .../devs/examples/epstein_civil_violence.py | 77 ++++++++----------- 1 file changed, 34 insertions(+), 43 deletions(-) diff --git a/mesa/experimental/devs/examples/epstein_civil_violence.py b/mesa/experimental/devs/examples/epstein_civil_violence.py index 108a3488f5c..3baa5a9510c 100644 --- a/mesa/experimental/devs/examples/epstein_civil_violence.py +++ b/mesa/experimental/devs/examples/epstein_civil_violence.py @@ -55,16 +55,16 @@ def release_from_jail(self): self.condition = AgentState.QUIESCENT def __init__( - self, - unique_id, - model, - vision, - movement, - hardship, - regime_legitimacy, - risk_aversion, - threshold, - arrest_prob_constant, + self, + unique_id, + model, + vision, + movement, + hardship, + regime_legitimacy, + risk_aversion, + threshold, + arrest_prob_constant, ): """ Create a new Citizen. @@ -206,20 +206,20 @@ class EpsteinCivilViolence(Model): """ def __init__( - self, - width=40, - height=40, - citizen_density=0.7, - cop_density=0.074, - citizen_vision=7, - cop_vision=7, - legitimacy=0.8, - max_jail_term=1000, - active_threshold=0.1, - arrest_prob_constant=2.3, - movement=True, - max_iters=1000, - seed=None, + self, + width=40, + height=40, + citizen_density=0.7, + cop_density=0.074, + citizen_vision=7, + cop_vision=7, + legitimacy=0.8, + max_jail_term=1000, + active_threshold=0.1, + arrest_prob_constant=2.3, + movement=True, + max_iters=1000, + seed=None, ): super().__init__(seed) if cop_density + citizen_density > 1: @@ -229,18 +229,9 @@ def __init__( self.height = height self.citizen_density = citizen_density self.cop_density = cop_density - self.legitimacy = legitimacy - self.max_jail_term = max_jail_term - self.active_threshold = active_threshold - self.arrest_prob_constant = arrest_prob_constant - self.movement = movement + self.max_iters = max_iters - self.cop_vision = cop_vision - self.citizen_vision = citizen_vision - self.active_agents: AgentSet | None = None - self.grid = None - def setup(self): self.grid = SingleGrid(self.width, self.height, torus=True) for _, pos in self.grid.coord_iter(): @@ -248,21 +239,21 @@ def setup(self): agent = Cop( self.next_id(), self, - self.cop_vision, - self.movement, - self.max_jail_term, + cop_vision, + movement, + max_jail_term, ) elif self.random.random() < (self.cop_density + self.citizen_density): agent = Citizen( self.next_id(), self, - self.citizen_vision, - self.movement, + citizen_vision, + movement, hardship=self.random.random(), - regime_legitimacy=self.legitimacy, + regime_legitimacy=legitimacy, risk_aversion=self.random.random(), - threshold=self.active_threshold, - arrest_prob_constant=self.arrest_prob_constant, + threshold=active_threshold, + arrest_prob_constant=arrest_prob_constant, ) else: continue @@ -280,4 +271,4 @@ def step(self): simulator.setup(model) - simulator.run(until=100) + simulator.run(time_delta=100) From 4e54addf7a6f64116ebbe233baa08d3a90552913 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Mon, 11 Mar 2024 20:40:43 +0100 Subject: [PATCH 25/44] set time to starttime upon reset --- mesa/experimental/devs/simulator.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/mesa/experimental/devs/simulator.py b/mesa/experimental/devs/simulator.py index 34e2f86bf3f..3a478805f55 100644 --- a/mesa/experimental/devs/simulator.py +++ b/mesa/experimental/devs/simulator.py @@ -25,8 +25,10 @@ def __init__(self, time_unit: type, start_time: int | float): # should model run in a separate thread, # and we can then interact with start, stop, run_until, and step? self.event_list = EventList() - self.time = start_time + self.start_time = start_time self.time_unit = time_unit + + self.time = self.start_time self.model = None def check_time_unit(self, time: int | float) -> bool: @@ -46,6 +48,7 @@ def reset(self): """Reset the simulator by clearing the event list and removing the model to simulate""" self.event_list.clear() self.model = None + self.time = self.start_time def run(self, time_delta: int | float): """run the simulator for the specified time delta @@ -84,16 +87,8 @@ def schedule_event_now( SimulationEvent: the simulation event that is scheduled """ - - event = SimulationEvent( - self.time, - function, - priority=priority, - function_args=function_args, - function_kwargs=function_kwargs, - ) - self._schedule_event(event) - return event + return self.schedule_event_relative(function, 0, priority=priority, + function_args=function_args, function_kwargs=function_kwargs) def schedule_event_absolute( self, From 1c86acaa165adc4d025d55392d13c9caab678991 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Mon, 11 Mar 2024 20:40:54 +0100 Subject: [PATCH 26/44] move to zero based indexing for _ids --- mesa/experimental/devs/eventlist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mesa/experimental/devs/eventlist.py b/mesa/experimental/devs/eventlist.py index 079c290f7bf..df870abbdc5 100644 --- a/mesa/experimental/devs/eventlist.py +++ b/mesa/experimental/devs/eventlist.py @@ -28,7 +28,7 @@ class SimulationEvent: """ - _ids = itertools.count(1) + _ids = itertools.count() @property def CANCELED(self) -> bool: From 2341b79c8e5cf236681a5c3a89158ca6bf7e22e9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 19:41:06 +0000 Subject: [PATCH 27/44] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- benchmarks/WolfSheep/wolf_sheep.py | 4 +- .../devs/examples/epstein_civil_violence.py | 49 +++++++++---------- mesa/experimental/devs/examples/wolf_sheep.py | 4 +- mesa/experimental/devs/simulator.py | 9 +++- 4 files changed, 37 insertions(+), 29 deletions(-) diff --git a/benchmarks/WolfSheep/wolf_sheep.py b/benchmarks/WolfSheep/wolf_sheep.py index bf52f2f12ba..f80db8eb057 100644 --- a/benchmarks/WolfSheep/wolf_sheep.py +++ b/benchmarks/WolfSheep/wolf_sheep.py @@ -103,7 +103,9 @@ def fully_grown(self, value: bool) -> None: if not value: self.model.simulator.schedule_event_relative( - setattr, self.grass_regrowth_time, function_args=[self, "fully_grown", True] + setattr, + self.grass_regrowth_time, + function_args=[self, "fully_grown", True], ) def __init__(self, unique_id, model, fully_grown, countdown, grass_regrowth_time): diff --git a/mesa/experimental/devs/examples/epstein_civil_violence.py b/mesa/experimental/devs/examples/epstein_civil_violence.py index 3baa5a9510c..f53e3e4c687 100644 --- a/mesa/experimental/devs/examples/epstein_civil_violence.py +++ b/mesa/experimental/devs/examples/epstein_civil_violence.py @@ -2,7 +2,6 @@ import math from mesa import Agent, Model -from mesa.agent import AgentSet from mesa.experimental.devs.simulator import ABMSimulator from mesa.space import SingleGrid @@ -55,16 +54,16 @@ def release_from_jail(self): self.condition = AgentState.QUIESCENT def __init__( - self, - unique_id, - model, - vision, - movement, - hardship, - regime_legitimacy, - risk_aversion, - threshold, - arrest_prob_constant, + self, + unique_id, + model, + vision, + movement, + hardship, + regime_legitimacy, + risk_aversion, + threshold, + arrest_prob_constant, ): """ Create a new Citizen. @@ -206,20 +205,20 @@ class EpsteinCivilViolence(Model): """ def __init__( - self, - width=40, - height=40, - citizen_density=0.7, - cop_density=0.074, - citizen_vision=7, - cop_vision=7, - legitimacy=0.8, - max_jail_term=1000, - active_threshold=0.1, - arrest_prob_constant=2.3, - movement=True, - max_iters=1000, - seed=None, + self, + width=40, + height=40, + citizen_density=0.7, + cop_density=0.074, + citizen_vision=7, + cop_vision=7, + legitimacy=0.8, + max_jail_term=1000, + active_threshold=0.1, + arrest_prob_constant=2.3, + movement=True, + max_iters=1000, + seed=None, ): super().__init__(seed) if cop_density + citizen_density > 1: diff --git a/mesa/experimental/devs/examples/wolf_sheep.py b/mesa/experimental/devs/examples/wolf_sheep.py index 79b695409cf..0313b64ad5e 100644 --- a/mesa/experimental/devs/examples/wolf_sheep.py +++ b/mesa/experimental/devs/examples/wolf_sheep.py @@ -105,7 +105,9 @@ def fully_grown(self, value: bool): if not value: self.model.simulator.schedule_event_relative( - setattr, self.grass_regrowth_time, function_args=[self, "fully_grown", True] + setattr, + self.grass_regrowth_time, + function_args=[self, "fully_grown", True], ) def __init__(self, unique_id, model, fully_grown, countdown, grass_regrowth_time): diff --git a/mesa/experimental/devs/simulator.py b/mesa/experimental/devs/simulator.py index 3a478805f55..ee36df3fd24 100644 --- a/mesa/experimental/devs/simulator.py +++ b/mesa/experimental/devs/simulator.py @@ -87,8 +87,13 @@ def schedule_event_now( SimulationEvent: the simulation event that is scheduled """ - return self.schedule_event_relative(function, 0, priority=priority, - function_args=function_args, function_kwargs=function_kwargs) + return self.schedule_event_relative( + function, + 0, + priority=priority, + function_args=function_args, + function_kwargs=function_kwargs, + ) def schedule_event_absolute( self, From d0611e37adc61b34c316c1ffe8de8e574502081a Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Mon, 11 Mar 2024 20:43:16 +0100 Subject: [PATCH 28/44] move methods to after init --- .../devs/examples/epstein_civil_violence.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/mesa/experimental/devs/examples/epstein_civil_violence.py b/mesa/experimental/devs/examples/epstein_civil_violence.py index f53e3e4c687..d1baa2e6f18 100644 --- a/mesa/experimental/devs/examples/epstein_civil_violence.py +++ b/mesa/experimental/devs/examples/epstein_civil_violence.py @@ -44,15 +44,6 @@ class Citizen(EpsteinAgent): rebellion """ - def sent_to_jail(self, value): - self.model.active_agents.remove(self) - self.condition = AgentState.ARRESTED - self.model.simulator.schedule_event_relative(self.release_from_jail, value) - - def release_from_jail(self): - self.model.active_agents.add(self) - self.condition = AgentState.QUIESCENT - def __init__( self, unique_id, @@ -130,6 +121,15 @@ def update_estimated_arrest_probability(self): self.arrest_probability = 1 - math.exp( -1 * self.arrest_prob_constant * (cops_in_vision / actives_in_vision) ) + + def sent_to_jail(self, value): + self.model.active_agents.remove(self) + self.condition = AgentState.ARRESTED + self.model.simulator.schedule_event_relative(self.release_from_jail, value) + + def release_from_jail(self): + self.model.active_agents.add(self) + self.condition = AgentState.QUIESCENT class Cop(EpsteinAgent): From 287f1386f52ebb2d420f0fdc835be36859d6c92a Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Mon, 11 Mar 2024 20:56:33 +0100 Subject: [PATCH 29/44] cleanup --- mesa/experimental/devs/eventlist.py | 6 +++--- mesa/experimental/devs/examples/epstein_civil_violence.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mesa/experimental/devs/eventlist.py b/mesa/experimental/devs/eventlist.py index df870abbdc5..bc9ee390f94 100644 --- a/mesa/experimental/devs/eventlist.py +++ b/mesa/experimental/devs/eventlist.py @@ -15,12 +15,12 @@ class Priority(IntEnum): class SimulationEvent: """A simulation event - the callable is wrapped using weakrefs, so there is no need to explicitly cancel event if e.g., an agent + the callable is wrapped using weakref, so there is no need to explicitly cancel event if e.g., an agent is removed from the simulation. Attributes: time (float): The simulation time of the event - function (Callable): The function to execute for this event + fn (Callable): The function to execute for this event priority (Priority): The priority of the event unique_id (int) the unique identifier of the event function_args (list[Any]): Argument for the function @@ -155,7 +155,7 @@ def remove(self, event: SimulationEvent) -> None: """remove an event from the event list""" # we cannot simply remove items from _eventlist because this breaks # heap structure invariant. So, we use a form of lazy deletion. - # SimEvents have a CANCELED flag that we set to True, while poping and peak_ahead + # SimEvents have a CANCELED flag that we set to True, while popping and peak_ahead # silently ignore canceled events event.cancel() diff --git a/mesa/experimental/devs/examples/epstein_civil_violence.py b/mesa/experimental/devs/examples/epstein_civil_violence.py index d1baa2e6f18..9a7314e0ff3 100644 --- a/mesa/experimental/devs/examples/epstein_civil_violence.py +++ b/mesa/experimental/devs/examples/epstein_civil_violence.py @@ -121,7 +121,7 @@ def update_estimated_arrest_probability(self): self.arrest_probability = 1 - math.exp( -1 * self.arrest_prob_constant * (cops_in_vision / actives_in_vision) ) - + def sent_to_jail(self, value): self.model.active_agents.remove(self) self.condition = AgentState.ARRESTED From 6b03f38cf8b674f01ff306427f0169881f72f163 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sun, 24 Mar 2024 11:06:10 +0100 Subject: [PATCH 30/44] unittests for eventlist.py --- mesa/experimental/devs/eventlist.py | 7 +- tests/test_devs.py | 134 ++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+), 3 deletions(-) create mode 100644 tests/test_devs.py diff --git a/mesa/experimental/devs/eventlist.py b/mesa/experimental/devs/eventlist.py index bc9ee390f94..21e464326c2 100644 --- a/mesa/experimental/devs/eventlist.py +++ b/mesa/experimental/devs/eventlist.py @@ -43,6 +43,9 @@ def __init__( function_kwargs: dict[str, Any] | None = None, ) -> None: super().__init__() + if not callable(function): + raise Exception() + self.time = time self.priority = priority.value self._canceled = False @@ -57,8 +60,6 @@ def __init__( self.function_args = function_args if function_args else [] self.function_kwargs = function_kwargs if function_kwargs else {} - if self.fn is None: - raise Exception() def execute(self): """execute this event""" @@ -105,7 +106,7 @@ def add_event(self, event: SimulationEvent): heappush(self._events, event) - def peek_ahead(self, n: int = 1) -> list[SimulationEvent]: + def peak_ahead(self, n: int = 1) -> list[SimulationEvent]: """Look at the first n non-canceled event in the event list Args: diff --git a/tests/test_devs.py b/tests/test_devs.py new file mode 100644 index 00000000000..c1c2113fc5b --- /dev/null +++ b/tests/test_devs.py @@ -0,0 +1,134 @@ +import pytest + +from unittest.mock import MagicMock + +from mesa.experimental.devs.simulator import ABMSimulator, DEVSimulator, Simulator +from mesa.experimental.devs.eventlist import EventList, SimulationEvent, Priority + + +from mesa import Model + +def test_simulator(): + pass + +def test_abms_simulator(): + pass + +def test_devs_simulator(): + pass + + +def test_simulation_event(): + some_test_function = MagicMock() + + time = 10 + event = SimulationEvent(time, some_test_function, priority=Priority.DEFAULT, function_args=[], function_kwargs={}) + + assert event.time == time + assert event.fn() is some_test_function + assert event.function_args == [] + assert event.function_kwargs == {} + assert event.priority == Priority.DEFAULT + + # execute + event.execute() + some_test_function.assert_called_once() + + with pytest.raises(Exception): + SimulationEvent(time, None, priority=Priority.DEFAULT, function_args=[], function_kwargs={}) + + # check calling with arguments + some_test_function = MagicMock() + event = SimulationEvent(time, some_test_function, priority=Priority.DEFAULT, function_args=['1'], function_kwargs={'x':2}) + event.execute() + some_test_function.assert_called_once_with('1', x=2) + + + # check if we pass over deletion of callable silenty because of weakrefs + some_test_function = lambda x, y: x + y + event = SimulationEvent(time, some_test_function, priority=Priority.DEFAULT) + del some_test_function + event.execute() + + # cancel + some_test_function = MagicMock() + event = SimulationEvent(time, some_test_function, priority=Priority.DEFAULT, function_args=['1'], + function_kwargs={'x': 2}) + event.cancel() + assert event.fn is None + assert event.function_args == [] + assert event.function_kwargs == {} + assert event.priority == Priority.DEFAULT + assert event.CANCELED + + # comparison for sorting + event1 = SimulationEvent(10, some_test_function, priority=Priority.DEFAULT, function_args=[], function_kwargs={}) + event2 = SimulationEvent(10, some_test_function, priority=Priority.DEFAULT, function_args=[], function_kwargs={}) + assert event1 < event2 # based on just unique_id as tiebraker + + event1 = SimulationEvent(11, some_test_function, priority=Priority.DEFAULT, function_args=[], function_kwargs={}) + event2 = SimulationEvent(10, some_test_function, priority=Priority.DEFAULT, function_args=[], function_kwargs={}) + assert event1 > event2 + + event1 = SimulationEvent(10, some_test_function, priority=Priority.DEFAULT, function_args=[], function_kwargs={}) + event2 = SimulationEvent(10, some_test_function, priority=Priority.HIGH, function_args=[], function_kwargs={}) + assert event1 > event2 + +def test_eventlist(): + event_list = EventList() + + assert len(event_list._events) == 0 + assert isinstance(event_list._events, list) + assert event_list.is_empty() + + # add event + some_test_function = MagicMock() + event = SimulationEvent(1, some_test_function, priority=Priority.DEFAULT, function_args=[], function_kwargs={}) + event_list.add_event(event) + assert len(event_list) == 1 + assert event in event_list + + # remove event + event_list.remove(event) + assert len(event_list) == 1 + assert event.CANCELED + + # peak ahead + event_list = EventList() + for i in range(10): + event = SimulationEvent(i, some_test_function, priority=Priority.DEFAULT, function_args=[], function_kwargs={}) + event_list.add_event(event) + events = event_list.peak_ahead(2) + assert len(events) == 2 + assert events[0].time == 0 + assert events[1].time == 1 + + events = event_list.peak_ahead(11) + assert len(events) == 10 + + event_list._events[6].cancel() + events = event_list.peak_ahead(10) + assert len(events) == 9 + + event_list = EventList() + with pytest.raises(Exception): + event_list.peak_ahead() + + # pop event + event_list = EventList() + for i in range(10): + event = SimulationEvent(i, some_test_function, priority=Priority.DEFAULT, function_args=[], function_kwargs={}) + event_list.add_event(event) + event = event_list.pop_event() + assert event.time == 0 + + event_list = EventList() + event = SimulationEvent(9, some_test_function, priority=Priority.DEFAULT, function_args=[], function_kwargs={}) + event_list.add_event(event) + event.cancel() + with pytest.raises(Exception): + event_list.pop_event() + + # clear + event_list.clear() + assert len(event_list) == 0 \ No newline at end of file From 75479d3b0a635460caec23548c8c7c8a07693c87 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 24 Mar 2024 10:06:23 +0000 Subject: [PATCH 31/44] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mesa/experimental/devs/eventlist.py | 1 - tests/test_devs.py | 124 ++++++++++++++++++++++------ 2 files changed, 101 insertions(+), 24 deletions(-) diff --git a/mesa/experimental/devs/eventlist.py b/mesa/experimental/devs/eventlist.py index 21e464326c2..6a558dcb0f5 100644 --- a/mesa/experimental/devs/eventlist.py +++ b/mesa/experimental/devs/eventlist.py @@ -60,7 +60,6 @@ def __init__( self.function_args = function_args if function_args else [] self.function_kwargs = function_kwargs if function_kwargs else {} - def execute(self): """execute this event""" fn = self.fn() diff --git a/tests/test_devs.py b/tests/test_devs.py index c1c2113fc5b..6b69e4f08d8 100644 --- a/tests/test_devs.py +++ b/tests/test_devs.py @@ -1,19 +1,18 @@ -import pytest - from unittest.mock import MagicMock -from mesa.experimental.devs.simulator import ABMSimulator, DEVSimulator, Simulator -from mesa.experimental.devs.eventlist import EventList, SimulationEvent, Priority +import pytest +from mesa.experimental.devs.eventlist import EventList, Priority, SimulationEvent -from mesa import Model def test_simulator(): pass + def test_abms_simulator(): pass + def test_devs_simulator(): pass @@ -22,7 +21,13 @@ def test_simulation_event(): some_test_function = MagicMock() time = 10 - event = SimulationEvent(time, some_test_function, priority=Priority.DEFAULT, function_args=[], function_kwargs={}) + event = SimulationEvent( + time, + some_test_function, + priority=Priority.DEFAULT, + function_args=[], + function_kwargs={}, + ) assert event.time == time assert event.fn() is some_test_function @@ -35,14 +40,21 @@ def test_simulation_event(): some_test_function.assert_called_once() with pytest.raises(Exception): - SimulationEvent(time, None, priority=Priority.DEFAULT, function_args=[], function_kwargs={}) + SimulationEvent( + time, None, priority=Priority.DEFAULT, function_args=[], function_kwargs={} + ) # check calling with arguments some_test_function = MagicMock() - event = SimulationEvent(time, some_test_function, priority=Priority.DEFAULT, function_args=['1'], function_kwargs={'x':2}) + event = SimulationEvent( + time, + some_test_function, + priority=Priority.DEFAULT, + function_args=["1"], + function_kwargs={"x": 2}, + ) event.execute() - some_test_function.assert_called_once_with('1', x=2) - + some_test_function.assert_called_once_with("1", x=2) # check if we pass over deletion of callable silenty because of weakrefs some_test_function = lambda x, y: x + y @@ -52,8 +64,13 @@ def test_simulation_event(): # cancel some_test_function = MagicMock() - event = SimulationEvent(time, some_test_function, priority=Priority.DEFAULT, function_args=['1'], - function_kwargs={'x': 2}) + event = SimulationEvent( + time, + some_test_function, + priority=Priority.DEFAULT, + function_args=["1"], + function_kwargs={"x": 2}, + ) event.cancel() assert event.fn is None assert event.function_args == [] @@ -62,18 +79,55 @@ def test_simulation_event(): assert event.CANCELED # comparison for sorting - event1 = SimulationEvent(10, some_test_function, priority=Priority.DEFAULT, function_args=[], function_kwargs={}) - event2 = SimulationEvent(10, some_test_function, priority=Priority.DEFAULT, function_args=[], function_kwargs={}) + event1 = SimulationEvent( + 10, + some_test_function, + priority=Priority.DEFAULT, + function_args=[], + function_kwargs={}, + ) + event2 = SimulationEvent( + 10, + some_test_function, + priority=Priority.DEFAULT, + function_args=[], + function_kwargs={}, + ) assert event1 < event2 # based on just unique_id as tiebraker - event1 = SimulationEvent(11, some_test_function, priority=Priority.DEFAULT, function_args=[], function_kwargs={}) - event2 = SimulationEvent(10, some_test_function, priority=Priority.DEFAULT, function_args=[], function_kwargs={}) + event1 = SimulationEvent( + 11, + some_test_function, + priority=Priority.DEFAULT, + function_args=[], + function_kwargs={}, + ) + event2 = SimulationEvent( + 10, + some_test_function, + priority=Priority.DEFAULT, + function_args=[], + function_kwargs={}, + ) assert event1 > event2 - event1 = SimulationEvent(10, some_test_function, priority=Priority.DEFAULT, function_args=[], function_kwargs={}) - event2 = SimulationEvent(10, some_test_function, priority=Priority.HIGH, function_args=[], function_kwargs={}) + event1 = SimulationEvent( + 10, + some_test_function, + priority=Priority.DEFAULT, + function_args=[], + function_kwargs={}, + ) + event2 = SimulationEvent( + 10, + some_test_function, + priority=Priority.HIGH, + function_args=[], + function_kwargs={}, + ) assert event1 > event2 + def test_eventlist(): event_list = EventList() @@ -83,7 +137,13 @@ def test_eventlist(): # add event some_test_function = MagicMock() - event = SimulationEvent(1, some_test_function, priority=Priority.DEFAULT, function_args=[], function_kwargs={}) + event = SimulationEvent( + 1, + some_test_function, + priority=Priority.DEFAULT, + function_args=[], + function_kwargs={}, + ) event_list.add_event(event) assert len(event_list) == 1 assert event in event_list @@ -96,7 +156,13 @@ def test_eventlist(): # peak ahead event_list = EventList() for i in range(10): - event = SimulationEvent(i, some_test_function, priority=Priority.DEFAULT, function_args=[], function_kwargs={}) + event = SimulationEvent( + i, + some_test_function, + priority=Priority.DEFAULT, + function_args=[], + function_kwargs={}, + ) event_list.add_event(event) events = event_list.peak_ahead(2) assert len(events) == 2 @@ -117,13 +183,25 @@ def test_eventlist(): # pop event event_list = EventList() for i in range(10): - event = SimulationEvent(i, some_test_function, priority=Priority.DEFAULT, function_args=[], function_kwargs={}) + event = SimulationEvent( + i, + some_test_function, + priority=Priority.DEFAULT, + function_args=[], + function_kwargs={}, + ) event_list.add_event(event) event = event_list.pop_event() assert event.time == 0 event_list = EventList() - event = SimulationEvent(9, some_test_function, priority=Priority.DEFAULT, function_args=[], function_kwargs={}) + event = SimulationEvent( + 9, + some_test_function, + priority=Priority.DEFAULT, + function_args=[], + function_kwargs={}, + ) event_list.add_event(event) event.cancel() with pytest.raises(Exception): @@ -131,4 +209,4 @@ def test_eventlist(): # clear event_list.clear() - assert len(event_list) == 0 \ No newline at end of file + assert len(event_list) == 0 From 36e9c9433865f485952d5c6055f2e874b61736c5 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sun, 24 Mar 2024 11:47:20 +0100 Subject: [PATCH 32/44] typo fixes --- mesa/experimental/devs/eventlist.py | 9 +++++---- tests/test_devs.py | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/mesa/experimental/devs/eventlist.py b/mesa/experimental/devs/eventlist.py index 6a558dcb0f5..4c0e567c19a 100644 --- a/mesa/experimental/devs/eventlist.py +++ b/mesa/experimental/devs/eventlist.py @@ -62,9 +62,10 @@ def __init__( def execute(self): """execute this event""" - fn = self.fn() - if fn is not None: - fn(*self.function_args, **self.function_kwargs) + if not self._canceled: + fn = self.fn() + if fn is not None: + fn(*self.function_args, **self.function_kwargs) def cancel(self) -> None: """cancel this event""" @@ -85,7 +86,7 @@ def __lt__(self, other): class EventList: """An event list - This is a heap queue sorted list of events. Events are allways removed from the left, so heapq is a performant and + This is a heap queue sorted list of events. Events are always removed from the left, so heapq is a performant and appropriate data structure. Events are sorted based on their time stamp, their priority, and their unique_id as a tie-breaker, guaranteeing a complete ordering. diff --git a/tests/test_devs.py b/tests/test_devs.py index 6b69e4f08d8..6f7a69dabcb 100644 --- a/tests/test_devs.py +++ b/tests/test_devs.py @@ -56,7 +56,7 @@ def test_simulation_event(): event.execute() some_test_function.assert_called_once_with("1", x=2) - # check if we pass over deletion of callable silenty because of weakrefs + # check if we pass over deletion of callable silently because of weakrefs some_test_function = lambda x, y: x + y event = SimulationEvent(time, some_test_function, priority=Priority.DEFAULT) del some_test_function From 1596a6ac3569142b1065b204e81c9ba17ab23988 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sun, 24 Mar 2024 17:18:35 +0100 Subject: [PATCH 33/44] simulator unit tests --- mesa/experimental/devs/eventlist.py | 1 + mesa/experimental/devs/simulator.py | 62 +++++++++++++------- tests/test_devs.py | 87 ++++++++++++++++++++++++++--- 3 files changed, 123 insertions(+), 27 deletions(-) diff --git a/mesa/experimental/devs/eventlist.py b/mesa/experimental/devs/eventlist.py index 4c0e567c19a..2e3ad57b326 100644 --- a/mesa/experimental/devs/eventlist.py +++ b/mesa/experimental/devs/eventlist.py @@ -1,3 +1,4 @@ +from __future__ import annotations import itertools from enum import IntEnum from heapq import heapify, heappop, heappush diff --git a/mesa/experimental/devs/simulator.py b/mesa/experimental/devs/simulator.py index ee36df3fd24..ae4ed1bb984 100644 --- a/mesa/experimental/devs/simulator.py +++ b/mesa/experimental/devs/simulator.py @@ -50,7 +50,7 @@ def reset(self): self.model = None self.time = self.start_time - def run(self, time_delta: int | float): + def run_for(self, time_delta: int | float): """run the simulator for the specified time delta Args: @@ -60,13 +60,20 @@ def run(self, time_delta: int | float): """ end_time = self.time + time_delta - while self.time < end_time: - self.step() - - def step(self): - event = self.event_list.pop_event() - self.time = event.time - event.execute() + while True: + try: + event = self.event_list.pop_event() + except IndexError: # event list is empty + self.time = end_time + break + + if event.time <= end_time: + self.time = event.time + event.execute() + else: + self.time = end_time + self._schedule_event(event) # reschedule event + break def schedule_event_now( self, @@ -89,7 +96,7 @@ def schedule_event_now( """ return self.schedule_event_relative( function, - 0, + 0.0, priority=priority, function_args=function_args, function_kwargs=function_kwargs, @@ -229,21 +236,36 @@ def schedule_event_next_tick( function_kwargs=function_kwargs, ) - def step(self): - """get the next event from the event list and execute it. - Note - if the event to execute is `model.step`, this method automatically also - schedules a new `model.step` event for the next time tick. This ensures - incremental time progression. + def run_for(self, time_delta: int | float): + """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 """ - event = self.event_list.pop_event() - self.time = event.time - if event.fn() == self.model.step: - self.schedule_event_next_tick(self.model.step, priority=Priority.HIGH) - event.execute() + end_time = self.time + time_delta + while True: + try: + event = self.event_list.pop_event() + except IndexError: + self.time = end_time + break + + + ## FIXME:: do we want to run up to and including? + if event.time < end_time: + self.time = event.time + 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._schedule_event(event) + break class DEVSimulator(Simulator): diff --git a/tests/test_devs.py b/tests/test_devs.py index 6f7a69dabcb..9b12f44ea7f 100644 --- a/tests/test_devs.py +++ b/tests/test_devs.py @@ -1,20 +1,93 @@ -from unittest.mock import MagicMock +from unittest.mock import MagicMock, call import pytest +from mesa import Model from mesa.experimental.devs.eventlist import EventList, Priority, SimulationEvent +from mesa.experimental.devs.simulator import Simulator, DEVSimulator, ABMSimulator +def test_devs_simulator(): + simulator = DEVSimulator() + + # setup + model = MagicMock(spec=Model) + simulator.setup(model) + + assert len(simulator.event_list) == 0 + assert simulator.model == model + assert simulator.time == 0 + + # schedule_event_now + fn1 = MagicMock() + event1 = simulator.schedule_event_now(fn1) + assert event1 in simulator.event_list + assert len(simulator.event_list) == 1 + + # schedule_event_absolute + fn2 = MagicMock() + event2 = simulator.schedule_event_absolute(fn2, 1.0) + assert event2 in simulator.event_list + assert len(simulator.event_list) == 2 + + # schedule_event_relative + fn3 = MagicMock() + event3 = simulator.schedule_event_relative(fn3, 0.5) + assert event3 in simulator.event_list + assert len(simulator.event_list) == 3 + + # run_for + simulator.run_for(0.8) + fn1.assert_called_once() + fn3.assert_called_once() + assert simulator.time == 0.8 + + simulator.run_for(0.2) + fn2.assert_called_once() + assert simulator.time == 1.0 + + simulator.run_for(0.2) + assert simulator.time == 1.2 + + with pytest.raises(ValueError): + simulator.schedule_event_absolute(fn2, 0.5) + + + # cancel_event + simulator = DEVSimulator() + model = MagicMock(spec=Model) + simulator.setup(model) + fn = MagicMock() + event = simulator.schedule_event_relative(fn, 0.5) + simulator.cancel_event(event) + assert event.CANCELED -def test_simulator(): - pass + # simulator reset + simulator.reset() + assert len(simulator.event_list) == 0 + assert simulator.model is None + assert simulator.time == 0.0 + + + +def test_abm_simulator(): + simulator = ABMSimulator() + + # setup + model = MagicMock(spec=Model) + simulator.setup(model) + + # schedule_event_next_tick + fn = MagicMock() + simulator.schedule_event_next_tick(fn) + assert len(simulator.event_list) == 2 + + simulator.run_for(3) + assert model.step.call_count == 3 + assert simulator.time == 3 -def test_abms_simulator(): - pass -def test_devs_simulator(): - pass def test_simulation_event(): From f80fd5b19437123b668efb6db2ae6a13e8e87cdd Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 24 Mar 2024 16:18:44 +0000 Subject: [PATCH 34/44] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mesa/experimental/devs/eventlist.py | 1 + mesa/experimental/devs/simulator.py | 6 +++--- tests/test_devs.py | 11 +++-------- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/mesa/experimental/devs/eventlist.py b/mesa/experimental/devs/eventlist.py index 2e3ad57b326..48af72a4315 100644 --- a/mesa/experimental/devs/eventlist.py +++ b/mesa/experimental/devs/eventlist.py @@ -1,4 +1,5 @@ from __future__ import annotations + import itertools from enum import IntEnum from heapq import heapify, heappop, heappush diff --git a/mesa/experimental/devs/simulator.py b/mesa/experimental/devs/simulator.py index ae4ed1bb984..ae6e4e304c2 100644 --- a/mesa/experimental/devs/simulator.py +++ b/mesa/experimental/devs/simulator.py @@ -236,7 +236,6 @@ def schedule_event_next_tick( function_kwargs=function_kwargs, ) - def run_for(self, time_delta: int | float): """run the simulator for the specified time delta @@ -254,12 +253,13 @@ def run_for(self, time_delta: int | float): self.time = end_time break - ## FIXME:: do we want to run up to and including? if event.time < end_time: self.time = event.time if event.fn() == self.model.step: - self.schedule_event_next_tick(self.model.step, priority=Priority.HIGH) + self.schedule_event_next_tick( + self.model.step, priority=Priority.HIGH + ) event.execute() else: diff --git a/tests/test_devs.py b/tests/test_devs.py index 9b12f44ea7f..4563a2ed2c9 100644 --- a/tests/test_devs.py +++ b/tests/test_devs.py @@ -1,10 +1,11 @@ -from unittest.mock import MagicMock, call +from unittest.mock import MagicMock import pytest from mesa import Model from mesa.experimental.devs.eventlist import EventList, Priority, SimulationEvent -from mesa.experimental.devs.simulator import Simulator, DEVSimulator, ABMSimulator +from mesa.experimental.devs.simulator import ABMSimulator, DEVSimulator + def test_devs_simulator(): simulator = DEVSimulator() @@ -51,7 +52,6 @@ def test_devs_simulator(): with pytest.raises(ValueError): simulator.schedule_event_absolute(fn2, 0.5) - # cancel_event simulator = DEVSimulator() model = MagicMock(spec=Model) @@ -68,7 +68,6 @@ def test_devs_simulator(): assert simulator.time == 0.0 - def test_abm_simulator(): simulator = ABMSimulator() @@ -86,10 +85,6 @@ def test_abm_simulator(): assert simulator.time == 3 - - - - def test_simulation_event(): some_test_function = MagicMock() From a7141342e58350399f7b8987b1671100d7e7cfc9 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sun, 24 Mar 2024 17:54:40 +0100 Subject: [PATCH 35/44] added run_until --- mesa/experimental/devs/simulator.py | 50 ++++++++++++++++------------- tests/test_devs.py | 2 +- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/mesa/experimental/devs/simulator.py b/mesa/experimental/devs/simulator.py index ae6e4e304c2..3140f3d9422 100644 --- a/mesa/experimental/devs/simulator.py +++ b/mesa/experimental/devs/simulator.py @@ -50,16 +50,7 @@ def reset(self): self.model = None self.time = self.start_time - def run_for(self, time_delta: int | float): - """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 + def run_until(self, end_time: int | float) -> None: while True: try: event = self.event_list.pop_event() @@ -75,6 +66,19 @@ def run_for(self, time_delta: int | float): self._schedule_event(event) # reschedule event break + + def run_for(self, time_delta: int | float): + """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 + self.run_until(end_time) + + def schedule_event_now( self, function: Callable, @@ -236,16 +240,7 @@ def schedule_event_next_tick( function_kwargs=function_kwargs, ) - def run_for(self, time_delta: int | float): - """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 + def run_until(self, end_time: int) -> None: while True: try: event = self.event_list.pop_event() @@ -253,8 +248,7 @@ def run_for(self, time_delta: int | float): self.time = end_time break - ## FIXME:: do we want to run up to and including? - if event.time < end_time: + if event.time <= end_time: self.time = event.time if event.fn() == self.model.step: self.schedule_event_next_tick( @@ -267,6 +261,18 @@ def run_for(self, time_delta: int | float): 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. Can be used for full-blown discrete event simulating using diff --git a/tests/test_devs.py b/tests/test_devs.py index 4563a2ed2c9..3eee78160d5 100644 --- a/tests/test_devs.py +++ b/tests/test_devs.py @@ -82,7 +82,7 @@ def test_abm_simulator(): simulator.run_for(3) assert model.step.call_count == 3 - assert simulator.time == 3 + assert simulator.time == 2 def test_simulation_event(): From ca58a7085747e4c4d6cb5b20d10c9a63c788b1a8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 24 Mar 2024 16:56:09 +0000 Subject: [PATCH 36/44] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mesa/experimental/devs/simulator.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/mesa/experimental/devs/simulator.py b/mesa/experimental/devs/simulator.py index 3140f3d9422..e9b22d39808 100644 --- a/mesa/experimental/devs/simulator.py +++ b/mesa/experimental/devs/simulator.py @@ -66,7 +66,6 @@ def run_until(self, end_time: int | float) -> None: self._schedule_event(event) # reschedule event break - def run_for(self, time_delta: int | float): """run the simulator for the specified time delta @@ -78,7 +77,6 @@ def run_for(self, time_delta: int | float): end_time = self.time + time_delta self.run_until(end_time) - def schedule_event_now( self, function: Callable, @@ -261,7 +259,7 @@ def run_until(self, end_time: int) -> None: self._schedule_event(event) break - def run_for(self, time_delta: int ): + def run_for(self, time_delta: int): """run the simulator for the specified time delta Args: @@ -273,7 +271,6 @@ def run_for(self, time_delta: int ): self.run_until(end_time) - class DEVSimulator(Simulator): """A simulator where the unit of time is a float. Can be used for full-blown discrete event simulating using event scheduling. From 4fc4957cf3e7cbc45092801c72cbf567cc0a3ae7 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sun, 24 Mar 2024 18:21:07 +0100 Subject: [PATCH 37/44] doc string update and benchmark fix --- benchmarks/global_benchmark.py | 2 +- mesa/experimental/devs/simulator.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/benchmarks/global_benchmark.py b/benchmarks/global_benchmark.py index 884e56ee168..539e1ca4bbe 100644 --- a/benchmarks/global_benchmark.py +++ b/benchmarks/global_benchmark.py @@ -23,7 +23,7 @@ def run_model(model_class, seed, parameters): end_init_start_run = timeit.default_timer() - simulator.run(config["steps"]) + simulator.run_for(config["steps"]) # for _ in range(config["steps"]): # model.step() diff --git a/mesa/experimental/devs/simulator.py b/mesa/experimental/devs/simulator.py index e9b22d39808..3becda411a1 100644 --- a/mesa/experimental/devs/simulator.py +++ b/mesa/experimental/devs/simulator.py @@ -239,6 +239,12 @@ def schedule_event_next_tick( ) def run_until(self, end_time: int) -> None: + """run the simulator up to and included the specified end time + + Args: + end_time (float| int): The end_time delta. The simulator is until the specified end time + + """ while True: try: event = self.event_list.pop_event() From 273245ddb22defde33cc597c866ebda905358e85 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 27 Mar 2024 09:49:50 +0000 Subject: [PATCH 38/44] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mesa/experimental/devs/examples/wolf_sheep.py | 3 +-- mesa/experimental/devs/simulator.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/mesa/experimental/devs/examples/wolf_sheep.py b/mesa/experimental/devs/examples/wolf_sheep.py index 0313b64ad5e..9fa6b3d96c7 100644 --- a/mesa/experimental/devs/examples/wolf_sheep.py +++ b/mesa/experimental/devs/examples/wolf_sheep.py @@ -39,8 +39,7 @@ def spawn_offspring(self): ) self.model.grid.place_agent(offspring, self.pos) - def feed(self): - ... + def feed(self): ... def die(self): self.model.grid.remove_agent(self) diff --git a/mesa/experimental/devs/simulator.py b/mesa/experimental/devs/simulator.py index 3becda411a1..920e8aba6f8 100644 --- a/mesa/experimental/devs/simulator.py +++ b/mesa/experimental/devs/simulator.py @@ -31,8 +31,7 @@ def __init__(self, time_unit: type, start_time: int | float): self.time = self.start_time self.model = None - def check_time_unit(self, time: int | float) -> bool: - ... + def check_time_unit(self, time: int | float) -> bool: ... def setup(self, model: "Model") -> None: """Setup the simulator with the model to simulate From 046c4bed1ad2a9534724c9c0f4a865b06e11a43e Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sun, 7 Apr 2024 10:06:12 +0200 Subject: [PATCH 39/44] typo fix --- mesa/experimental/devs/simulator.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mesa/experimental/devs/simulator.py b/mesa/experimental/devs/simulator.py index 920e8aba6f8..8ac82752a23 100644 --- a/mesa/experimental/devs/simulator.py +++ b/mesa/experimental/devs/simulator.py @@ -1,3 +1,4 @@ +from __future__ import annotations import numbers from typing import Any, Callable @@ -34,7 +35,7 @@ def __init__(self, time_unit: type, start_time: int | float): def check_time_unit(self, time: int | float) -> bool: ... def setup(self, model: "Model") -> None: - """Setup the simulator with the model to simulate + """Set up the simulator with the model to simulate Args: model (Model): The model to simulate From fbd68bc4a9f46b4c070ec1716b51c1c85dedea91 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 7 Apr 2024 08:06:29 +0000 Subject: [PATCH 40/44] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mesa/experimental/devs/simulator.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mesa/experimental/devs/simulator.py b/mesa/experimental/devs/simulator.py index 8ac82752a23..4e6a56eb271 100644 --- a/mesa/experimental/devs/simulator.py +++ b/mesa/experimental/devs/simulator.py @@ -1,4 +1,5 @@ from __future__ import annotations + import numbers from typing import Any, Callable @@ -34,7 +35,7 @@ def __init__(self, time_unit: type, start_time: int | float): def check_time_unit(self, time: int | float) -> bool: ... - def setup(self, model: "Model") -> None: + def setup(self, model: Model) -> None: """Set up the simulator with the model to simulate Args: From 8e48a291b631b7e070dbdbab57674c4ff29ee037 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sun, 7 Apr 2024 10:21:58 +0200 Subject: [PATCH 41/44] deprecate time.DiscreteEventScheduler --- mesa/experimental/devs/simulator.py | 2 + mesa/time.py | 113 +--------------------------- tests/test_devs.py | 3 +- 3 files changed, 6 insertions(+), 112 deletions(-) diff --git a/mesa/experimental/devs/simulator.py b/mesa/experimental/devs/simulator.py index 4e6a56eb271..11c33f4e074 100644 --- a/mesa/experimental/devs/simulator.py +++ b/mesa/experimental/devs/simulator.py @@ -3,6 +3,8 @@ import numbers from typing import Any, Callable +from mesa import Model + from .eventlist import EventList, Priority, SimulationEvent diff --git a/mesa/time.py b/mesa/time.py index 50d564be551..9d9e706e945 100644 --- a/mesa/time.py +++ b/mesa/time.py @@ -25,9 +25,7 @@ # Remove this __future__ import once the oldest supported Python is 3.10 from __future__ import annotations -import heapq import warnings -import weakref from collections import defaultdict from collections.abc import Iterable @@ -393,46 +391,7 @@ def get_type_count(self, agenttype: type[Agent]) -> int: class DiscreteEventScheduler(BaseScheduler): """ - A scheduler for discrete event simulation in Mesa. - - This scheduler manages events where each event is associated with a - specific time and agent. The scheduler advances time not in fixed - increments, but to the moment the next event is scheduled to occur. - - This implementation uses a priority queue (heapq) to manage events. Each - event is a tuple of the form (time, random_value, agent), where: - - time (float): The scheduled time for the event. - - random_value (float): A secondary sorting criterion to randomize - the order of events that are scheduled for the same time. - - agent (Agent): The agent associated with the event. - - The random value for secondary sorting ensures that when two events are - scheduled for the same time, their execution order is randomized, thus - preventing direct comparison issues between different types of agents and - maintaining the integrity of the simulation's randomness. - - Attributes: - model (Model): The model instance associated with the scheduler. - event_queue (list): A priority queue of scheduled events. - time_step (int or float): The fixed time period by which the model advances - on each step. Defaults to 1. - - Methods: - schedule_event(time, agent): Schedule an event for a specific time. - schedule_in(delay, agent): Schedule an event after a specified delay. - step(): Execute all events within the next time_step period. - get_next_event_time(): Returns the time of the next scheduled event. - - Usage: - 1. Instantiate the DiscreteEventScheduler with a model instance and a time_step period. - 2. Add agents to the scheduler using schedule.add(). With schedule_now=True (default), - the first event for the agent will be scheduled immediately. - 3. In the Agent step() method, schedule the next event for the agent - (using schedule_in or schedule_event). - 3. Add self.schedule.step() to the model's step() method, as usual. - - Now, with each model step, the scheduler will execute all events within the - next time_step period, and advance time one time_step forward. + This class has been deprecated and replaced by the functionality provided by experimental.devs """ def __init__(self, model: Model, time_step: TimeT = 1) -> None: @@ -444,72 +403,4 @@ def __init__(self, model: Model, time_step: TimeT = 1) -> None: """ super().__init__(model) - self.event_queue: list[tuple[TimeT, float, weakref.ref]] = [] - self.time_step: TimeT = time_step # Fixed time period for each step - - warnings.warn( - "The DiscreteEventScheduler is experimental. It may be changed or removed in any and all future releases, including patch releases.\n" - "We would love to hear what you think about this new feature. If you have any thoughts, share them with us here: https://github.com/projectmesa/mesa/discussions/1923", - FutureWarning, - stacklevel=2, - ) - - def schedule_event(self, time: TimeT, agent: Agent) -> None: - """Schedule an event for an agent at a specific time.""" - if time < self.time: - raise ValueError( - f"Scheduled time ({time}) must be >= the current time ({self.time})" - ) - if agent not in self._agents: - raise ValueError( - "trying to schedule an event for agent which is not known to the scheduler" - ) - - # Create an event, sorted first on time, secondary on a random value - event = (time, self.model.random.random(), weakref.ref(agent)) - heapq.heappush(self.event_queue, event) - - def schedule_in(self, delay: TimeT, agent: Agent) -> None: - """Schedule an event for an agent after a specified delay.""" - if delay < 0: - raise ValueError("Delay must be non-negative") - event_time = self.time + delay - self.schedule_event(event_time, agent) - - def step(self) -> None: - """Execute the next event and advance the time.""" - end_time = self.time + self.time_step - - while self.event_queue and self.event_queue[0][0] <= end_time: - # Get the next event (ignore the random value during unpacking) - time, _, agent = heapq.heappop(self.event_queue) - agent = agent() # unpack weakref - - if agent: - # Advance time to the event's time - self.time = time - # Execute the event - agent.step() - - # After processing events, advance time by the time_step - self.time = end_time - self.steps += 1 - - def get_next_event_time(self) -> TimeT | None: - """Returns the time of the next scheduled event.""" - if not self.event_queue: - return None - return self.event_queue[0][0] - - def add(self, agent: Agent, schedule_now: bool = True) -> None: - """Add an Agent object to the schedule and optionally schedule its first event. - - Args: - agent: An Agent to be added to the schedule. Must have a step() method. - schedule_now: If True, schedules the first event for the agent immediately. - """ - super().add(agent) # Call the add method from BaseScheduler - - if schedule_now: - # Schedule the first event immediately - self.schedule_event(self.time, agent) + raise Exception("DiscreteEventScheduler is deprecated in favor of the functionality provided by experimental.devs") diff --git a/tests/test_devs.py b/tests/test_devs.py index 3eee78160d5..f01e4742efe 100644 --- a/tests/test_devs.py +++ b/tests/test_devs.py @@ -125,7 +125,8 @@ def test_simulation_event(): some_test_function.assert_called_once_with("1", x=2) # check if we pass over deletion of callable silently because of weakrefs - some_test_function = lambda x, y: x + y + def some_test_function(x, y): + return x + y event = SimulationEvent(time, some_test_function, priority=Priority.DEFAULT) del some_test_function event.execute() From b7237b82864029cc2d320ba6104421be0d66e531 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 7 Apr 2024 08:22:17 +0000 Subject: [PATCH 42/44] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mesa/time.py | 4 +++- tests/test_devs.py | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/mesa/time.py b/mesa/time.py index 9d9e706e945..10fa4005ac2 100644 --- a/mesa/time.py +++ b/mesa/time.py @@ -403,4 +403,6 @@ def __init__(self, model: Model, time_step: TimeT = 1) -> None: """ super().__init__(model) - raise Exception("DiscreteEventScheduler is deprecated in favor of the functionality provided by experimental.devs") + raise Exception( + "DiscreteEventScheduler is deprecated in favor of the functionality provided by experimental.devs" + ) diff --git a/tests/test_devs.py b/tests/test_devs.py index f01e4742efe..06d3629ed16 100644 --- a/tests/test_devs.py +++ b/tests/test_devs.py @@ -127,6 +127,7 @@ def test_simulation_event(): # check if we pass over deletion of callable silently because of weakrefs def some_test_function(x, y): return x + y + event = SimulationEvent(time, some_test_function, priority=Priority.DEFAULT) del some_test_function event.execute() From 47e3c3f9c8a79125efcb723f9a7e67faa2533b56 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sun, 7 Apr 2024 10:35:06 +0200 Subject: [PATCH 43/44] remove unit test for time.DiscreteEventScheduler --- tests/test_time.py | 114 --------------------------------------------- 1 file changed, 114 deletions(-) diff --git a/tests/test_time.py b/tests/test_time.py index 53967d1a8ef..85e4a4151ae 100644 --- a/tests/test_time.py +++ b/tests/test_time.py @@ -339,119 +339,5 @@ def test_random_activation_counts(self): # model.schedule.add(b) -class TestDiscreteEventScheduler(TestCase): - def setUp(self): - self.model = MockModel() - self.scheduler = DiscreteEventScheduler(self.model, time_step=1) - self.model.schedule = self.scheduler - self.agent1 = MockAgent(1, self.model) - self.agent2 = MockAgent(2, self.model) - self.model.schedule.add(self.agent1, schedule_now=False) - self.model.schedule.add(self.agent2, schedule_now=False) - - # Testing Initialization and Attributes - def test_initialization(self): - self.assertIsInstance(self.scheduler.event_queue, list) - self.assertEqual(self.scheduler.time_step, 1) - - # Testing Event Scheduling - def test_schedule_event(self): - self.scheduler.schedule_event(5, self.agent1) - self.assertEqual(len(self.scheduler.event_queue), 1) - event_time, _, event_agent = self.scheduler.event_queue[0] - self.assertEqual(event_time, 5) - self.assertEqual(event_agent(), self.agent1) - - def test_schedule_event_with_float_time(self): - self.scheduler.schedule_event(5.5, self.agent1) - self.assertEqual(len(self.scheduler.event_queue), 1) - event_time, _, event_agent = self.scheduler.event_queue[0] - self.assertEqual(event_time, 5.5) - self.assertEqual(event_agent(), self.agent1) - - def test_schedule_in(self): - self.scheduler.schedule_in(3, self.agent2) - _, _, event_agent = self.scheduler.event_queue[0] - self.assertEqual(event_agent(), self.agent2) - self.assertEqual(self.scheduler.get_next_event_time(), self.scheduler.time + 3) - - # Testing Event Execution and Time Advancement - def test_step_function(self): - self.scheduler.schedule_event(1, self.agent1) - self.scheduler.schedule_event(2, self.agent2) - self.scheduler.step() - self.assertEqual(self.scheduler.time, 1) - self.assertEqual(self.agent1.steps, 1) - self.assertEqual(self.agent2.steps, 0) - - def test_time_advancement(self): - self.scheduler.schedule_event(5, self.agent1) - self.scheduler.step() - self.assertEqual(self.scheduler.time, 1) - self.scheduler.step() - self.assertEqual(self.scheduler.time, 2) - - def test_no_events(self): - self.scheduler.step() - self.assertEqual(self.scheduler.time, 1) - - # Testing Edge Cases and Error Handling - def test_invalid_event_time(self): - with self.assertRaises(ValueError): - self.scheduler.schedule_event(-1, self.agent1) - - def test_invalid_aget_time(self): - with self.assertRaises(ValueError): - agent3 = MockAgent(3, self.model) - self.scheduler.schedule_event(2, agent3) - - def test_immediate_event_execution(self): - # Current time of the scheduler - current_time = self.scheduler.time - - # Schedule an event at the current time - self.scheduler.schedule_event(current_time, self.agent1) - - # Step the scheduler and check if the event is executed immediately - self.scheduler.step() - self.assertEqual(self.agent1.steps, 1) - - # The time should advance to the next time step after execution - self.assertEqual(self.scheduler.time, current_time + 1) - - # Testing Utility Functions - def test_get_next_event_time(self): - self.scheduler.schedule_event(10, self.agent1) - self.assertEqual(self.scheduler.get_next_event_time(), 10) - - # Test add() method with and without immediate scheduling - def test_add_with_immediate_scheduling(self): - # Add an agent with schedule_now set to True (default behavior) - new_agent = MockAgent(3, self.model) - self.scheduler.add(new_agent) - - # Check if the agent's first event is scheduled immediately - self.assertEqual(len(self.scheduler.event_queue), 1) - event_time, _, event_agent = self.scheduler.event_queue[0] - self.assertEqual(event_time, self.scheduler.time) - self.assertEqual(event_agent(), new_agent) - - # Step the scheduler and check if the agent's step method is executed - self.scheduler.step() - self.assertEqual(new_agent.steps, 1) - - def test_add_without_immediate_scheduling(self): - # Add an agent with schedule_now set to False - new_agent = MockAgent(4, self.model) - self.scheduler.add(new_agent, schedule_now=False) - - # Check if the event queue is not updated - self.assertEqual(len(self.scheduler.event_queue), 0) - - # Step the scheduler and verify that the agent's step method is not executed - self.scheduler.step() - self.assertEqual(new_agent.steps, 0) - - if __name__ == "__main__": unittest.main() From 4f1b88f970ddc808bb484d52ecd01360906853f1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 7 Apr 2024 08:35:21 +0000 Subject: [PATCH 44/44] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_time.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_time.py b/tests/test_time.py index 85e4a4151ae..a9d8e2e6055 100644 --- a/tests/test_time.py +++ b/tests/test_time.py @@ -8,7 +8,6 @@ from mesa import Agent, Model from mesa.time import ( BaseScheduler, - DiscreteEventScheduler, RandomActivation, RandomActivationByType, SimultaneousActivation,