Mesa 2.4.0__py3-none-any.whl → 3.0.0__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.

Files changed (110) hide show
  1. mesa/__init__.py +3 -5
  2. mesa/agent.py +105 -92
  3. mesa/batchrunner.py +55 -31
  4. mesa/datacollection.py +10 -14
  5. mesa/examples/README.md +37 -0
  6. mesa/examples/__init__.py +21 -0
  7. mesa/examples/advanced/epstein_civil_violence/Epstein Civil Violence.ipynb +116 -0
  8. mesa/examples/advanced/epstein_civil_violence/Readme.md +34 -0
  9. mesa/examples/advanced/epstein_civil_violence/__init__.py +0 -0
  10. mesa/examples/advanced/epstein_civil_violence/agents.py +164 -0
  11. mesa/examples/advanced/epstein_civil_violence/app.py +73 -0
  12. mesa/examples/advanced/epstein_civil_violence/model.py +114 -0
  13. mesa/examples/advanced/pd_grid/Readme.md +43 -0
  14. mesa/examples/advanced/pd_grid/__init__.py +0 -0
  15. mesa/examples/advanced/pd_grid/agents.py +50 -0
  16. mesa/examples/advanced/pd_grid/analysis.ipynb +228 -0
  17. mesa/examples/advanced/pd_grid/app.py +54 -0
  18. mesa/examples/advanced/pd_grid/model.py +71 -0
  19. mesa/examples/advanced/sugarscape_g1mt/Readme.md +64 -0
  20. mesa/examples/advanced/sugarscape_g1mt/__init__.py +0 -0
  21. mesa/examples/advanced/sugarscape_g1mt/agents.py +344 -0
  22. mesa/examples/advanced/sugarscape_g1mt/app.py +62 -0
  23. mesa/examples/advanced/sugarscape_g1mt/model.py +180 -0
  24. mesa/examples/advanced/sugarscape_g1mt/sugar-map.txt +50 -0
  25. mesa/examples/advanced/sugarscape_g1mt/tests.py +69 -0
  26. mesa/examples/advanced/wolf_sheep/Readme.md +57 -0
  27. mesa/examples/advanced/wolf_sheep/__init__.py +0 -0
  28. mesa/examples/advanced/wolf_sheep/agents.py +102 -0
  29. mesa/examples/advanced/wolf_sheep/app.py +84 -0
  30. mesa/examples/advanced/wolf_sheep/model.py +137 -0
  31. mesa/examples/basic/__init__.py +0 -0
  32. mesa/examples/basic/boid_flockers/Readme.md +22 -0
  33. mesa/examples/basic/boid_flockers/__init__.py +0 -0
  34. mesa/examples/basic/boid_flockers/agents.py +71 -0
  35. mesa/examples/basic/boid_flockers/app.py +58 -0
  36. mesa/examples/basic/boid_flockers/model.py +69 -0
  37. mesa/examples/basic/boltzmann_wealth_model/Readme.md +56 -0
  38. mesa/examples/basic/boltzmann_wealth_model/__init__.py +0 -0
  39. mesa/examples/basic/boltzmann_wealth_model/agents.py +31 -0
  40. mesa/examples/basic/boltzmann_wealth_model/app.py +74 -0
  41. mesa/examples/basic/boltzmann_wealth_model/model.py +43 -0
  42. mesa/examples/basic/boltzmann_wealth_model/st_app.py +115 -0
  43. mesa/examples/basic/conways_game_of_life/Readme.md +39 -0
  44. mesa/examples/basic/conways_game_of_life/__init__.py +0 -0
  45. mesa/examples/basic/conways_game_of_life/agents.py +47 -0
  46. mesa/examples/basic/conways_game_of_life/app.py +51 -0
  47. mesa/examples/basic/conways_game_of_life/model.py +31 -0
  48. mesa/examples/basic/conways_game_of_life/st_app.py +72 -0
  49. mesa/examples/basic/schelling/Readme.md +40 -0
  50. mesa/examples/basic/schelling/__init__.py +0 -0
  51. mesa/examples/basic/schelling/agents.py +26 -0
  52. mesa/examples/basic/schelling/analysis.ipynb +205 -0
  53. mesa/examples/basic/schelling/app.py +42 -0
  54. mesa/examples/basic/schelling/model.py +59 -0
  55. mesa/examples/basic/virus_on_network/Readme.md +61 -0
  56. mesa/examples/basic/virus_on_network/__init__.py +0 -0
  57. mesa/examples/basic/virus_on_network/agents.py +69 -0
  58. mesa/examples/basic/virus_on_network/app.py +114 -0
  59. mesa/examples/basic/virus_on_network/model.py +96 -0
  60. mesa/experimental/UserParam.py +18 -7
  61. mesa/experimental/__init__.py +10 -2
  62. mesa/experimental/cell_space/__init__.py +16 -1
  63. mesa/experimental/cell_space/cell.py +93 -23
  64. mesa/experimental/cell_space/cell_agent.py +117 -21
  65. mesa/experimental/cell_space/cell_collection.py +56 -19
  66. mesa/experimental/cell_space/discrete_space.py +92 -8
  67. mesa/experimental/cell_space/grid.py +33 -9
  68. mesa/experimental/cell_space/network.py +15 -10
  69. mesa/experimental/cell_space/voronoi.py +257 -0
  70. mesa/experimental/components/altair.py +11 -2
  71. mesa/experimental/components/matplotlib.py +132 -26
  72. mesa/experimental/devs/__init__.py +2 -0
  73. mesa/experimental/devs/eventlist.py +54 -15
  74. mesa/experimental/devs/examples/epstein_civil_violence.py +69 -38
  75. mesa/experimental/devs/examples/wolf_sheep.py +42 -43
  76. mesa/experimental/devs/simulator.py +57 -16
  77. mesa/experimental/{jupyter_viz.py → solara_viz.py} +151 -99
  78. mesa/model.py +136 -78
  79. mesa/space.py +208 -148
  80. mesa/time.py +63 -80
  81. mesa/visualization/__init__.py +25 -6
  82. mesa/visualization/components/__init__.py +83 -0
  83. mesa/visualization/components/altair_components.py +188 -0
  84. mesa/visualization/components/matplotlib_components.py +175 -0
  85. mesa/visualization/mpl_space_drawing.py +593 -0
  86. mesa/visualization/solara_viz.py +458 -0
  87. mesa/visualization/user_param.py +69 -0
  88. mesa/visualization/utils.py +9 -0
  89. {mesa-2.4.0.dist-info → mesa-3.0.0.dist-info}/METADATA +62 -17
  90. mesa-3.0.0.dist-info/RECORD +95 -0
  91. mesa-3.0.0.dist-info/licenses/LICENSE +202 -0
  92. mesa-2.4.0.dist-info/licenses/LICENSE → mesa-3.0.0.dist-info/licenses/NOTICE +2 -2
  93. mesa/cookiecutter-mesa/cookiecutter.json +0 -8
  94. mesa/cookiecutter-mesa/hooks/post_gen_project.py +0 -11
  95. mesa/cookiecutter-mesa/{{cookiecutter.snake}}/README.md +0 -4
  96. mesa/cookiecutter-mesa/{{cookiecutter.snake}}/run.pytemplate +0 -3
  97. mesa/cookiecutter-mesa/{{cookiecutter.snake}}/setup.pytemplate +0 -11
  98. mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/model.pytemplate +0 -60
  99. mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/server.pytemplate +0 -36
  100. mesa/flat/__init__.py +0 -6
  101. mesa/flat/visualization.py +0 -5
  102. mesa/main.py +0 -63
  103. mesa/visualization/ModularVisualization.py +0 -1
  104. mesa/visualization/TextVisualization.py +0 -1
  105. mesa/visualization/UserParam.py +0 -1
  106. mesa/visualization/modules.py +0 -1
  107. mesa-2.4.0.dist-info/RECORD +0 -45
  108. /mesa/{cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}} → examples/advanced}/__init__.py +0 -0
  109. {mesa-2.4.0.dist-info → mesa-3.0.0.dist-info}/WHEEL +0 -0
  110. {mesa-2.4.0.dist-info → mesa-3.0.0.dist-info}/entry_points.txt +0 -0
mesa/__init__.py CHANGED
@@ -1,14 +1,13 @@
1
- """
2
- Mesa Agent-Based Modeling Framework
1
+ """Mesa Agent-Based Modeling Framework.
3
2
 
4
3
  Core Objects: Model, and Agent.
5
4
  """
6
5
 
7
6
  import datetime
8
7
 
8
+ import mesa.experimental as experimental
9
9
  import mesa.space as space
10
10
  import mesa.time as time
11
- import mesa.visualization as visualization
12
11
  from mesa.agent import Agent
13
12
  from mesa.batchrunner import batch_run
14
13
  from mesa.datacollection import DataCollector
@@ -19,14 +18,13 @@ __all__ = [
19
18
  "Agent",
20
19
  "time",
21
20
  "space",
22
- "visualization",
23
21
  "DataCollector",
24
22
  "batch_run",
25
23
  "experimental",
26
24
  ]
27
25
 
28
26
  __title__ = "mesa"
29
- __version__ = "2.4.0"
27
+ __version__ = "3.0.0"
30
28
  __license__ = "Apache 2.0"
31
29
  _this_year = datetime.datetime.now(tz=datetime.timezone.utc).date().year
32
30
  __copyright__ = f"Copyright {_this_year} Project Mesa Team"
mesa/agent.py CHANGED
@@ -1,7 +1,6 @@
1
- """
2
- The agent class for Mesa framework.
1
+ """Agent related classes.
3
2
 
4
- Core Objects: Agent
3
+ Core Objects: Agent and AgentSet.
5
4
  """
6
5
 
7
6
  # Mypy; for the `|` operator purpose
@@ -10,15 +9,19 @@ from __future__ import annotations
10
9
 
11
10
  import contextlib
12
11
  import copy
12
+ import functools
13
+ import itertools
13
14
  import operator
14
15
  import warnings
15
16
  import weakref
16
17
  from collections import defaultdict
17
- from collections.abc import Hashable, Iterable, Iterator, MutableSet, Sequence
18
+ from collections.abc import Callable, Hashable, Iterable, Iterator, MutableSet, Sequence
18
19
  from random import Random
19
20
 
20
21
  # mypy
21
- from typing import TYPE_CHECKING, Any, Callable, Literal, overload
22
+ from typing import TYPE_CHECKING, Any, Literal, overload
23
+
24
+ import numpy as np
22
25
 
23
26
  if TYPE_CHECKING:
24
27
  # We ensure that these are not imported during runtime to prevent cyclic
@@ -28,77 +31,99 @@ if TYPE_CHECKING:
28
31
 
29
32
 
30
33
  class Agent:
31
- """
32
- Base class for a model agent in Mesa.
34
+ """Base class for a model agent in Mesa.
33
35
 
34
36
  Attributes:
35
- unique_id (int): A unique identifier for this agent.
36
37
  model (Model): A reference to the model instance.
37
- self.pos: Position | None = None
38
+ unique_id (int): A unique identifier for this agent.
39
+ pos (Position): A reference to the position where this agent is located.
40
+
41
+ Notes:
42
+ unique_id is unique relative to a model instance and starts from 1
43
+
38
44
  """
39
45
 
40
- def __init__(self, unique_id: int, model: Model) -> None:
41
- """
42
- Create a new agent.
46
+ # this is a class level attribute
47
+ # it is a dictionary, indexed by model instance
48
+ # so, unique_id is unique relative to a model, and counting starts from 1
49
+ _ids = defaultdict(functools.partial(itertools.count, 1))
50
+
51
+ def __init__(self, model: Model, *args, **kwargs) -> None:
52
+ """Create a new agent.
43
53
 
44
54
  Args:
45
- unique_id (int): A unique identifier for this agent.
46
55
  model (Model): The model instance in which the agent exists.
56
+ args: passed on to super
57
+ kwargs: passed on to super
58
+
59
+ Notes:
60
+ to make proper use of python's super, in each class remove the arguments and
61
+ keyword arguments you need and pass on the rest to super
62
+
47
63
  """
48
- self.unique_id = unique_id
49
- self.model = model
50
- self.pos: Position | None = None
64
+ super().__init__(*args, **kwargs)
51
65
 
66
+ self.model: Model = model
67
+ self.unique_id: int = next(self._ids[model])
68
+ self.pos: Position | None = None
52
69
  self.model.register_agent(self)
53
70
 
54
71
  def remove(self) -> None:
55
- """Remove and delete the agent from the model."""
72
+ """Remove and delete the agent from the model.
73
+
74
+ Notes:
75
+ If you need to do additional cleanup when removing an agent by for example removing
76
+ it from a space, consider extending this method in your own agent class.
77
+
78
+ """
56
79
  with contextlib.suppress(KeyError):
57
80
  self.model.deregister_agent(self)
58
81
 
59
82
  def step(self) -> None:
60
83
  """A single step of the agent."""
61
84
 
62
- def advance(self) -> None:
85
+ def advance(self) -> None: # noqa: D102
63
86
  pass
64
87
 
65
88
  @property
66
89
  def random(self) -> Random:
90
+ """Return a seeded stdlib rng."""
67
91
  return self.model.random
68
92
 
93
+ @property
94
+ def rng(self) -> np.random.Generator:
95
+ """Return a seeded np.random rng."""
96
+ return self.model.rng
97
+
69
98
 
70
99
  class AgentSet(MutableSet, Sequence):
71
- """
72
- A collection class that represents an ordered set of agents within an agent-based model (ABM). This class
73
- extends both MutableSet and Sequence, providing set-like functionality with order preservation and
100
+ """A collection class that represents an ordered set of agents within an agent-based model (ABM).
101
+
102
+ This class extends both MutableSet and Sequence, providing set-like functionality with order preservation and
74
103
  sequence operations.
75
104
 
76
105
  Attributes:
77
106
  model (Model): The ABM model instance to which this AgentSet belongs.
78
107
 
79
- Methods:
80
- __len__, __iter__, __contains__, select, shuffle, sort, _update, do, get, __getitem__,
81
- add, discard, remove, __getstate__, __setstate__, random
82
-
83
- Note:
108
+ Notes:
84
109
  The AgentSet maintains weak references to agents, allowing for efficient management of agent lifecycles
85
110
  without preventing garbage collection. It is associated with a specific model instance, enabling
86
111
  interactions with the model's environment and other agents.The implementation uses a WeakKeyDictionary to store agents,
87
112
  which means that agents not referenced elsewhere in the program may be automatically removed from the AgentSet.
88
113
  """
89
114
 
90
- agentset_experimental_warning_given = False
91
-
92
- def __init__(self, agents: Iterable[Agent], model: Model):
93
- """
94
- Initializes the AgentSet with a collection of agents and a reference to the model.
115
+ def __init__(self, agents: Iterable[Agent], random: Random | None = None):
116
+ """Initializes the AgentSet with a collection of agents and a reference to the model.
95
117
 
96
118
  Args:
97
119
  agents (Iterable[Agent]): An iterable of Agent objects to be included in the set.
98
- model (Model): The ABM model instance to which this AgentSet belongs.
120
+ random (Random): the random number generator
99
121
  """
100
-
101
- self.model = model
122
+ if random is None:
123
+ random = (
124
+ Random()
125
+ ) # FIXME see issue 1981, how to get the central rng from model
126
+ self.random = random
102
127
  self._agents = weakref.WeakKeyDictionary({agent: None for agent in agents})
103
128
 
104
129
  def __len__(self) -> int:
@@ -121,8 +146,7 @@ class AgentSet(MutableSet, Sequence):
121
146
  agent_type: type[Agent] | None = None,
122
147
  n: int | None = None,
123
148
  ) -> AgentSet:
124
- """
125
- Select a subset of agents from the AgentSet based on a filter function and/or quantity limit.
149
+ """Select a subset of agents from the AgentSet based on a filter function and/or quantity limit.
126
150
 
127
151
  Args:
128
152
  filter_func (Callable[[Agent], bool], optional): A function that takes an Agent and returns True if the
@@ -132,6 +156,7 @@ class AgentSet(MutableSet, Sequence):
132
156
  - If a float between 0 and 1, at most that fraction of original the agents are selected.
133
157
  inplace (bool, optional): If True, modifies the current AgentSet; otherwise, returns a new AgentSet. Defaults to False.
134
158
  agent_type (type[Agent], optional): The class type of the agents to select. Defaults to None, meaning no type filtering is applied.
159
+ n (int): deprecated, use at_most instead
135
160
 
136
161
  Returns:
137
162
  AgentSet: A new AgentSet containing the selected agents, unless inplace is True, in which case the current AgentSet is updated.
@@ -169,11 +194,10 @@ class AgentSet(MutableSet, Sequence):
169
194
 
170
195
  agents = agent_generator(filter_func, agent_type, at_most)
171
196
 
172
- return AgentSet(agents, self.model) if not inplace else self._update(agents)
197
+ return AgentSet(agents, self.random) if not inplace else self._update(agents)
173
198
 
174
199
  def shuffle(self, inplace: bool = False) -> AgentSet:
175
- """
176
- Randomly shuffle the order of agents in the AgentSet.
200
+ """Randomly shuffle the order of agents in the AgentSet.
177
201
 
178
202
  Args:
179
203
  inplace (bool, optional): If True, shuffles the agents in the current AgentSet; otherwise, returns a new shuffled AgentSet. Defaults to False.
@@ -193,7 +217,7 @@ class AgentSet(MutableSet, Sequence):
193
217
  return self
194
218
  else:
195
219
  return AgentSet(
196
- (agent for ref in weakrefs if (agent := ref()) is not None), self.model
220
+ (agent for ref in weakrefs if (agent := ref()) is not None), self.random
197
221
  )
198
222
 
199
223
  def sort(
@@ -202,8 +226,7 @@ class AgentSet(MutableSet, Sequence):
202
226
  ascending: bool = False,
203
227
  inplace: bool = False,
204
228
  ) -> AgentSet:
205
- """
206
- Sort the agents in the AgentSet based on a specified attribute or custom function.
229
+ """Sort the agents in the AgentSet based on a specified attribute or custom function.
207
230
 
208
231
  Args:
209
232
  key (Callable[[Agent], Any] | str): A function or attribute name based on which the agents are sorted.
@@ -219,22 +242,21 @@ class AgentSet(MutableSet, Sequence):
219
242
  sorted_agents = sorted(self._agents.keys(), key=key, reverse=not ascending)
220
243
 
221
244
  return (
222
- AgentSet(sorted_agents, self.model)
245
+ AgentSet(sorted_agents, self.random)
223
246
  if not inplace
224
247
  else self._update(sorted_agents)
225
248
  )
226
249
 
227
250
  def _update(self, agents: Iterable[Agent]):
228
251
  """Update the AgentSet with a new set of agents.
252
+
229
253
  This is a private method primarily used internally by other methods like select, shuffle, and sort.
230
254
  """
231
-
232
255
  self._agents = weakref.WeakKeyDictionary({agent: None for agent in agents})
233
256
  return self
234
257
 
235
258
  def do(self, method: str | Callable, *args, **kwargs) -> AgentSet:
236
- """
237
- Invoke a method or function on each agent in the AgentSet.
259
+ """Invoke a method or function on each agent in the AgentSet.
238
260
 
239
261
  Args:
240
262
  method (str, callable): the callable to do on each agent
@@ -279,21 +301,22 @@ class AgentSet(MutableSet, Sequence):
279
301
 
280
302
  It's a fast, optimized version of calling shuffle() followed by do().
281
303
  """
282
- agents = list(self._agents.keys())
283
- self.random.shuffle(agents)
304
+ weakrefs = list(self._agents.keyrefs())
305
+ self.random.shuffle(weakrefs)
284
306
 
285
307
  if isinstance(method, str):
286
- for agent in agents:
287
- getattr(agent, method)(*args, **kwargs)
308
+ for ref in weakrefs:
309
+ if (agent := ref()) is not None:
310
+ getattr(agent, method)(*args, **kwargs)
288
311
  else:
289
- for agent in agents:
290
- method(agent, *args, **kwargs)
312
+ for ref in weakrefs:
313
+ if (agent := ref()) is not None:
314
+ method(agent, *args, **kwargs)
291
315
 
292
316
  return self
293
317
 
294
318
  def map(self, method: str | Callable, *args, **kwargs) -> list[Any]:
295
- """
296
- Invoke a method or function on each agent in the AgentSet and return the results.
319
+ """Invoke a method or function on each agent in the AgentSet and return the results.
297
320
 
298
321
  Args:
299
322
  method (str, callable): the callable to apply on each agent
@@ -324,8 +347,7 @@ class AgentSet(MutableSet, Sequence):
324
347
  return res
325
348
 
326
349
  def agg(self, attribute: str, func: Callable) -> Any:
327
- """
328
- Aggregate an attribute of all agents in the AgentSet using a specified function.
350
+ """Aggregate an attribute of all agents in the AgentSet using a specified function.
329
351
 
330
352
  Args:
331
353
  attribute (str): The name of the attribute to aggregate.
@@ -359,8 +381,7 @@ class AgentSet(MutableSet, Sequence):
359
381
  handle_missing="error",
360
382
  default_value=None,
361
383
  ):
362
- """
363
- Retrieve the specified attribute(s) from each agent in the AgentSet.
384
+ """Retrieve the specified attribute(s) from each agent in the AgentSet.
364
385
 
365
386
  Args:
366
387
  attr_names (str | list[str]): The name(s) of the attribute(s) to retrieve from each agent.
@@ -407,8 +428,7 @@ class AgentSet(MutableSet, Sequence):
407
428
  )
408
429
 
409
430
  def set(self, attr_name: str, value: Any) -> AgentSet:
410
- """
411
- Set a specified attribute to a given value for all agents in the AgentSet.
431
+ """Set a specified attribute to a given value for all agents in the AgentSet.
412
432
 
413
433
  Args:
414
434
  attr_name (str): The name of the attribute to set.
@@ -422,8 +442,7 @@ class AgentSet(MutableSet, Sequence):
422
442
  return self
423
443
 
424
444
  def __getitem__(self, item: int | slice) -> Agent:
425
- """
426
- Retrieve an agent or a slice of agents from the AgentSet.
445
+ """Retrieve an agent or a slice of agents from the AgentSet.
427
446
 
428
447
  Args:
429
448
  item (int | slice): The index or slice for selecting agents.
@@ -434,8 +453,7 @@ class AgentSet(MutableSet, Sequence):
434
453
  return list(self._agents.keys())[item]
435
454
 
436
455
  def add(self, agent: Agent):
437
- """
438
- Add an agent to the AgentSet.
456
+ """Add an agent to the AgentSet.
439
457
 
440
458
  Args:
441
459
  agent (Agent): The agent to add to the set.
@@ -446,8 +464,7 @@ class AgentSet(MutableSet, Sequence):
446
464
  self._agents[agent] = None
447
465
 
448
466
  def discard(self, agent: Agent):
449
- """
450
- Remove an agent from the AgentSet if it exists.
467
+ """Remove an agent from the AgentSet if it exists.
451
468
 
452
469
  This method does not raise an error if the agent is not present.
453
470
 
@@ -461,8 +478,7 @@ class AgentSet(MutableSet, Sequence):
461
478
  del self._agents[agent]
462
479
 
463
480
  def remove(self, agent: Agent):
464
- """
465
- Remove an agent from the AgentSet.
481
+ """Remove an agent from the AgentSet.
466
482
 
467
483
  This method raises an error if the agent is not present.
468
484
 
@@ -475,37 +491,24 @@ class AgentSet(MutableSet, Sequence):
475
491
  del self._agents[agent]
476
492
 
477
493
  def __getstate__(self):
478
- """
479
- Retrieve the state of the AgentSet for serialization.
494
+ """Retrieve the state of the AgentSet for serialization.
480
495
 
481
496
  Returns:
482
497
  dict: A dictionary representing the state of the AgentSet.
483
498
  """
484
- return {"agents": list(self._agents.keys()), "model": self.model}
499
+ return {"agents": list(self._agents.keys()), "random": self.random}
485
500
 
486
501
  def __setstate__(self, state):
487
- """
488
- Set the state of the AgentSet during deserialization.
502
+ """Set the state of the AgentSet during deserialization.
489
503
 
490
504
  Args:
491
505
  state (dict): A dictionary representing the state to restore.
492
506
  """
493
- self.model = state["model"]
507
+ self.random = state["random"]
494
508
  self._update(state["agents"])
495
509
 
496
- @property
497
- def random(self) -> Random:
498
- """
499
- Provide access to the model's random number generator.
500
-
501
- Returns:
502
- Random: The random number generator associated with the model.
503
- """
504
- return self.model.random
505
-
506
510
  def groupby(self, by: Callable | str, result_type: str = "agentset") -> GroupBy:
507
- """
508
- Group agents by the specified attribute or return from the callable
511
+ """Group agents by the specified attribute or return from the callable.
509
512
 
510
513
  Args:
511
514
  by (Callable, str): used to determine what to group agents by
@@ -515,6 +518,7 @@ class AgentSet(MutableSet, Sequence):
515
518
  * if ``by`` is a str, it should refer to an attribute on the agent and the value
516
519
  of this attribute will be used for grouping
517
520
  result_type (str, optional): The datatype for the resulting groups {"agentset", "list"}
521
+
518
522
  Returns:
519
523
  GroupBy
520
524
 
@@ -535,7 +539,7 @@ class AgentSet(MutableSet, Sequence):
535
539
 
536
540
  if result_type == "agentset":
537
541
  return GroupBy(
538
- {k: AgentSet(v, model=self.model) for k, v in groups.items()}
542
+ {k: AgentSet(v, random=self.random) for k, v in groups.items()}
539
543
  )
540
544
  else:
541
545
  return GroupBy(groups)
@@ -546,8 +550,7 @@ class AgentSet(MutableSet, Sequence):
546
550
 
547
551
 
548
552
  class GroupBy:
549
- """Helper class for AgentSet.groupby
550
-
553
+ """Helper class for AgentSet.groupby.
551
554
 
552
555
  Attributes:
553
556
  groups (dict): A dictionary with the group_name as key and group as values
@@ -555,6 +558,12 @@ class GroupBy:
555
558
  """
556
559
 
557
560
  def __init__(self, groups: dict[Any, list | AgentSet]):
561
+ """Initialize a GroupBy instance.
562
+
563
+ Args:
564
+ groups (dict): A dictionary with the group_name as key and group as values
565
+
566
+ """
558
567
  self.groups: dict[Any, list | AgentSet] = groups
559
568
 
560
569
  def map(self, method: Callable | str, *args, **kwargs) -> dict[Any, Any]:
@@ -567,6 +576,8 @@ class GroupBy:
567
576
  * if ``method`` is a str, it should refer to a method on the group
568
577
 
569
578
  Additional arguments and keyword arguments will be passed on to the callable.
579
+ args: arguments to pass to the callable
580
+ kwargs: keyword arguments to pass to the callable
570
581
 
571
582
  Returns:
572
583
  dict with group_name as key and the return of the method as value
@@ -584,7 +595,7 @@ class GroupBy:
584
595
  return {k: method(v, *args, **kwargs) for k, v in self.groups.items()}
585
596
 
586
597
  def do(self, method: Callable | str, *args, **kwargs) -> GroupBy:
587
- """Apply the specified callable to each group
598
+ """Apply the specified callable to each group.
588
599
 
589
600
  Args:
590
601
  method (Callable, str): The callable to apply to each group,
@@ -593,6 +604,8 @@ class GroupBy:
593
604
  * if ``method`` is a str, it should refer to a method on the group
594
605
 
595
606
  Additional arguments and keyword arguments will be passed on to the callable.
607
+ args: arguments to pass to the callable
608
+ kwargs: keyword arguments to pass to the callable
596
609
 
597
610
  Returns:
598
611
  the original GroupBy instance
@@ -634,8 +647,8 @@ class GroupBy:
634
647
  for group_name, group in self.groups.items()
635
648
  }
636
649
 
637
- def __iter__(self):
650
+ def __iter__(self): # noqa: D105
638
651
  return iter(self.groups.items())
639
652
 
640
- def __len__(self):
653
+ def __len__(self): # noqa: D105
641
654
  return len(self.groups)
mesa/batchrunner.py CHANGED
@@ -1,9 +1,36 @@
1
+ """batchrunner for running a factorial experiment design over a model.
2
+
3
+ To take advantage of parallel execution of experiments, `batch_run` uses
4
+ multiprocessing if ``number_processes`` is larger than 1. It is strongly advised
5
+ to only run in parallel using a normal python file (so don't try to do it in a
6
+ jupyter notebook). Moreover, best practice when using multiprocessing is to
7
+ put the code inside an ``if __name__ == '__main__':`` code black as shown below::
8
+
9
+ from mesa.batchrunner import batch_run
10
+
11
+ params = {"width": 10, "height": 10, "N": range(10, 500, 10)}
12
+
13
+ if __name__ == '__main__':
14
+ results = batch_run(
15
+ MoneyModel,
16
+ parameters=params,
17
+ iterations=5,
18
+ max_steps=100,
19
+ number_processes=None,
20
+ data_collection_period=1,
21
+ display_progress=True,
22
+ )
23
+
24
+
25
+
26
+ """
27
+
1
28
  import itertools
2
29
  import multiprocessing
3
30
  from collections.abc import Iterable, Mapping
4
31
  from functools import partial
5
32
  from multiprocessing import Pool
6
- from typing import Any, Optional, Union
33
+ from typing import Any
7
34
 
8
35
  from tqdm.auto import tqdm
9
36
 
@@ -14,9 +41,9 @@ multiprocessing.set_start_method("spawn", force=True)
14
41
 
15
42
  def batch_run(
16
43
  model_cls: type[Model],
17
- parameters: Mapping[str, Union[Any, Iterable[Any]]],
44
+ parameters: Mapping[str, Any | Iterable[Any]],
18
45
  # We still retain the Optional[int] because users may set it to None (i.e. use all CPUs)
19
- number_processes: Optional[int] = 1,
46
+ number_processes: int | None = 1,
20
47
  iterations: int = 1,
21
48
  data_collection_period: int = -1,
22
49
  max_steps: int = 1000,
@@ -24,29 +51,22 @@ def batch_run(
24
51
  ) -> list[dict[str, Any]]:
25
52
  """Batch run a mesa model with a set of parameter values.
26
53
 
27
- Parameters
28
- ----------
29
- model_cls : Type[Model]
30
- The model class to batch-run
31
- parameters : Mapping[str, Union[Any, Iterable[Any]]],
32
- Dictionary with model parameters over which to run the model. You can either pass single values or iterables.
33
- number_processes : int, optional
34
- Number of processes used, by default 1. Set this to None if you want to use all CPUs.
35
- iterations : int, optional
36
- Number of iterations for each parameter combination, by default 1
37
- data_collection_period : int, optional
38
- Number of steps after which data gets collected, by default -1 (end of episode)
39
- max_steps : int, optional
40
- Maximum number of model steps after which the model halts, by default 1000
41
- display_progress : bool, optional
42
- Display batch run process, by default True
54
+ Args:
55
+ model_cls (Type[Model]): The model class to batch-run
56
+ parameters (Mapping[str, Union[Any, Iterable[Any]]]): Dictionary with model parameters over which to run the model. You can either pass single values or iterables.
57
+ number_processes (int, optional): Number of processes used, by default 1. Set this to None if you want to use all CPUs.
58
+ iterations (int, optional): Number of iterations for each parameter combination, by default 1
59
+ data_collection_period (int, optional): Number of steps after which data gets collected, by default -1 (end of episode)
60
+ max_steps (int, optional): Maximum number of model steps after which the model halts, by default 1000
61
+ display_progress (bool, optional): Display batch run process, by default True
43
62
 
44
- Returns
45
- -------
46
- List[Dict[str, Any]]
47
- [description]
48
- """
63
+ Returns:
64
+ List[Dict[str, Any]]
49
65
 
66
+ Notes:
67
+ batch_run assumes the model has a `datacollector` attribute that has a DataCollector object initialized.
68
+
69
+ """
50
70
  runs_list = []
51
71
  run_id = 0
52
72
  for iteration in range(iterations):
@@ -79,7 +99,7 @@ def batch_run(
79
99
 
80
100
 
81
101
  def _make_model_kwargs(
82
- parameters: Mapping[str, Union[Any, Iterable[Any]]],
102
+ parameters: Mapping[str, Any | Iterable[Any]],
83
103
  ) -> list[dict[str, Any]]:
84
104
  """Create model kwargs from parameters dictionary.
85
105
 
@@ -88,7 +108,7 @@ def _make_model_kwargs(
88
108
  parameters : Mapping[str, Union[Any, Iterable[Any]]]
89
109
  Single or multiple values for each model parameter name
90
110
 
91
- Returns
111
+ Returns:
92
112
  -------
93
113
  List[Dict[str, Any]]
94
114
  A list of all kwargs combinations.
@@ -128,21 +148,21 @@ def _model_run_func(
128
148
  data_collection_period : int
129
149
  Number of steps after which data gets collected
130
150
 
131
- Returns
151
+ Returns:
132
152
  -------
133
153
  List[Dict[str, Any]]
134
154
  Return model_data, agent_data from the reporters
135
155
  """
136
156
  run_id, iteration, kwargs = run
137
157
  model = model_cls(**kwargs)
138
- while model.running and model._steps <= max_steps:
158
+ while model.running and model.steps <= max_steps:
139
159
  model.step()
140
160
 
141
161
  data = []
142
162
 
143
- steps = list(range(0, model._steps, data_collection_period))
144
- if not steps or steps[-1] != model._steps - 1:
145
- steps.append(model._steps - 1)
163
+ steps = list(range(0, model.steps, data_collection_period))
164
+ if not steps or steps[-1] != model.steps - 1:
165
+ steps.append(model.steps - 1)
146
166
 
147
167
  for step in steps:
148
168
  model_data, all_agents_data = _collect_data(model, step)
@@ -181,6 +201,10 @@ def _collect_data(
181
201
  step: int,
182
202
  ) -> tuple[dict[str, Any], list[dict[str, Any]]]:
183
203
  """Collect model and agent data from a model using mesas datacollector."""
204
+ if not hasattr(model, "datacollector"):
205
+ raise AttributeError(
206
+ "The model does not have a datacollector attribute. Please add a DataCollector to your model."
207
+ )
184
208
  dc = model.datacollector
185
209
 
186
210
  model_data = {param: values[step] for param, values in dc.model_vars.items()}