Mesa 2.3.0.dev0__py3-none-any.whl → 2.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of Mesa might be problematic. Click here for more details.

@@ -0,0 +1,250 @@
1
+ """
2
+ Wolf-Sheep Predation Model
3
+ ================================
4
+
5
+ Replication of the model found in NetLogo:
6
+ Wilensky, U. (1997). NetLogo Wolf Sheep Predation model.
7
+ http://ccl.northwestern.edu/netlogo/models/WolfSheepPredation.
8
+ Center for Connected Learning and Computer-Based Modeling,
9
+ Northwestern University, Evanston, IL.
10
+ """
11
+
12
+ import mesa
13
+ from mesa.experimental.devs.simulator import ABMSimulator
14
+
15
+
16
+ class Animal(mesa.Agent):
17
+ def __init__(self, unique_id, model, moore, energy, p_reproduce, energy_from_food):
18
+ super().__init__(unique_id, model)
19
+ self.energy = energy
20
+ self.p_reproduce = p_reproduce
21
+ self.energy_from_food = energy_from_food
22
+ self.moore = moore
23
+
24
+ def random_move(self):
25
+ next_moves = self.model.grid.get_neighborhood(self.pos, self.moore, True)
26
+ next_move = self.random.choice(next_moves)
27
+ # Now move:
28
+ self.model.grid.move_agent(self, next_move)
29
+
30
+ def spawn_offspring(self):
31
+ self.energy /= 2
32
+ offspring = self.__class__(
33
+ self.model.next_id(),
34
+ self.model,
35
+ self.moore,
36
+ self.energy,
37
+ self.p_reproduce,
38
+ self.energy_from_food,
39
+ )
40
+ self.model.grid.place_agent(offspring, self.pos)
41
+
42
+ def feed(self): ...
43
+
44
+ def die(self):
45
+ self.model.grid.remove_agent(self)
46
+ self.remove()
47
+
48
+ def step(self):
49
+ self.random_move()
50
+ self.energy -= 1
51
+
52
+ self.feed()
53
+
54
+ if self.energy < 0:
55
+ self.die()
56
+ elif self.random.random() < self.p_reproduce:
57
+ self.spawn_offspring()
58
+
59
+
60
+ class Sheep(Animal):
61
+ """
62
+ A sheep that walks around, reproduces (asexually) and gets eaten.
63
+
64
+ The init is the same as the RandomWalker.
65
+ """
66
+
67
+ def feed(self):
68
+ # If there is grass available, eat it
69
+ agents = self.model.grid.get_cell_list_contents(self.pos)
70
+ grass_patch = next(obj for obj in agents if isinstance(obj, GrassPatch))
71
+ if grass_patch.fully_grown:
72
+ self.energy += self.energy_from_food
73
+ grass_patch.fully_grown = False
74
+
75
+
76
+ class Wolf(Animal):
77
+ """
78
+ A wolf that walks around, reproduces (asexually) and eats sheep.
79
+ """
80
+
81
+ def feed(self):
82
+ agents = self.model.grid.get_cell_list_contents(self.pos)
83
+ sheep = [obj for obj in agents if isinstance(obj, Sheep)]
84
+ if len(sheep) > 0:
85
+ sheep_to_eat = self.random.choice(sheep)
86
+ self.energy += self.energy
87
+
88
+ # Kill the sheep
89
+ sheep_to_eat.die()
90
+
91
+
92
+ class GrassPatch(mesa.Agent):
93
+ """
94
+ A patch of grass that grows at a fixed rate and it is eaten by sheep
95
+ """
96
+
97
+ @property
98
+ def fully_grown(self) -> bool:
99
+ return self._fully_grown
100
+
101
+ @fully_grown.setter
102
+ def fully_grown(self, value: bool):
103
+ self._fully_grown = value
104
+
105
+ if not value:
106
+ self.model.simulator.schedule_event_relative(
107
+ setattr,
108
+ self.grass_regrowth_time,
109
+ function_args=[self, "fully_grown", True],
110
+ )
111
+
112
+ def __init__(self, unique_id, model, fully_grown, countdown, grass_regrowth_time):
113
+ """
114
+ Creates a new patch of grass
115
+
116
+ Args:
117
+ grown: (boolean) Whether the patch of grass is fully grown or not
118
+ countdown: Time for the patch of grass to be fully grown again
119
+ """
120
+ super().__init__(unique_id, model)
121
+ self._fully_grown = fully_grown
122
+ self.grass_regrowth_time = grass_regrowth_time
123
+
124
+ if not self.fully_grown:
125
+ self.model.simulator.schedule_event_relative(
126
+ setattr, countdown, function_args=[self, "fully_grown", True]
127
+ )
128
+
129
+ def set_fully_grown(self):
130
+ self.fully_grown = True
131
+
132
+
133
+ class WolfSheep(mesa.Model):
134
+ """
135
+ Wolf-Sheep Predation Model
136
+
137
+ A model for simulating wolf and sheep (predator-prey) ecosystem modelling.
138
+ """
139
+
140
+ def __init__(
141
+ self,
142
+ height,
143
+ width,
144
+ initial_sheep,
145
+ initial_wolves,
146
+ sheep_reproduce,
147
+ wolf_reproduce,
148
+ grass_regrowth_time,
149
+ wolf_gain_from_food=13,
150
+ sheep_gain_from_food=5,
151
+ moore=False,
152
+ simulator=None,
153
+ seed=None,
154
+ ):
155
+ """
156
+ Create a new Wolf-Sheep model with the given parameters.
157
+
158
+ Args:
159
+ initial_sheep: Number of sheep to start with
160
+ initial_wolves: Number of wolves to start with
161
+ sheep_reproduce: Probability of each sheep reproducing each step
162
+ wolf_reproduce: Probability of each wolf reproducing each step
163
+ wolf_gain_from_food: Energy a wolf gains from eating a sheep
164
+ grass: Whether to have the sheep eat grass for energy
165
+ grass_regrowth_time: How long it takes for a grass patch to regrow
166
+ once it is eaten
167
+ sheep_gain_from_food: Energy sheep gain from grass, if enabled.
168
+ moore:
169
+ """
170
+ super().__init__(seed=seed)
171
+ # Set parameters
172
+ self.height = height
173
+ self.width = width
174
+ self.initial_sheep = initial_sheep
175
+ self.initial_wolves = initial_wolves
176
+ self.simulator = simulator
177
+
178
+ # self.sheep_reproduce = sheep_reproduce
179
+ # self.wolf_reproduce = wolf_reproduce
180
+ # self.grass_regrowth_time = grass_regrowth_time
181
+ # self.wolf_gain_from_food = wolf_gain_from_food
182
+ # self.sheep_gain_from_food = sheep_gain_from_food
183
+ # self.moore = moore
184
+
185
+ self.grid = mesa.space.MultiGrid(self.height, self.width, torus=False)
186
+
187
+ for _ in range(self.initial_sheep):
188
+ pos = (
189
+ self.random.randrange(self.width),
190
+ self.random.randrange(self.height),
191
+ )
192
+ energy = self.random.randrange(2 * sheep_gain_from_food)
193
+ sheep = Sheep(
194
+ self.next_id(),
195
+ self,
196
+ moore,
197
+ energy,
198
+ sheep_reproduce,
199
+ sheep_gain_from_food,
200
+ )
201
+ self.grid.place_agent(sheep, pos)
202
+
203
+ # Create wolves
204
+ for _ in range(self.initial_wolves):
205
+ pos = (
206
+ self.random.randrange(self.width),
207
+ self.random.randrange(self.height),
208
+ )
209
+ energy = self.random.randrange(2 * wolf_gain_from_food)
210
+ wolf = Wolf(
211
+ self.next_id(),
212
+ self,
213
+ moore,
214
+ energy,
215
+ wolf_reproduce,
216
+ wolf_gain_from_food,
217
+ )
218
+ self.grid.place_agent(wolf, pos)
219
+
220
+ # Create grass patches
221
+ possibly_fully_grown = [True, False]
222
+ for _agent, pos in self.grid.coord_iter():
223
+ fully_grown = self.random.choice(possibly_fully_grown)
224
+ if fully_grown:
225
+ countdown = grass_regrowth_time
226
+ else:
227
+ countdown = self.random.randrange(grass_regrowth_time)
228
+ patch = GrassPatch(
229
+ self.next_id(), self, fully_grown, countdown, grass_regrowth_time
230
+ )
231
+ self.grid.place_agent(patch, pos)
232
+
233
+ def step(self):
234
+ self.get_agents_of_type(Sheep).shuffle(inplace=True).do("step")
235
+ self.get_agents_of_type(Wolf).shuffle(inplace=True).do("step")
236
+
237
+
238
+ if __name__ == "__main__":
239
+ import time
240
+
241
+ simulator = ABMSimulator()
242
+
243
+ model = WolfSheep(25, 25, 60, 40, 0.2, 0.1, 20, simulator=simulator, seed=15)
244
+
245
+ simulator.setup(model)
246
+
247
+ start_time = time.perf_counter()
248
+ simulator.run(100)
249
+ print(simulator.time)
250
+ print("Time:", time.perf_counter() - start_time)
@@ -0,0 +1,293 @@
1
+ from __future__ import annotations
2
+
3
+ import numbers
4
+ from typing import Any, Callable
5
+
6
+ from mesa import Model
7
+
8
+ from .eventlist import EventList, Priority, SimulationEvent
9
+
10
+
11
+ class Simulator:
12
+ """The Simulator controls the time advancement of the model.
13
+
14
+ The simulator uses next event time progression to advance the simulation time, and execute the next event
15
+
16
+ Attributes:
17
+ event_list (EventList): The list of events to execute
18
+ time (float | int): The current simulation time
19
+ time_unit (type) : The unit of the simulation time
20
+ model (Model): The model to simulate
21
+
22
+
23
+ """
24
+
25
+ # TODO: add replication support
26
+ # TODO: add experimentation support
27
+
28
+ def __init__(self, time_unit: type, start_time: int | float):
29
+ # should model run in a separate thread,
30
+ # and we can then interact with start, stop, run_until, and step?
31
+ self.event_list = EventList()
32
+ self.start_time = start_time
33
+ self.time_unit = time_unit
34
+
35
+ self.time = self.start_time
36
+ self.model = None
37
+
38
+ def check_time_unit(self, time: int | float) -> bool: ...
39
+
40
+ def setup(self, model: Model) -> None:
41
+ """Set up the simulator with the model to simulate
42
+
43
+ Args:
44
+ model (Model): The model to simulate
45
+
46
+ """
47
+ self.event_list.clear()
48
+ self.model = model
49
+
50
+ def reset(self):
51
+ """Reset the simulator by clearing the event list and removing the model to simulate"""
52
+ self.event_list.clear()
53
+ self.model = None
54
+ self.time = self.start_time
55
+
56
+ def run_until(self, end_time: int | float) -> None:
57
+ while True:
58
+ try:
59
+ event = self.event_list.pop_event()
60
+ except IndexError: # event list is empty
61
+ self.time = end_time
62
+ break
63
+
64
+ if event.time <= end_time:
65
+ self.time = event.time
66
+ event.execute()
67
+ else:
68
+ self.time = end_time
69
+ self._schedule_event(event) # reschedule event
70
+ break
71
+
72
+ def run_for(self, time_delta: int | float):
73
+ """run the simulator for the specified time delta
74
+
75
+ Args:
76
+ time_delta (float| int): The time delta. The simulator is run from the current time to the current time
77
+ plus the time delta
78
+
79
+ """
80
+ end_time = self.time + time_delta
81
+ self.run_until(end_time)
82
+
83
+ def schedule_event_now(
84
+ self,
85
+ function: Callable,
86
+ priority: Priority = Priority.DEFAULT,
87
+ function_args: list[Any] | None = None,
88
+ function_kwargs: dict[str, Any] | None = None,
89
+ ) -> SimulationEvent:
90
+ """Schedule event for the current time instant
91
+
92
+ Args:
93
+ function (Callable): The callable to execute for this event
94
+ priority (Priority): the priority of the event, optional
95
+ function_args (List[Any]): list of arguments for function
96
+ function_kwargs (Dict[str, Any]): dict of keyword arguments for function
97
+
98
+ Returns:
99
+ SimulationEvent: the simulation event that is scheduled
100
+
101
+ """
102
+ return self.schedule_event_relative(
103
+ function,
104
+ 0.0,
105
+ priority=priority,
106
+ function_args=function_args,
107
+ function_kwargs=function_kwargs,
108
+ )
109
+
110
+ def schedule_event_absolute(
111
+ self,
112
+ function: Callable,
113
+ time: int | float,
114
+ priority: Priority = Priority.DEFAULT,
115
+ function_args: list[Any] | None = None,
116
+ function_kwargs: dict[str, Any] | None = None,
117
+ ) -> SimulationEvent:
118
+ """Schedule event for the specified time instant
119
+
120
+ Args:
121
+ function (Callable): The callable to execute for this event
122
+ time (int | float): the time for which to schedule the event
123
+ priority (Priority): the priority of the event, optional
124
+ function_args (List[Any]): list of arguments for function
125
+ function_kwargs (Dict[str, Any]): dict of keyword arguments for function
126
+
127
+ Returns:
128
+ SimulationEvent: the simulation event that is scheduled
129
+
130
+ """
131
+ if self.time > time:
132
+ raise ValueError("trying to schedule an event in the past")
133
+
134
+ event = SimulationEvent(
135
+ time,
136
+ function,
137
+ priority=priority,
138
+ function_args=function_args,
139
+ function_kwargs=function_kwargs,
140
+ )
141
+ self._schedule_event(event)
142
+ return event
143
+
144
+ def schedule_event_relative(
145
+ self,
146
+ function: Callable,
147
+ time_delta: int | float,
148
+ priority: Priority = Priority.DEFAULT,
149
+ function_args: list[Any] | None = None,
150
+ function_kwargs: dict[str, Any] | None = None,
151
+ ) -> SimulationEvent:
152
+ """Schedule event for the current time plus the time delta
153
+
154
+ Args:
155
+ function (Callable): The callable to execute for this event
156
+ time_delta (int | float): the time delta
157
+ priority (Priority): the priority of the event, optional
158
+ function_args (List[Any]): list of arguments for function
159
+ function_kwargs (Dict[str, Any]): dict of keyword arguments for function
160
+
161
+ Returns:
162
+ SimulationEvent: the simulation event that is scheduled
163
+
164
+ """
165
+ event = SimulationEvent(
166
+ self.time + time_delta,
167
+ function,
168
+ priority=priority,
169
+ function_args=function_args,
170
+ function_kwargs=function_kwargs,
171
+ )
172
+ self._schedule_event(event)
173
+ return event
174
+
175
+ def cancel_event(self, event: SimulationEvent) -> None:
176
+ """remove the event from the event list
177
+
178
+ Args:
179
+ event (SimulationEvent): The simulation event to remove
180
+
181
+ """
182
+
183
+ self.event_list.remove(event)
184
+
185
+ def _schedule_event(self, event: SimulationEvent):
186
+ if not self.check_time_unit(event.time):
187
+ raise ValueError(
188
+ f"time unit mismatch {event.time} is not of time unit {self.time_unit}"
189
+ )
190
+
191
+ # check timeunit of events
192
+ self.event_list.add_event(event)
193
+
194
+
195
+ class ABMSimulator(Simulator):
196
+ """This simulator uses incremental time progression, while allowing for additional event scheduling.
197
+
198
+ The basic time unit of this simulator is an integer. It schedules `model.step` for each tick with the
199
+ highest priority. This implies that by default, `model.step` is the first event executed at a specific tick.
200
+ In addition, discrete event scheduling, using integer as the time unit is fully supported, paving the way
201
+ for hybrid ABM-DEVS simulations.
202
+
203
+ """
204
+
205
+ def __init__(self):
206
+ super().__init__(int, 0)
207
+
208
+ def setup(self, model):
209
+ super().setup(model)
210
+ self.schedule_event_now(self.model.step, priority=Priority.HIGH)
211
+
212
+ def check_time_unit(self, time) -> bool:
213
+ if isinstance(time, int):
214
+ return True
215
+ if isinstance(time, float):
216
+ return time.is_integer()
217
+ else:
218
+ return False
219
+
220
+ def schedule_event_next_tick(
221
+ self,
222
+ function: Callable,
223
+ priority: Priority = Priority.DEFAULT,
224
+ function_args: list[Any] | None = None,
225
+ function_kwargs: dict[str, Any] | None = None,
226
+ ) -> SimulationEvent:
227
+ """Schedule a SimulationEvent for the next tick
228
+
229
+ Args
230
+ function (Callable): the callable to execute
231
+ priority (Priority): the priority of the event
232
+ function_args (List[Any]): List of arguments to pass to the callable
233
+ function_kwargs (Dict[str, Any]): List of keyword arguments to pass to the callable
234
+
235
+ """
236
+ return self.schedule_event_relative(
237
+ function,
238
+ 1,
239
+ priority=priority,
240
+ function_args=function_args,
241
+ function_kwargs=function_kwargs,
242
+ )
243
+
244
+ def run_until(self, end_time: int) -> None:
245
+ """run the simulator up to and included the specified end time
246
+
247
+ Args:
248
+ end_time (float| int): The end_time delta. The simulator is until the specified end time
249
+
250
+ """
251
+ while True:
252
+ try:
253
+ event = self.event_list.pop_event()
254
+ except IndexError:
255
+ self.time = end_time
256
+ break
257
+
258
+ if event.time <= end_time:
259
+ self.time = event.time
260
+ if event.fn() == self.model.step:
261
+ self.schedule_event_next_tick(
262
+ self.model.step, priority=Priority.HIGH
263
+ )
264
+
265
+ event.execute()
266
+ else:
267
+ self.time = end_time
268
+ self._schedule_event(event)
269
+ break
270
+
271
+ def run_for(self, time_delta: int):
272
+ """run the simulator for the specified time delta
273
+
274
+ Args:
275
+ time_delta (float| int): The time delta. The simulator is run from the current time to the current time
276
+ plus the time delta
277
+
278
+ """
279
+ end_time = self.time + time_delta - 1
280
+ self.run_until(end_time)
281
+
282
+
283
+ class DEVSimulator(Simulator):
284
+ """A simulator where the unit of time is a float. Can be used for full-blown discrete event simulating using
285
+ event scheduling.
286
+
287
+ """
288
+
289
+ def __init__(self):
290
+ super().__init__(float, 0.0)
291
+
292
+ def check_time_unit(self, time) -> bool:
293
+ return isinstance(time, numbers.Number)