Mesa 3.0.0__py3-none-any.whl → 3.0.0a1__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 (104) hide show
  1. mesa/__init__.py +3 -3
  2. mesa/agent.py +114 -406
  3. mesa/batchrunner.py +27 -54
  4. mesa/cookiecutter-mesa/cookiecutter.json +8 -0
  5. mesa/cookiecutter-mesa/hooks/post_gen_project.py +11 -0
  6. mesa/cookiecutter-mesa/{{cookiecutter.snake}}/README.md +4 -0
  7. mesa/cookiecutter-mesa/{{cookiecutter.snake}}/app.pytemplate +27 -0
  8. mesa/cookiecutter-mesa/{{cookiecutter.snake}}/setup.pytemplate +11 -0
  9. mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/model.pytemplate +60 -0
  10. mesa/datacollection.py +29 -140
  11. mesa/experimental/__init__.py +1 -11
  12. mesa/experimental/cell_space/__init__.py +1 -16
  13. mesa/experimental/cell_space/cell.py +23 -93
  14. mesa/experimental/cell_space/cell_agent.py +21 -117
  15. mesa/experimental/cell_space/cell_collection.py +17 -54
  16. mesa/experimental/cell_space/discrete_space.py +8 -92
  17. mesa/experimental/cell_space/grid.py +8 -32
  18. mesa/experimental/cell_space/network.py +7 -12
  19. mesa/experimental/devs/__init__.py +0 -2
  20. mesa/experimental/devs/eventlist.py +14 -52
  21. mesa/experimental/devs/examples/epstein_civil_violence.py +39 -71
  22. mesa/experimental/devs/examples/wolf_sheep.py +45 -45
  23. mesa/experimental/devs/simulator.py +15 -55
  24. mesa/main.py +63 -0
  25. mesa/model.py +83 -211
  26. mesa/space.py +149 -215
  27. mesa/time.py +77 -62
  28. mesa/{experimental → visualization}/UserParam.py +6 -17
  29. mesa/visualization/__init__.py +2 -25
  30. mesa/{experimental → visualization}/components/altair.py +0 -10
  31. mesa/visualization/components/matplotlib.py +134 -0
  32. mesa/visualization/solara_viz.py +266 -267
  33. {mesa-3.0.0.dist-info → mesa-3.0.0a1.dist-info}/METADATA +13 -65
  34. mesa-3.0.0a1.dist-info/RECORD +38 -0
  35. mesa-3.0.0.dist-info/licenses/NOTICE → mesa-3.0.0a1.dist-info/licenses/LICENSE +2 -2
  36. mesa/examples/README.md +0 -37
  37. mesa/examples/__init__.py +0 -21
  38. mesa/examples/advanced/epstein_civil_violence/Epstein Civil Violence.ipynb +0 -116
  39. mesa/examples/advanced/epstein_civil_violence/Readme.md +0 -34
  40. mesa/examples/advanced/epstein_civil_violence/__init__.py +0 -0
  41. mesa/examples/advanced/epstein_civil_violence/agents.py +0 -164
  42. mesa/examples/advanced/epstein_civil_violence/app.py +0 -73
  43. mesa/examples/advanced/epstein_civil_violence/model.py +0 -114
  44. mesa/examples/advanced/pd_grid/Readme.md +0 -43
  45. mesa/examples/advanced/pd_grid/__init__.py +0 -0
  46. mesa/examples/advanced/pd_grid/agents.py +0 -50
  47. mesa/examples/advanced/pd_grid/analysis.ipynb +0 -228
  48. mesa/examples/advanced/pd_grid/app.py +0 -54
  49. mesa/examples/advanced/pd_grid/model.py +0 -71
  50. mesa/examples/advanced/sugarscape_g1mt/Readme.md +0 -64
  51. mesa/examples/advanced/sugarscape_g1mt/__init__.py +0 -0
  52. mesa/examples/advanced/sugarscape_g1mt/agents.py +0 -344
  53. mesa/examples/advanced/sugarscape_g1mt/app.py +0 -62
  54. mesa/examples/advanced/sugarscape_g1mt/model.py +0 -180
  55. mesa/examples/advanced/sugarscape_g1mt/sugar-map.txt +0 -50
  56. mesa/examples/advanced/sugarscape_g1mt/tests.py +0 -69
  57. mesa/examples/advanced/wolf_sheep/Readme.md +0 -57
  58. mesa/examples/advanced/wolf_sheep/__init__.py +0 -0
  59. mesa/examples/advanced/wolf_sheep/agents.py +0 -102
  60. mesa/examples/advanced/wolf_sheep/app.py +0 -84
  61. mesa/examples/advanced/wolf_sheep/model.py +0 -137
  62. mesa/examples/basic/__init__.py +0 -0
  63. mesa/examples/basic/boid_flockers/Readme.md +0 -22
  64. mesa/examples/basic/boid_flockers/__init__.py +0 -0
  65. mesa/examples/basic/boid_flockers/agents.py +0 -71
  66. mesa/examples/basic/boid_flockers/app.py +0 -58
  67. mesa/examples/basic/boid_flockers/model.py +0 -69
  68. mesa/examples/basic/boltzmann_wealth_model/Readme.md +0 -56
  69. mesa/examples/basic/boltzmann_wealth_model/__init__.py +0 -0
  70. mesa/examples/basic/boltzmann_wealth_model/agents.py +0 -31
  71. mesa/examples/basic/boltzmann_wealth_model/app.py +0 -74
  72. mesa/examples/basic/boltzmann_wealth_model/model.py +0 -43
  73. mesa/examples/basic/boltzmann_wealth_model/st_app.py +0 -115
  74. mesa/examples/basic/conways_game_of_life/Readme.md +0 -39
  75. mesa/examples/basic/conways_game_of_life/__init__.py +0 -0
  76. mesa/examples/basic/conways_game_of_life/agents.py +0 -47
  77. mesa/examples/basic/conways_game_of_life/app.py +0 -51
  78. mesa/examples/basic/conways_game_of_life/model.py +0 -31
  79. mesa/examples/basic/conways_game_of_life/st_app.py +0 -72
  80. mesa/examples/basic/schelling/Readme.md +0 -40
  81. mesa/examples/basic/schelling/__init__.py +0 -0
  82. mesa/examples/basic/schelling/agents.py +0 -26
  83. mesa/examples/basic/schelling/analysis.ipynb +0 -205
  84. mesa/examples/basic/schelling/app.py +0 -42
  85. mesa/examples/basic/schelling/model.py +0 -59
  86. mesa/examples/basic/virus_on_network/Readme.md +0 -61
  87. mesa/examples/basic/virus_on_network/__init__.py +0 -0
  88. mesa/examples/basic/virus_on_network/agents.py +0 -69
  89. mesa/examples/basic/virus_on_network/app.py +0 -114
  90. mesa/examples/basic/virus_on_network/model.py +0 -96
  91. mesa/experimental/cell_space/voronoi.py +0 -257
  92. mesa/experimental/components/matplotlib.py +0 -242
  93. mesa/experimental/solara_viz.py +0 -453
  94. mesa/visualization/components/__init__.py +0 -83
  95. mesa/visualization/components/altair_components.py +0 -188
  96. mesa/visualization/components/matplotlib_components.py +0 -175
  97. mesa/visualization/mpl_space_drawing.py +0 -593
  98. mesa/visualization/user_param.py +0 -69
  99. mesa/visualization/utils.py +0 -9
  100. mesa-3.0.0.dist-info/RECORD +0 -95
  101. mesa-3.0.0.dist-info/licenses/LICENSE +0 -202
  102. /mesa/{examples/advanced → cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}}/__init__.py +0 -0
  103. {mesa-3.0.0.dist-info → mesa-3.0.0a1.dist-info}/WHEEL +0 -0
  104. {mesa-3.0.0.dist-info → mesa-3.0.0a1.dist-info}/entry_points.txt +0 -0
mesa/agent.py CHANGED
@@ -1,6 +1,7 @@
1
- """Agent related classes.
1
+ """
2
+ The agent class for Mesa framework.
2
3
 
3
- Core Objects: Agent and AgentSet.
4
+ Core Objects: Agent
4
5
  """
5
6
 
6
7
  # Mypy; for the `|` operator purpose
@@ -9,19 +10,15 @@ from __future__ import annotations
9
10
 
10
11
  import contextlib
11
12
  import copy
12
- import functools
13
- import itertools
14
13
  import operator
15
14
  import warnings
16
15
  import weakref
17
16
  from collections import defaultdict
18
- from collections.abc import Callable, Hashable, Iterable, Iterator, MutableSet, Sequence
17
+ from collections.abc import Callable, Iterable, Iterator, MutableSet, Sequence
19
18
  from random import Random
20
19
 
21
20
  # mypy
22
- from typing import TYPE_CHECKING, Any, Literal, overload
23
-
24
- import numpy as np
21
+ from typing import TYPE_CHECKING, Any
25
22
 
26
23
  if TYPE_CHECKING:
27
24
  # We ensure that these are not imported during runtime to prevent cyclic
@@ -31,99 +28,90 @@ if TYPE_CHECKING:
31
28
 
32
29
 
33
30
  class Agent:
34
- """Base class for a model agent in Mesa.
31
+ """
32
+ Base class for a model agent in Mesa.
35
33
 
36
34
  Attributes:
37
- model (Model): A reference to the model instance.
38
35
  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
-
36
+ model (Model): A reference to the model instance.
37
+ self.pos: Position | None = None
44
38
  """
45
39
 
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.
40
+ def __init__(self, unique_id: int, model: Model) -> None:
41
+ """
42
+ Create a new agent.
53
43
 
54
44
  Args:
45
+ unique_id (int): A unique identifier for this agent.
55
46
  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
-
63
47
  """
64
- super().__init__(*args, **kwargs)
65
-
66
- self.model: Model = model
67
- self.unique_id: int = next(self._ids[model])
48
+ self.unique_id = unique_id
49
+ self.model = model
68
50
  self.pos: Position | None = None
69
- self.model.register_agent(self)
70
51
 
71
- def remove(self) -> None:
72
- """Remove and delete the agent from the model.
52
+ # register agent
53
+ try:
54
+ self.model.agents_[type(self)][self] = None
55
+ except AttributeError:
56
+ # model super has not been called
57
+ self.model.agents_ = defaultdict(dict)
58
+ self.model.agents_[type(self)][self] = None
59
+ self.model.agentset_experimental_warning_given = False
73
60
 
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.
61
+ warnings.warn(
62
+ "The Mesa Model class was not initialized. In the future, you need to explicitly initialize the Model by calling super().__init__() on initialization.",
63
+ FutureWarning,
64
+ stacklevel=2,
65
+ )
77
66
 
78
- """
67
+ def remove(self) -> None:
68
+ """Remove and delete the agent from the model."""
79
69
  with contextlib.suppress(KeyError):
80
- self.model.deregister_agent(self)
70
+ self.model.agents_[type(self)].pop(self)
81
71
 
82
72
  def step(self) -> None:
83
73
  """A single step of the agent."""
84
74
 
85
- def advance(self) -> None: # noqa: D102
75
+ def advance(self) -> None:
86
76
  pass
87
77
 
88
78
  @property
89
79
  def random(self) -> Random:
90
- """Return a seeded stdlib rng."""
91
80
  return self.model.random
92
81
 
93
- @property
94
- def rng(self) -> np.random.Generator:
95
- """Return a seeded np.random rng."""
96
- return self.model.rng
97
-
98
82
 
99
83
  class AgentSet(MutableSet, Sequence):
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
84
+ """
85
+ A collection class that represents an ordered set of agents within an agent-based model (ABM). This class
86
+ extends both MutableSet and Sequence, providing set-like functionality with order preservation and
103
87
  sequence operations.
104
88
 
105
89
  Attributes:
106
90
  model (Model): The ABM model instance to which this AgentSet belongs.
107
91
 
108
- Notes:
92
+ Methods:
93
+ __len__, __iter__, __contains__, select, shuffle, sort, _update, do, get, __getitem__,
94
+ add, discard, remove, __getstate__, __setstate__, random
95
+
96
+ Note:
109
97
  The AgentSet maintains weak references to agents, allowing for efficient management of agent lifecycles
110
98
  without preventing garbage collection. It is associated with a specific model instance, enabling
111
99
  interactions with the model's environment and other agents.The implementation uses a WeakKeyDictionary to store agents,
112
100
  which means that agents not referenced elsewhere in the program may be automatically removed from the AgentSet.
113
101
  """
114
102
 
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.
103
+ agentset_experimental_warning_given = False
104
+
105
+ def __init__(self, agents: Iterable[Agent], model: Model):
106
+ """
107
+ Initializes the AgentSet with a collection of agents and a reference to the model.
117
108
 
118
109
  Args:
119
110
  agents (Iterable[Agent]): An iterable of Agent objects to be included in the set.
120
- random (Random): the random number generator
111
+ model (Model): The ABM model instance to which this AgentSet belongs.
121
112
  """
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
113
+
114
+ self.model = model
127
115
  self._agents = weakref.WeakKeyDictionary({agent: None for agent in agents})
128
116
 
129
117
  def __len__(self) -> int:
@@ -141,63 +129,45 @@ class AgentSet(MutableSet, Sequence):
141
129
  def select(
142
130
  self,
143
131
  filter_func: Callable[[Agent], bool] | None = None,
144
- at_most: int | float = float("inf"),
132
+ n: int = 0,
145
133
  inplace: bool = False,
146
134
  agent_type: type[Agent] | None = None,
147
- n: int | None = None,
148
135
  ) -> AgentSet:
149
- """Select a subset of agents from the AgentSet based on a filter function and/or quantity limit.
136
+ """
137
+ Select a subset of agents from the AgentSet based on a filter function and/or quantity limit.
150
138
 
151
139
  Args:
152
140
  filter_func (Callable[[Agent], bool], optional): A function that takes an Agent and returns True if the
153
141
  agent should be included in the result. Defaults to None, meaning no filtering is applied.
154
- at_most (int | float, optional): The maximum amount of agents to select. Defaults to infinity.
155
- - If an integer, at most the first number of matching agents are selected.
156
- - If a float between 0 and 1, at most that fraction of original the agents are selected.
142
+ n (int, optional): The number of agents to select. If 0, all matching agents are selected. Defaults to 0.
157
143
  inplace (bool, optional): If True, modifies the current AgentSet; otherwise, returns a new AgentSet. Defaults to False.
158
144
  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
160
145
 
161
146
  Returns:
162
147
  AgentSet: A new AgentSet containing the selected agents, unless inplace is True, in which case the current AgentSet is updated.
163
-
164
- Notes:
165
- - at_most just return the first n or fraction of agents. To take a random sample, shuffle() beforehand.
166
- - at_most is an upper limit. When specifying other criteria, the number of agents returned can be smaller.
167
148
  """
168
- if n is not None:
169
- warnings.warn(
170
- "The parameter 'n' is deprecated. Use 'at_most' instead.",
171
- DeprecationWarning,
172
- stacklevel=2,
173
- )
174
- at_most = n
175
149
 
176
- inf = float("inf")
177
- if filter_func is None and agent_type is None and at_most == inf:
150
+ if filter_func is None and agent_type is None and n == 0:
178
151
  return self if inplace else copy.copy(self)
179
152
 
180
- # Check if at_most is of type float
181
- if at_most <= 1.0 and isinstance(at_most, float):
182
- at_most = int(len(self) * at_most) # Note that it rounds down (floor)
183
-
184
- def agent_generator(filter_func, agent_type, at_most):
153
+ def agent_generator(filter_func=None, agent_type=None, n=0):
185
154
  count = 0
186
155
  for agent in self:
187
- if count >= at_most:
188
- break
189
156
  if (not filter_func or filter_func(agent)) and (
190
157
  not agent_type or isinstance(agent, agent_type)
191
158
  ):
192
159
  yield agent
193
160
  count += 1
161
+ if 0 < n <= count:
162
+ break
194
163
 
195
- agents = agent_generator(filter_func, agent_type, at_most)
164
+ agents = agent_generator(filter_func, agent_type, n)
196
165
 
197
- return AgentSet(agents, self.random) if not inplace else self._update(agents)
166
+ return AgentSet(agents, self.model) if not inplace else self._update(agents)
198
167
 
199
168
  def shuffle(self, inplace: bool = False) -> AgentSet:
200
- """Randomly shuffle the order of agents in the AgentSet.
169
+ """
170
+ Randomly shuffle the order of agents in the AgentSet.
201
171
 
202
172
  Args:
203
173
  inplace (bool, optional): If True, shuffles the agents in the current AgentSet; otherwise, returns a new shuffled AgentSet. Defaults to False.
@@ -217,7 +187,7 @@ class AgentSet(MutableSet, Sequence):
217
187
  return self
218
188
  else:
219
189
  return AgentSet(
220
- (agent for ref in weakrefs if (agent := ref()) is not None), self.random
190
+ (agent for ref in weakrefs if (agent := ref()) is not None), self.model
221
191
  )
222
192
 
223
193
  def sort(
@@ -226,7 +196,8 @@ class AgentSet(MutableSet, Sequence):
226
196
  ascending: bool = False,
227
197
  inplace: bool = False,
228
198
  ) -> AgentSet:
229
- """Sort the agents in the AgentSet based on a specified attribute or custom function.
199
+ """
200
+ Sort the agents in the AgentSet based on a specified attribute or custom function.
230
201
 
231
202
  Args:
232
203
  key (Callable[[Agent], Any] | str): A function or attribute name based on which the agents are sorted.
@@ -242,207 +213,70 @@ class AgentSet(MutableSet, Sequence):
242
213
  sorted_agents = sorted(self._agents.keys(), key=key, reverse=not ascending)
243
214
 
244
215
  return (
245
- AgentSet(sorted_agents, self.random)
216
+ AgentSet(sorted_agents, self.model)
246
217
  if not inplace
247
218
  else self._update(sorted_agents)
248
219
  )
249
220
 
250
221
  def _update(self, agents: Iterable[Agent]):
251
222
  """Update the AgentSet with a new set of agents.
252
-
253
223
  This is a private method primarily used internally by other methods like select, shuffle, and sort.
254
224
  """
255
- self._agents = weakref.WeakKeyDictionary({agent: None for agent in agents})
256
- return self
257
-
258
- def do(self, method: str | Callable, *args, **kwargs) -> AgentSet:
259
- """Invoke a method or function on each agent in the AgentSet.
260
-
261
- Args:
262
- method (str, callable): the callable to do on each agent
263
-
264
- * in case of str, the name of the method to call on each agent.
265
- * in case of callable, the function to be called with each agent as first argument
266
-
267
- *args: Variable length argument list passed to the callable being called.
268
- **kwargs: Arbitrary keyword arguments passed to the callable being called.
269
-
270
- Returns:
271
- AgentSet | list[Any]: The results of the callable calls if return_results is True, otherwise the AgentSet itself.
272
- """
273
- try:
274
- return_results = kwargs.pop("return_results")
275
- except KeyError:
276
- return_results = False
277
- else:
278
- warnings.warn(
279
- "Using return_results is deprecated. Use AgenSet.do in case of return_results=False, and "
280
- "AgentSet.map in case of return_results=True",
281
- stacklevel=2,
282
- )
283
-
284
- if return_results:
285
- return self.map(method, *args, **kwargs)
286
-
287
- # we iterate over the actual weakref keys and check if weakref is alive before calling the method
288
- if isinstance(method, str):
289
- for agentref in self._agents.keyrefs():
290
- if (agent := agentref()) is not None:
291
- getattr(agent, method)(*args, **kwargs)
292
- else:
293
- for agentref in self._agents.keyrefs():
294
- if (agent := agentref()) is not None:
295
- method(agent, *args, **kwargs)
296
225
 
226
+ self._agents = weakref.WeakKeyDictionary({agent: None for agent in agents})
297
227
  return self
298
228
 
299
- def shuffle_do(self, method: str | Callable, *args, **kwargs) -> AgentSet:
300
- """Shuffle the agents in the AgentSet and then invoke a method or function on each agent.
301
-
302
- It's a fast, optimized version of calling shuffle() followed by do().
229
+ def do(
230
+ self, method_name: str, *args, return_results: bool = False, **kwargs
231
+ ) -> AgentSet | list[Any]:
303
232
  """
304
- weakrefs = list(self._agents.keyrefs())
305
- self.random.shuffle(weakrefs)
306
-
307
- if isinstance(method, str):
308
- for ref in weakrefs:
309
- if (agent := ref()) is not None:
310
- getattr(agent, method)(*args, **kwargs)
311
- else:
312
- for ref in weakrefs:
313
- if (agent := ref()) is not None:
314
- method(agent, *args, **kwargs)
315
-
316
- return self
317
-
318
- def map(self, method: str | Callable, *args, **kwargs) -> list[Any]:
319
- """Invoke a method or function on each agent in the AgentSet and return the results.
233
+ Invoke a method on each agent in the AgentSet.
320
234
 
321
235
  Args:
322
- method (str, callable): the callable to apply on each agent
323
-
324
- * in case of str, the name of the method to call on each agent.
325
- * in case of callable, the function to be called with each agent as first argument
326
-
327
- *args: Variable length argument list passed to the callable being called.
328
- **kwargs: Arbitrary keyword arguments passed to the callable being called.
236
+ method_name (str): The name of the method to call on each agent.
237
+ return_results (bool, optional): If True, returns the results of the method calls; otherwise, returns the AgentSet itself. Defaults to False, so you can chain method calls.
238
+ *args: Variable length argument list passed to the method being called.
239
+ **kwargs: Arbitrary keyword arguments passed to the method being called.
329
240
 
330
241
  Returns:
331
- list[Any]: The results of the callable calls
242
+ AgentSet | list[Any]: The results of the method calls if return_results is True, otherwise the AgentSet itself.
332
243
  """
333
244
  # we iterate over the actual weakref keys and check if weakref is alive before calling the method
334
- if isinstance(method, str):
335
- res = [
336
- getattr(agent, method)(*args, **kwargs)
337
- for agentref in self._agents.keyrefs()
338
- if (agent := agentref()) is not None
339
- ]
340
- else:
341
- res = [
342
- method(agent, *args, **kwargs)
343
- for agentref in self._agents.keyrefs()
344
- if (agent := agentref()) is not None
345
- ]
245
+ res = [
246
+ getattr(agent, method_name)(*args, **kwargs)
247
+ for agentref in self._agents.keyrefs()
248
+ if (agent := agentref()) is not None
249
+ ]
346
250
 
347
- return res
251
+ return res if return_results else self
348
252
 
349
- def agg(self, attribute: str, func: Callable) -> Any:
350
- """Aggregate an attribute of all agents in the AgentSet using a specified function.
351
-
352
- Args:
353
- attribute (str): The name of the attribute to aggregate.
354
- func (Callable): The function to apply to the attribute values (e.g., min, max, sum, np.mean).
355
-
356
- Returns:
357
- Any: The result of applying the function to the attribute values. Often a single value.
253
+ def get(self, attr_names: str | list[str]) -> list[Any]:
358
254
  """
359
- values = self.get(attribute)
360
- return func(values)
361
-
362
- @overload
363
- def get(
364
- self,
365
- attr_names: str,
366
- handle_missing: Literal["error", "default"] = "error",
367
- default_value: Any = None,
368
- ) -> list[Any]: ...
369
-
370
- @overload
371
- def get(
372
- self,
373
- attr_names: list[str],
374
- handle_missing: Literal["error", "default"] = "error",
375
- default_value: Any = None,
376
- ) -> list[list[Any]]: ...
377
-
378
- def get(
379
- self,
380
- attr_names,
381
- handle_missing="error",
382
- default_value=None,
383
- ):
384
- """Retrieve the specified attribute(s) from each agent in the AgentSet.
255
+ Retrieve the specified attribute(s) from each agent in the AgentSet.
385
256
 
386
257
  Args:
387
258
  attr_names (str | list[str]): The name(s) of the attribute(s) to retrieve from each agent.
388
- handle_missing (str, optional): How to handle missing attributes. Can be:
389
- - 'error' (default): raises an AttributeError if attribute is missing.
390
- - 'default': returns the specified default_value.
391
- default_value (Any, optional): The default value to return if 'handle_missing' is set to 'default'
392
- and the agent does not have the attribute.
393
259
 
394
260
  Returns:
395
- list[Any]: A list with the attribute value for each agent if attr_names is a str.
396
- list[list[Any]]: A list with a lists of attribute values for each agent if attr_names is a list of str.
261
+ list[Any]: A list with the attribute value for each agent in the set if attr_names is a str
262
+ list[list[Any]]: A list with a list of attribute values for each agent in the set if attr_names is a list of str
397
263
 
398
264
  Raises:
399
- AttributeError: If 'handle_missing' is 'error' and the agent does not have the specified attribute(s).
400
- ValueError: If an unknown 'handle_missing' option is provided.
265
+ AttributeError if an agent does not have the specified attribute(s)
266
+
401
267
  """
402
- is_single_attr = isinstance(attr_names, str)
403
-
404
- if handle_missing == "error":
405
- if is_single_attr:
406
- return [getattr(agent, attr_names) for agent in self._agents]
407
- else:
408
- return [
409
- [getattr(agent, attr) for attr in attr_names]
410
- for agent in self._agents
411
- ]
412
-
413
- elif handle_missing == "default":
414
- if is_single_attr:
415
- return [
416
- getattr(agent, attr_names, default_value) for agent in self._agents
417
- ]
418
- else:
419
- return [
420
- [getattr(agent, attr, default_value) for attr in attr_names]
421
- for agent in self._agents
422
- ]
423
268
 
269
+ if isinstance(attr_names, str):
270
+ return [getattr(agent, attr_names) for agent in self._agents]
424
271
  else:
425
- raise ValueError(
426
- f"Unknown handle_missing option: {handle_missing}, "
427
- "should be one of 'error' or 'default'"
428
- )
429
-
430
- def set(self, attr_name: str, value: Any) -> AgentSet:
431
- """Set a specified attribute to a given value for all agents in the AgentSet.
432
-
433
- Args:
434
- attr_name (str): The name of the attribute to set.
435
- value (Any): The value to set the attribute to.
436
-
437
- Returns:
438
- AgentSet: The AgentSet instance itself, after setting the attribute.
439
- """
440
- for agent in self:
441
- setattr(agent, attr_name, value)
442
- return self
272
+ return [
273
+ [getattr(agent, attr_name) for attr_name in attr_names]
274
+ for agent in self._agents
275
+ ]
443
276
 
444
277
  def __getitem__(self, item: int | slice) -> Agent:
445
- """Retrieve an agent or a slice of agents from the AgentSet.
278
+ """
279
+ Retrieve an agent or a slice of agents from the AgentSet.
446
280
 
447
281
  Args:
448
282
  item (int | slice): The index or slice for selecting agents.
@@ -453,7 +287,8 @@ class AgentSet(MutableSet, Sequence):
453
287
  return list(self._agents.keys())[item]
454
288
 
455
289
  def add(self, agent: Agent):
456
- """Add an agent to the AgentSet.
290
+ """
291
+ Add an agent to the AgentSet.
457
292
 
458
293
  Args:
459
294
  agent (Agent): The agent to add to the set.
@@ -464,7 +299,8 @@ class AgentSet(MutableSet, Sequence):
464
299
  self._agents[agent] = None
465
300
 
466
301
  def discard(self, agent: Agent):
467
- """Remove an agent from the AgentSet if it exists.
302
+ """
303
+ Remove an agent from the AgentSet if it exists.
468
304
 
469
305
  This method does not raise an error if the agent is not present.
470
306
 
@@ -478,7 +314,8 @@ class AgentSet(MutableSet, Sequence):
478
314
  del self._agents[agent]
479
315
 
480
316
  def remove(self, agent: Agent):
481
- """Remove an agent from the AgentSet.
317
+ """
318
+ Remove an agent from the AgentSet.
482
319
 
483
320
  This method raises an error if the agent is not present.
484
321
 
@@ -491,164 +328,35 @@ class AgentSet(MutableSet, Sequence):
491
328
  del self._agents[agent]
492
329
 
493
330
  def __getstate__(self):
494
- """Retrieve the state of the AgentSet for serialization.
331
+ """
332
+ Retrieve the state of the AgentSet for serialization.
495
333
 
496
334
  Returns:
497
335
  dict: A dictionary representing the state of the AgentSet.
498
336
  """
499
- return {"agents": list(self._agents.keys()), "random": self.random}
337
+ return {"agents": list(self._agents.keys()), "model": self.model}
500
338
 
501
339
  def __setstate__(self, state):
502
- """Set the state of the AgentSet during deserialization.
340
+ """
341
+ Set the state of the AgentSet during deserialization.
503
342
 
504
343
  Args:
505
344
  state (dict): A dictionary representing the state to restore.
506
345
  """
507
- self.random = state["random"]
346
+ self.model = state["model"]
508
347
  self._update(state["agents"])
509
348
 
510
- def groupby(self, by: Callable | str, result_type: str = "agentset") -> GroupBy:
511
- """Group agents by the specified attribute or return from the callable.
512
-
513
- Args:
514
- by (Callable, str): used to determine what to group agents by
515
-
516
- * if ``by`` is a callable, it will be called for each agent and the return is used
517
- for grouping
518
- * if ``by`` is a str, it should refer to an attribute on the agent and the value
519
- of this attribute will be used for grouping
520
- result_type (str, optional): The datatype for the resulting groups {"agentset", "list"}
521
-
522
- Returns:
523
- GroupBy
524
-
525
-
526
- Notes:
527
- There might be performance benefits to using `result_type='list'` if you don't need the advanced functionality
528
- of an AgentSet.
529
-
530
- """
531
- groups = defaultdict(list)
532
-
533
- if isinstance(by, Callable):
534
- for agent in self:
535
- groups[by(agent)].append(agent)
536
- else:
537
- for agent in self:
538
- groups[getattr(agent, by)].append(agent)
539
-
540
- if result_type == "agentset":
541
- return GroupBy(
542
- {k: AgentSet(v, random=self.random) for k, v in groups.items()}
543
- )
544
- else:
545
- return GroupBy(groups)
546
-
547
- # consider adding for performance reasons
548
- # for Sequence: __reversed__, index, and count
549
- # for MutableSet clear, pop, remove, __ior__, __iand__, __ixor__, and __isub__
550
-
551
-
552
- class GroupBy:
553
- """Helper class for AgentSet.groupby.
554
-
555
- Attributes:
556
- groups (dict): A dictionary with the group_name as key and group as values
557
-
558
- """
559
-
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
- """
567
- self.groups: dict[Any, list | AgentSet] = groups
568
-
569
- def map(self, method: Callable | str, *args, **kwargs) -> dict[Any, Any]:
570
- """Apply the specified callable to each group and return the results.
571
-
572
- Args:
573
- method (Callable, str): The callable to apply to each group,
574
-
575
- * if ``method`` is a callable, it will be called it will be called with the group as first argument
576
- * if ``method`` is a str, it should refer to a method on the group
577
-
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
581
-
582
- Returns:
583
- dict with group_name as key and the return of the method as value
584
-
585
- Notes:
586
- this method is useful for methods or functions that do return something. It
587
- will break method chaining. For that, use ``do`` instead.
588
-
589
- """
590
- if isinstance(method, str):
591
- return {
592
- k: getattr(v, method)(*args, **kwargs) for k, v in self.groups.items()
593
- }
594
- else:
595
- return {k: method(v, *args, **kwargs) for k, v in self.groups.items()}
596
-
597
- def do(self, method: Callable | str, *args, **kwargs) -> GroupBy:
598
- """Apply the specified callable to each group.
599
-
600
- Args:
601
- method (Callable, str): The callable to apply to each group,
602
-
603
- * if ``method`` is a callable, it will be called it will be called with the group as first argument
604
- * if ``method`` is a str, it should refer to a method on the group
605
-
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
609
-
610
- Returns:
611
- the original GroupBy instance
612
-
613
- Notes:
614
- this method is useful for methods or functions that don't return anything and/or
615
- if you want to chain multiple do calls
616
-
617
- """
618
- if isinstance(method, str):
619
- for v in self.groups.values():
620
- getattr(v, method)(*args, **kwargs)
621
- else:
622
- for v in self.groups.values():
623
- method(v, *args, **kwargs)
624
-
625
- return self
626
-
627
- def count(self) -> dict[Any, int]:
628
- """Return the count of agents in each group.
629
-
630
- Returns:
631
- dict: A dictionary mapping group names to the number of agents in each group.
349
+ @property
350
+ def random(self) -> Random:
632
351
  """
633
- return {k: len(v) for k, v in self.groups.items()}
634
-
635
- def agg(self, attr_name: str, func: Callable) -> dict[Hashable, Any]:
636
- """Aggregate the values of a specific attribute across each group using the provided function.
637
-
638
- Args:
639
- attr_name (str): The name of the attribute to aggregate.
640
- func (Callable): The function to apply (e.g., sum, min, max, mean).
352
+ Provide access to the model's random number generator.
641
353
 
642
354
  Returns:
643
- dict[Hashable, Any]: A dictionary mapping group names to the result of applying the aggregation function.
355
+ Random: The random number generator associated with the model.
644
356
  """
645
- return {
646
- group_name: func([getattr(agent, attr_name) for agent in group])
647
- for group_name, group in self.groups.items()
648
- }
357
+ return self.model.random
649
358
 
650
- def __iter__(self): # noqa: D105
651
- return iter(self.groups.items())
652
359
 
653
- def __len__(self): # noqa: D105
654
- return len(self.groups)
360
+ # consider adding for performance reasons
361
+ # for Sequence: __reversed__, index, and count
362
+ # for MutableSet clear, pop, remove, __ior__, __iand__, __ixor__, and __isub__