Mesa 2.3.4__py3-none-any.whl → 2.4.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.
- mesa/__init__.py +1 -1
- mesa/agent.py +317 -53
- mesa/batchrunner.py +3 -0
- mesa/datacollection.py +135 -20
- mesa/experimental/devs/examples/epstein_civil_violence.py +2 -1
- mesa/experimental/devs/examples/wolf_sheep.py +3 -2
- mesa/experimental/jupyter_viz.py +9 -8
- mesa/model.py +91 -21
- mesa/space.py +9 -3
- {mesa-2.3.4.dist-info → mesa-2.4.0.dist-info}/METADATA +5 -4
- {mesa-2.3.4.dist-info → mesa-2.4.0.dist-info}/RECORD +14 -14
- {mesa-2.3.4.dist-info → mesa-2.4.0.dist-info}/WHEEL +0 -0
- {mesa-2.3.4.dist-info → mesa-2.4.0.dist-info}/entry_points.txt +0 -0
- {mesa-2.3.4.dist-info → mesa-2.4.0.dist-info}/licenses/LICENSE +0 -0
mesa/__init__.py
CHANGED
|
@@ -26,7 +26,7 @@ __all__ = [
|
|
|
26
26
|
]
|
|
27
27
|
|
|
28
28
|
__title__ = "mesa"
|
|
29
|
-
__version__ = "2.
|
|
29
|
+
__version__ = "2.4.0"
|
|
30
30
|
__license__ = "Apache 2.0"
|
|
31
31
|
_this_year = datetime.datetime.now(tz=datetime.timezone.utc).date().year
|
|
32
32
|
__copyright__ = f"Copyright {_this_year} Project Mesa Team"
|
mesa/agent.py
CHANGED
|
@@ -14,11 +14,11 @@ import operator
|
|
|
14
14
|
import warnings
|
|
15
15
|
import weakref
|
|
16
16
|
from collections import defaultdict
|
|
17
|
-
from collections.abc import Iterable, Iterator, MutableSet, Sequence
|
|
17
|
+
from collections.abc import Hashable, Iterable, Iterator, MutableSet, Sequence
|
|
18
18
|
from random import Random
|
|
19
19
|
|
|
20
20
|
# mypy
|
|
21
|
-
from typing import TYPE_CHECKING, Any, Callable
|
|
21
|
+
from typing import TYPE_CHECKING, Any, Callable, Literal, overload
|
|
22
22
|
|
|
23
23
|
if TYPE_CHECKING:
|
|
24
24
|
# We ensure that these are not imported during runtime to prevent cyclic
|
|
@@ -49,25 +49,12 @@ class Agent:
|
|
|
49
49
|
self.model = model
|
|
50
50
|
self.pos: Position | None = None
|
|
51
51
|
|
|
52
|
-
|
|
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
|
|
60
|
-
|
|
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
|
-
)
|
|
52
|
+
self.model.register_agent(self)
|
|
66
53
|
|
|
67
54
|
def remove(self) -> None:
|
|
68
55
|
"""Remove and delete the agent from the model."""
|
|
69
56
|
with contextlib.suppress(KeyError):
|
|
70
|
-
self.model.
|
|
57
|
+
self.model.deregister_agent(self)
|
|
71
58
|
|
|
72
59
|
def step(self) -> None:
|
|
73
60
|
"""A single step of the agent."""
|
|
@@ -129,9 +116,10 @@ class AgentSet(MutableSet, Sequence):
|
|
|
129
116
|
def select(
|
|
130
117
|
self,
|
|
131
118
|
filter_func: Callable[[Agent], bool] | None = None,
|
|
132
|
-
|
|
119
|
+
at_most: int | float = float("inf"),
|
|
133
120
|
inplace: bool = False,
|
|
134
121
|
agent_type: type[Agent] | None = None,
|
|
122
|
+
n: int | None = None,
|
|
135
123
|
) -> AgentSet:
|
|
136
124
|
"""
|
|
137
125
|
Select a subset of agents from the AgentSet based on a filter function and/or quantity limit.
|
|
@@ -139,29 +127,47 @@ class AgentSet(MutableSet, Sequence):
|
|
|
139
127
|
Args:
|
|
140
128
|
filter_func (Callable[[Agent], bool], optional): A function that takes an Agent and returns True if the
|
|
141
129
|
agent should be included in the result. Defaults to None, meaning no filtering is applied.
|
|
142
|
-
|
|
130
|
+
at_most (int | float, optional): The maximum amount of agents to select. Defaults to infinity.
|
|
131
|
+
- If an integer, at most the first number of matching agents are selected.
|
|
132
|
+
- If a float between 0 and 1, at most that fraction of original the agents are selected.
|
|
143
133
|
inplace (bool, optional): If True, modifies the current AgentSet; otherwise, returns a new AgentSet. Defaults to False.
|
|
144
134
|
agent_type (type[Agent], optional): The class type of the agents to select. Defaults to None, meaning no type filtering is applied.
|
|
145
135
|
|
|
146
136
|
Returns:
|
|
147
137
|
AgentSet: A new AgentSet containing the selected agents, unless inplace is True, in which case the current AgentSet is updated.
|
|
138
|
+
|
|
139
|
+
Notes:
|
|
140
|
+
- at_most just return the first n or fraction of agents. To take a random sample, shuffle() beforehand.
|
|
141
|
+
- at_most is an upper limit. When specifying other criteria, the number of agents returned can be smaller.
|
|
148
142
|
"""
|
|
143
|
+
if n is not None:
|
|
144
|
+
warnings.warn(
|
|
145
|
+
"The parameter 'n' is deprecated. Use 'at_most' instead.",
|
|
146
|
+
DeprecationWarning,
|
|
147
|
+
stacklevel=2,
|
|
148
|
+
)
|
|
149
|
+
at_most = n
|
|
149
150
|
|
|
150
|
-
|
|
151
|
+
inf = float("inf")
|
|
152
|
+
if filter_func is None and agent_type is None and at_most == inf:
|
|
151
153
|
return self if inplace else copy.copy(self)
|
|
152
154
|
|
|
153
|
-
|
|
155
|
+
# Check if at_most is of type float
|
|
156
|
+
if at_most <= 1.0 and isinstance(at_most, float):
|
|
157
|
+
at_most = int(len(self) * at_most) # Note that it rounds down (floor)
|
|
158
|
+
|
|
159
|
+
def agent_generator(filter_func, agent_type, at_most):
|
|
154
160
|
count = 0
|
|
155
161
|
for agent in self:
|
|
162
|
+
if count >= at_most:
|
|
163
|
+
break
|
|
156
164
|
if (not filter_func or filter_func(agent)) and (
|
|
157
165
|
not agent_type or isinstance(agent, agent_type)
|
|
158
166
|
):
|
|
159
167
|
yield agent
|
|
160
168
|
count += 1
|
|
161
|
-
if 0 < n <= count:
|
|
162
|
-
break
|
|
163
169
|
|
|
164
|
-
agents = agent_generator(filter_func, agent_type,
|
|
170
|
+
agents = agent_generator(filter_func, agent_type, at_most)
|
|
165
171
|
|
|
166
172
|
return AgentSet(agents, self.model) if not inplace else self._update(agents)
|
|
167
173
|
|
|
@@ -226,53 +232,179 @@ class AgentSet(MutableSet, Sequence):
|
|
|
226
232
|
self._agents = weakref.WeakKeyDictionary({agent: None for agent in agents})
|
|
227
233
|
return self
|
|
228
234
|
|
|
229
|
-
def do(
|
|
230
|
-
self, method_name: str, *args, return_results: bool = False, **kwargs
|
|
231
|
-
) -> AgentSet | list[Any]:
|
|
235
|
+
def do(self, method: str | Callable, *args, **kwargs) -> AgentSet:
|
|
232
236
|
"""
|
|
233
|
-
Invoke a method on each agent in the AgentSet.
|
|
237
|
+
Invoke a method or function on each agent in the AgentSet.
|
|
234
238
|
|
|
235
239
|
Args:
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
+
method (str, callable): the callable to do on each agent
|
|
241
|
+
|
|
242
|
+
* in case of str, the name of the method to call on each agent.
|
|
243
|
+
* in case of callable, the function to be called with each agent as first argument
|
|
244
|
+
|
|
245
|
+
*args: Variable length argument list passed to the callable being called.
|
|
246
|
+
**kwargs: Arbitrary keyword arguments passed to the callable being called.
|
|
240
247
|
|
|
241
248
|
Returns:
|
|
242
|
-
AgentSet | list[Any]: The results of the
|
|
249
|
+
AgentSet | list[Any]: The results of the callable calls if return_results is True, otherwise the AgentSet itself.
|
|
243
250
|
"""
|
|
251
|
+
try:
|
|
252
|
+
return_results = kwargs.pop("return_results")
|
|
253
|
+
except KeyError:
|
|
254
|
+
return_results = False
|
|
255
|
+
else:
|
|
256
|
+
warnings.warn(
|
|
257
|
+
"Using return_results is deprecated. Use AgenSet.do in case of return_results=False, and "
|
|
258
|
+
"AgentSet.map in case of return_results=True",
|
|
259
|
+
stacklevel=2,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
if return_results:
|
|
263
|
+
return self.map(method, *args, **kwargs)
|
|
264
|
+
|
|
244
265
|
# we iterate over the actual weakref keys and check if weakref is alive before calling the method
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
266
|
+
if isinstance(method, str):
|
|
267
|
+
for agentref in self._agents.keyrefs():
|
|
268
|
+
if (agent := agentref()) is not None:
|
|
269
|
+
getattr(agent, method)(*args, **kwargs)
|
|
270
|
+
else:
|
|
271
|
+
for agentref in self._agents.keyrefs():
|
|
272
|
+
if (agent := agentref()) is not None:
|
|
273
|
+
method(agent, *args, **kwargs)
|
|
274
|
+
|
|
275
|
+
return self
|
|
250
276
|
|
|
251
|
-
|
|
277
|
+
def shuffle_do(self, method: str | Callable, *args, **kwargs) -> AgentSet:
|
|
278
|
+
"""Shuffle the agents in the AgentSet and then invoke a method or function on each agent.
|
|
252
279
|
|
|
253
|
-
|
|
280
|
+
It's a fast, optimized version of calling shuffle() followed by do().
|
|
281
|
+
"""
|
|
282
|
+
agents = list(self._agents.keys())
|
|
283
|
+
self.random.shuffle(agents)
|
|
284
|
+
|
|
285
|
+
if isinstance(method, str):
|
|
286
|
+
for agent in agents:
|
|
287
|
+
getattr(agent, method)(*args, **kwargs)
|
|
288
|
+
else:
|
|
289
|
+
for agent in agents:
|
|
290
|
+
method(agent, *args, **kwargs)
|
|
291
|
+
|
|
292
|
+
return self
|
|
293
|
+
|
|
294
|
+
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.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
method (str, callable): the callable to apply on each agent
|
|
300
|
+
|
|
301
|
+
* in case of str, the name of the method to call on each agent.
|
|
302
|
+
* in case of callable, the function to be called with each agent as first argument
|
|
303
|
+
|
|
304
|
+
*args: Variable length argument list passed to the callable being called.
|
|
305
|
+
**kwargs: Arbitrary keyword arguments passed to the callable being called.
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
list[Any]: The results of the callable calls
|
|
309
|
+
"""
|
|
310
|
+
# we iterate over the actual weakref keys and check if weakref is alive before calling the method
|
|
311
|
+
if isinstance(method, str):
|
|
312
|
+
res = [
|
|
313
|
+
getattr(agent, method)(*args, **kwargs)
|
|
314
|
+
for agentref in self._agents.keyrefs()
|
|
315
|
+
if (agent := agentref()) is not None
|
|
316
|
+
]
|
|
317
|
+
else:
|
|
318
|
+
res = [
|
|
319
|
+
method(agent, *args, **kwargs)
|
|
320
|
+
for agentref in self._agents.keyrefs()
|
|
321
|
+
if (agent := agentref()) is not None
|
|
322
|
+
]
|
|
323
|
+
|
|
324
|
+
return res
|
|
325
|
+
|
|
326
|
+
def agg(self, attribute: str, func: Callable) -> Any:
|
|
327
|
+
"""
|
|
328
|
+
Aggregate an attribute of all agents in the AgentSet using a specified function.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
attribute (str): The name of the attribute to aggregate.
|
|
332
|
+
func (Callable): The function to apply to the attribute values (e.g., min, max, sum, np.mean).
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
Any: The result of applying the function to the attribute values. Often a single value.
|
|
336
|
+
"""
|
|
337
|
+
values = self.get(attribute)
|
|
338
|
+
return func(values)
|
|
339
|
+
|
|
340
|
+
@overload
|
|
341
|
+
def get(
|
|
342
|
+
self,
|
|
343
|
+
attr_names: str,
|
|
344
|
+
handle_missing: Literal["error", "default"] = "error",
|
|
345
|
+
default_value: Any = None,
|
|
346
|
+
) -> list[Any]: ...
|
|
347
|
+
|
|
348
|
+
@overload
|
|
349
|
+
def get(
|
|
350
|
+
self,
|
|
351
|
+
attr_names: list[str],
|
|
352
|
+
handle_missing: Literal["error", "default"] = "error",
|
|
353
|
+
default_value: Any = None,
|
|
354
|
+
) -> list[list[Any]]: ...
|
|
355
|
+
|
|
356
|
+
def get(
|
|
357
|
+
self,
|
|
358
|
+
attr_names,
|
|
359
|
+
handle_missing="error",
|
|
360
|
+
default_value=None,
|
|
361
|
+
):
|
|
254
362
|
"""
|
|
255
363
|
Retrieve the specified attribute(s) from each agent in the AgentSet.
|
|
256
364
|
|
|
257
365
|
Args:
|
|
258
366
|
attr_names (str | list[str]): The name(s) of the attribute(s) to retrieve from each agent.
|
|
367
|
+
handle_missing (str, optional): How to handle missing attributes. Can be:
|
|
368
|
+
- 'error' (default): raises an AttributeError if attribute is missing.
|
|
369
|
+
- 'default': returns the specified default_value.
|
|
370
|
+
default_value (Any, optional): The default value to return if 'handle_missing' is set to 'default'
|
|
371
|
+
and the agent does not have the attribute.
|
|
259
372
|
|
|
260
373
|
Returns:
|
|
261
|
-
list[Any]: A list with the attribute value for each agent
|
|
262
|
-
list[list[Any]]: A list with a
|
|
374
|
+
list[Any]: A list with the attribute value for each agent if attr_names is a str.
|
|
375
|
+
list[list[Any]]: A list with a lists of attribute values for each agent if attr_names is a list of str.
|
|
263
376
|
|
|
264
377
|
Raises:
|
|
265
|
-
AttributeError
|
|
266
|
-
|
|
378
|
+
AttributeError: If 'handle_missing' is 'error' and the agent does not have the specified attribute(s).
|
|
379
|
+
ValueError: If an unknown 'handle_missing' option is provided.
|
|
267
380
|
"""
|
|
381
|
+
is_single_attr = isinstance(attr_names, str)
|
|
382
|
+
|
|
383
|
+
if handle_missing == "error":
|
|
384
|
+
if is_single_attr:
|
|
385
|
+
return [getattr(agent, attr_names) for agent in self._agents]
|
|
386
|
+
else:
|
|
387
|
+
return [
|
|
388
|
+
[getattr(agent, attr) for attr in attr_names]
|
|
389
|
+
for agent in self._agents
|
|
390
|
+
]
|
|
391
|
+
|
|
392
|
+
elif handle_missing == "default":
|
|
393
|
+
if is_single_attr:
|
|
394
|
+
return [
|
|
395
|
+
getattr(agent, attr_names, default_value) for agent in self._agents
|
|
396
|
+
]
|
|
397
|
+
else:
|
|
398
|
+
return [
|
|
399
|
+
[getattr(agent, attr, default_value) for attr in attr_names]
|
|
400
|
+
for agent in self._agents
|
|
401
|
+
]
|
|
268
402
|
|
|
269
|
-
if isinstance(attr_names, str):
|
|
270
|
-
return [getattr(agent, attr_names) for agent in self._agents]
|
|
271
403
|
else:
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
404
|
+
raise ValueError(
|
|
405
|
+
f"Unknown handle_missing option: {handle_missing}, "
|
|
406
|
+
"should be one of 'error' or 'default'"
|
|
407
|
+
)
|
|
276
408
|
|
|
277
409
|
def set(self, attr_name: str, value: Any) -> AgentSet:
|
|
278
410
|
"""
|
|
@@ -371,7 +503,139 @@ class AgentSet(MutableSet, Sequence):
|
|
|
371
503
|
"""
|
|
372
504
|
return self.model.random
|
|
373
505
|
|
|
506
|
+
def groupby(self, by: Callable | str, result_type: str = "agentset") -> GroupBy:
|
|
507
|
+
"""
|
|
508
|
+
Group agents by the specified attribute or return from the callable
|
|
509
|
+
|
|
510
|
+
Args:
|
|
511
|
+
by (Callable, str): used to determine what to group agents by
|
|
512
|
+
|
|
513
|
+
* if ``by`` is a callable, it will be called for each agent and the return is used
|
|
514
|
+
for grouping
|
|
515
|
+
* if ``by`` is a str, it should refer to an attribute on the agent and the value
|
|
516
|
+
of this attribute will be used for grouping
|
|
517
|
+
result_type (str, optional): The datatype for the resulting groups {"agentset", "list"}
|
|
518
|
+
Returns:
|
|
519
|
+
GroupBy
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
Notes:
|
|
523
|
+
There might be performance benefits to using `result_type='list'` if you don't need the advanced functionality
|
|
524
|
+
of an AgentSet.
|
|
525
|
+
|
|
526
|
+
"""
|
|
527
|
+
groups = defaultdict(list)
|
|
528
|
+
|
|
529
|
+
if isinstance(by, Callable):
|
|
530
|
+
for agent in self:
|
|
531
|
+
groups[by(agent)].append(agent)
|
|
532
|
+
else:
|
|
533
|
+
for agent in self:
|
|
534
|
+
groups[getattr(agent, by)].append(agent)
|
|
535
|
+
|
|
536
|
+
if result_type == "agentset":
|
|
537
|
+
return GroupBy(
|
|
538
|
+
{k: AgentSet(v, model=self.model) for k, v in groups.items()}
|
|
539
|
+
)
|
|
540
|
+
else:
|
|
541
|
+
return GroupBy(groups)
|
|
542
|
+
|
|
543
|
+
# consider adding for performance reasons
|
|
544
|
+
# for Sequence: __reversed__, index, and count
|
|
545
|
+
# for MutableSet clear, pop, remove, __ior__, __iand__, __ixor__, and __isub__
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
class GroupBy:
|
|
549
|
+
"""Helper class for AgentSet.groupby
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
Attributes:
|
|
553
|
+
groups (dict): A dictionary with the group_name as key and group as values
|
|
554
|
+
|
|
555
|
+
"""
|
|
556
|
+
|
|
557
|
+
def __init__(self, groups: dict[Any, list | AgentSet]):
|
|
558
|
+
self.groups: dict[Any, list | AgentSet] = groups
|
|
559
|
+
|
|
560
|
+
def map(self, method: Callable | str, *args, **kwargs) -> dict[Any, Any]:
|
|
561
|
+
"""Apply the specified callable to each group and return the results.
|
|
562
|
+
|
|
563
|
+
Args:
|
|
564
|
+
method (Callable, str): The callable to apply to each group,
|
|
565
|
+
|
|
566
|
+
* if ``method`` is a callable, it will be called it will be called with the group as first argument
|
|
567
|
+
* if ``method`` is a str, it should refer to a method on the group
|
|
568
|
+
|
|
569
|
+
Additional arguments and keyword arguments will be passed on to the callable.
|
|
570
|
+
|
|
571
|
+
Returns:
|
|
572
|
+
dict with group_name as key and the return of the method as value
|
|
573
|
+
|
|
574
|
+
Notes:
|
|
575
|
+
this method is useful for methods or functions that do return something. It
|
|
576
|
+
will break method chaining. For that, use ``do`` instead.
|
|
577
|
+
|
|
578
|
+
"""
|
|
579
|
+
if isinstance(method, str):
|
|
580
|
+
return {
|
|
581
|
+
k: getattr(v, method)(*args, **kwargs) for k, v in self.groups.items()
|
|
582
|
+
}
|
|
583
|
+
else:
|
|
584
|
+
return {k: method(v, *args, **kwargs) for k, v in self.groups.items()}
|
|
585
|
+
|
|
586
|
+
def do(self, method: Callable | str, *args, **kwargs) -> GroupBy:
|
|
587
|
+
"""Apply the specified callable to each group
|
|
588
|
+
|
|
589
|
+
Args:
|
|
590
|
+
method (Callable, str): The callable to apply to each group,
|
|
591
|
+
|
|
592
|
+
* if ``method`` is a callable, it will be called it will be called with the group as first argument
|
|
593
|
+
* if ``method`` is a str, it should refer to a method on the group
|
|
594
|
+
|
|
595
|
+
Additional arguments and keyword arguments will be passed on to the callable.
|
|
596
|
+
|
|
597
|
+
Returns:
|
|
598
|
+
the original GroupBy instance
|
|
599
|
+
|
|
600
|
+
Notes:
|
|
601
|
+
this method is useful for methods or functions that don't return anything and/or
|
|
602
|
+
if you want to chain multiple do calls
|
|
603
|
+
|
|
604
|
+
"""
|
|
605
|
+
if isinstance(method, str):
|
|
606
|
+
for v in self.groups.values():
|
|
607
|
+
getattr(v, method)(*args, **kwargs)
|
|
608
|
+
else:
|
|
609
|
+
for v in self.groups.values():
|
|
610
|
+
method(v, *args, **kwargs)
|
|
611
|
+
|
|
612
|
+
return self
|
|
613
|
+
|
|
614
|
+
def count(self) -> dict[Any, int]:
|
|
615
|
+
"""Return the count of agents in each group.
|
|
616
|
+
|
|
617
|
+
Returns:
|
|
618
|
+
dict: A dictionary mapping group names to the number of agents in each group.
|
|
619
|
+
"""
|
|
620
|
+
return {k: len(v) for k, v in self.groups.items()}
|
|
621
|
+
|
|
622
|
+
def agg(self, attr_name: str, func: Callable) -> dict[Hashable, Any]:
|
|
623
|
+
"""Aggregate the values of a specific attribute across each group using the provided function.
|
|
624
|
+
|
|
625
|
+
Args:
|
|
626
|
+
attr_name (str): The name of the attribute to aggregate.
|
|
627
|
+
func (Callable): The function to apply (e.g., sum, min, max, mean).
|
|
628
|
+
|
|
629
|
+
Returns:
|
|
630
|
+
dict[Hashable, Any]: A dictionary mapping group names to the result of applying the aggregation function.
|
|
631
|
+
"""
|
|
632
|
+
return {
|
|
633
|
+
group_name: func([getattr(agent, attr_name) for agent in group])
|
|
634
|
+
for group_name, group in self.groups.items()
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
def __iter__(self):
|
|
638
|
+
return iter(self.groups.items())
|
|
374
639
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
# for MutableSet clear, pop, remove, __ior__, __iand__, __ixor__, and __isub__
|
|
640
|
+
def __len__(self):
|
|
641
|
+
return len(self.groups)
|
mesa/batchrunner.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import itertools
|
|
2
|
+
import multiprocessing
|
|
2
3
|
from collections.abc import Iterable, Mapping
|
|
3
4
|
from functools import partial
|
|
4
5
|
from multiprocessing import Pool
|
|
@@ -8,6 +9,8 @@ from tqdm.auto import tqdm
|
|
|
8
9
|
|
|
9
10
|
from mesa.model import Model
|
|
10
11
|
|
|
12
|
+
multiprocessing.set_start_method("spawn", force=True)
|
|
13
|
+
|
|
11
14
|
|
|
12
15
|
def batch_run(
|
|
13
16
|
model_cls: type[Model],
|
mesa/datacollection.py
CHANGED
|
@@ -3,28 +3,30 @@ Mesa Data Collection Module
|
|
|
3
3
|
===========================
|
|
4
4
|
|
|
5
5
|
DataCollector is meant to provide a simple, standard way to collect data
|
|
6
|
-
generated by a Mesa model. It collects
|
|
7
|
-
agent-level data, and tables.
|
|
6
|
+
generated by a Mesa model. It collects four types of data: model-level data,
|
|
7
|
+
agent-level data, agent-type-level data, and tables.
|
|
8
8
|
|
|
9
|
-
A DataCollector is instantiated with
|
|
10
|
-
associated variable names or functions for each, one for model-level data
|
|
11
|
-
one for agent-level data
|
|
12
|
-
Variable names are converted into functions
|
|
13
|
-
name.
|
|
9
|
+
A DataCollector is instantiated with three dictionaries of reporter names and
|
|
10
|
+
associated variable names or functions for each, one for model-level data,
|
|
11
|
+
one for agent-level data, and one for agent-type-level data; a fourth dictionary
|
|
12
|
+
provides table names and columns. Variable names are converted into functions
|
|
13
|
+
which retrieve attributes of that name.
|
|
14
14
|
|
|
15
15
|
When the collect() method is called, each model-level function is called, with
|
|
16
16
|
the model as the argument, and the results associated with the relevant
|
|
17
|
-
variable. Then the agent-level functions are called on each agent
|
|
17
|
+
variable. Then the agent-level functions are called on each agent, and the
|
|
18
|
+
agent-type-level functions are called on each agent of the specified type.
|
|
18
19
|
|
|
19
20
|
Additionally, other objects can write directly to tables by passing in an
|
|
20
21
|
appropriate dictionary object for a table row.
|
|
21
22
|
|
|
22
|
-
The DataCollector then stores the data it collects in dictionaries:
|
|
23
23
|
* model_vars maps each reporter to a list of its values
|
|
24
24
|
* tables maps each table to a dictionary, with each column as a key with a
|
|
25
25
|
list as its value.
|
|
26
|
-
* _agent_records maps each model step to a list of each
|
|
26
|
+
* _agent_records maps each model step to a list of each agent's id
|
|
27
27
|
and its values.
|
|
28
|
+
* _agenttype_records maps each model step to a dictionary of agent types,
|
|
29
|
+
each containing a list of each agent's id and its values.
|
|
28
30
|
|
|
29
31
|
Finally, DataCollector can create a pandas DataFrame from each collection.
|
|
30
32
|
|
|
@@ -36,6 +38,7 @@ The default DataCollector here makes several assumptions:
|
|
|
36
38
|
import contextlib
|
|
37
39
|
import itertools
|
|
38
40
|
import types
|
|
41
|
+
import warnings
|
|
39
42
|
from copy import deepcopy
|
|
40
43
|
from functools import partial
|
|
41
44
|
|
|
@@ -46,24 +49,25 @@ with contextlib.suppress(ImportError):
|
|
|
46
49
|
class DataCollector:
|
|
47
50
|
"""Class for collecting data generated by a Mesa model.
|
|
48
51
|
|
|
49
|
-
A DataCollector is instantiated with dictionaries of names of model
|
|
50
|
-
agent-level variables to collect, associated with
|
|
51
|
-
functions which actually collect them. When the
|
|
52
|
-
called, it collects these attributes and executes
|
|
53
|
-
one and stores the results.
|
|
52
|
+
A DataCollector is instantiated with dictionaries of names of model-,
|
|
53
|
+
agent-, and agent-type-level variables to collect, associated with
|
|
54
|
+
attribute names or functions which actually collect them. When the
|
|
55
|
+
collect(...) method is called, it collects these attributes and executes
|
|
56
|
+
these functions one by one and stores the results.
|
|
54
57
|
"""
|
|
55
58
|
|
|
56
59
|
def __init__(
|
|
57
60
|
self,
|
|
58
61
|
model_reporters=None,
|
|
59
62
|
agent_reporters=None,
|
|
63
|
+
agenttype_reporters=None,
|
|
60
64
|
tables=None,
|
|
61
65
|
):
|
|
62
|
-
"""
|
|
63
|
-
|
|
64
|
-
Both model_reporters and
|
|
65
|
-
variable name to either an attribute name, a function,
|
|
66
|
-
or a function with parameters placed in a list.
|
|
66
|
+
"""Instantiate a DataCollector with lists of model, agent, and agent-type reporters.
|
|
67
|
+
|
|
68
|
+
Both model_reporters, agent_reporters, and agenttype_reporters accept a
|
|
69
|
+
dictionary mapping a variable name to either an attribute name, a function,
|
|
70
|
+
a method of a class/instance, or a function with parameters placed in a list.
|
|
67
71
|
|
|
68
72
|
Model reporters can take four types of arguments:
|
|
69
73
|
1. Lambda function:
|
|
@@ -87,6 +91,10 @@ class DataCollector:
|
|
|
87
91
|
4. Functions with parameters placed in a list:
|
|
88
92
|
{"Agent_Function": [function, [param_1, param_2]]}
|
|
89
93
|
|
|
94
|
+
Agenttype reporters take a dictionary mapping agent types to dictionaries
|
|
95
|
+
of reporter names and attributes/funcs/methods, similar to agent_reporters:
|
|
96
|
+
{Wolf: {"energy": lambda a: a.energy}}
|
|
97
|
+
|
|
90
98
|
The tables arg accepts a dictionary mapping names of tables to lists of
|
|
91
99
|
columns. For example, if we want to allow agents to write their age
|
|
92
100
|
when they are destroyed (to keep track of lifespans), it might look
|
|
@@ -96,6 +104,8 @@ class DataCollector:
|
|
|
96
104
|
Args:
|
|
97
105
|
model_reporters: Dictionary of reporter names and attributes/funcs/methods.
|
|
98
106
|
agent_reporters: Dictionary of reporter names and attributes/funcs/methods.
|
|
107
|
+
agenttype_reporters: Dictionary of agent types to dictionaries of
|
|
108
|
+
reporter names and attributes/funcs/methods.
|
|
99
109
|
tables: Dictionary of table names to lists of column names.
|
|
100
110
|
|
|
101
111
|
Notes:
|
|
@@ -105,9 +115,11 @@ class DataCollector:
|
|
|
105
115
|
"""
|
|
106
116
|
self.model_reporters = {}
|
|
107
117
|
self.agent_reporters = {}
|
|
118
|
+
self.agenttype_reporters = {}
|
|
108
119
|
|
|
109
120
|
self.model_vars = {}
|
|
110
121
|
self._agent_records = {}
|
|
122
|
+
self._agenttype_records = {}
|
|
111
123
|
self.tables = {}
|
|
112
124
|
|
|
113
125
|
if model_reporters is not None:
|
|
@@ -118,6 +130,11 @@ class DataCollector:
|
|
|
118
130
|
for name, reporter in agent_reporters.items():
|
|
119
131
|
self._new_agent_reporter(name, reporter)
|
|
120
132
|
|
|
133
|
+
if agenttype_reporters is not None:
|
|
134
|
+
for agent_type, reporters in agenttype_reporters.items():
|
|
135
|
+
for name, reporter in reporters.items():
|
|
136
|
+
self._new_agenttype_reporter(agent_type, name, reporter)
|
|
137
|
+
|
|
121
138
|
if tables is not None:
|
|
122
139
|
for name, columns in tables.items():
|
|
123
140
|
self._new_table(name, columns)
|
|
@@ -165,6 +182,38 @@ class DataCollector:
|
|
|
165
182
|
|
|
166
183
|
self.agent_reporters[name] = reporter
|
|
167
184
|
|
|
185
|
+
def _new_agenttype_reporter(self, agent_type, name, reporter):
|
|
186
|
+
"""Add a new agent-type-level reporter to collect.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
agent_type: The type of agent to collect data for.
|
|
190
|
+
name: Name of the agent-type-level variable to collect.
|
|
191
|
+
reporter: Attribute string, function object, method of a class/instance, or
|
|
192
|
+
function with parameters placed in a list that returns the
|
|
193
|
+
variable when given an agent instance.
|
|
194
|
+
"""
|
|
195
|
+
if agent_type not in self.agenttype_reporters:
|
|
196
|
+
self.agenttype_reporters[agent_type] = {}
|
|
197
|
+
|
|
198
|
+
# Use the same logic as _new_agent_reporter
|
|
199
|
+
if isinstance(reporter, str):
|
|
200
|
+
attribute_name = reporter
|
|
201
|
+
|
|
202
|
+
def attr_reporter(agent):
|
|
203
|
+
return getattr(agent, attribute_name, None)
|
|
204
|
+
|
|
205
|
+
reporter = attr_reporter
|
|
206
|
+
|
|
207
|
+
elif isinstance(reporter, list):
|
|
208
|
+
func, params = reporter[0], reporter[1]
|
|
209
|
+
|
|
210
|
+
def func_with_params(agent):
|
|
211
|
+
return func(agent, *params)
|
|
212
|
+
|
|
213
|
+
reporter = func_with_params
|
|
214
|
+
|
|
215
|
+
self.agenttype_reporters[agent_type][name] = reporter
|
|
216
|
+
|
|
168
217
|
def _new_table(self, table_name, table_columns):
|
|
169
218
|
"""Add a new table that objects can write to.
|
|
170
219
|
|
|
@@ -192,6 +241,34 @@ class DataCollector:
|
|
|
192
241
|
)
|
|
193
242
|
return agent_records
|
|
194
243
|
|
|
244
|
+
def _record_agenttype(self, model, agent_type):
|
|
245
|
+
"""Record agent-type data in a mapping of functions and agents."""
|
|
246
|
+
rep_funcs = self.agenttype_reporters[agent_type].values()
|
|
247
|
+
|
|
248
|
+
def get_reports(agent):
|
|
249
|
+
_prefix = (agent.model._steps, agent.unique_id)
|
|
250
|
+
reports = tuple(rep(agent) for rep in rep_funcs)
|
|
251
|
+
return _prefix + reports
|
|
252
|
+
|
|
253
|
+
agent_types = model.agent_types
|
|
254
|
+
if agent_type in agent_types:
|
|
255
|
+
agents = model.agents_by_type[agent_type]
|
|
256
|
+
else:
|
|
257
|
+
from mesa import Agent
|
|
258
|
+
|
|
259
|
+
if issubclass(agent_type, Agent):
|
|
260
|
+
agents = [
|
|
261
|
+
agent for agent in model.agents if isinstance(agent, agent_type)
|
|
262
|
+
]
|
|
263
|
+
else:
|
|
264
|
+
# Raise error if agent_type is not in model.agent_types
|
|
265
|
+
raise ValueError(
|
|
266
|
+
f"Agent type {agent_type} is not recognized as an Agent type in the model or Agent subclass. Use an Agent (sub)class, like {agent_types}."
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
agenttype_records = map(get_reports, agents)
|
|
270
|
+
return agenttype_records
|
|
271
|
+
|
|
195
272
|
def collect(self, model):
|
|
196
273
|
"""Collect all the data for the given model object."""
|
|
197
274
|
if self.model_reporters:
|
|
@@ -218,6 +295,14 @@ class DataCollector:
|
|
|
218
295
|
agent_records = self._record_agents(model)
|
|
219
296
|
self._agent_records[model._steps] = list(agent_records)
|
|
220
297
|
|
|
298
|
+
if self.agenttype_reporters:
|
|
299
|
+
self._agenttype_records[model._steps] = {}
|
|
300
|
+
for agent_type in self.agenttype_reporters:
|
|
301
|
+
agenttype_records = self._record_agenttype(model, agent_type)
|
|
302
|
+
self._agenttype_records[model._steps][agent_type] = list(
|
|
303
|
+
agenttype_records
|
|
304
|
+
)
|
|
305
|
+
|
|
221
306
|
def add_table_row(self, table_name, row, ignore_missing=False):
|
|
222
307
|
"""Add a row dictionary to a specific table.
|
|
223
308
|
|
|
@@ -274,6 +359,36 @@ class DataCollector:
|
|
|
274
359
|
)
|
|
275
360
|
return df
|
|
276
361
|
|
|
362
|
+
def get_agenttype_vars_dataframe(self, agent_type):
|
|
363
|
+
"""Create a pandas DataFrame from the agent-type variables for a specific agent type.
|
|
364
|
+
The DataFrame has one column for each variable, with two additional
|
|
365
|
+
columns for tick and agent_id.
|
|
366
|
+
Args:
|
|
367
|
+
agent_type: The type of agent to get the data for.
|
|
368
|
+
"""
|
|
369
|
+
# Check if self.agenttype_reporters dictionary is empty for this agent type, if so return empty DataFrame
|
|
370
|
+
if agent_type not in self.agenttype_reporters:
|
|
371
|
+
warnings.warn(
|
|
372
|
+
f"No agent-type reporters have been defined for {agent_type} in the DataCollector, returning empty DataFrame.",
|
|
373
|
+
UserWarning,
|
|
374
|
+
stacklevel=2,
|
|
375
|
+
)
|
|
376
|
+
return pd.DataFrame()
|
|
377
|
+
|
|
378
|
+
all_records = itertools.chain.from_iterable(
|
|
379
|
+
records[agent_type]
|
|
380
|
+
for records in self._agenttype_records.values()
|
|
381
|
+
if agent_type in records
|
|
382
|
+
)
|
|
383
|
+
rep_names = list(self.agenttype_reporters[agent_type])
|
|
384
|
+
|
|
385
|
+
df = pd.DataFrame.from_records(
|
|
386
|
+
data=all_records,
|
|
387
|
+
columns=["Step", "AgentID", *rep_names],
|
|
388
|
+
index=["Step", "AgentID"],
|
|
389
|
+
)
|
|
390
|
+
return df
|
|
391
|
+
|
|
277
392
|
def get_table_dataframe(self, table_name):
|
|
278
393
|
"""Create a pandas DataFrame from a particular table.
|
|
279
394
|
|
|
@@ -261,7 +261,8 @@ class EpsteinCivilViolence(Model):
|
|
|
261
261
|
self.active_agents = self.agents
|
|
262
262
|
|
|
263
263
|
def step(self):
|
|
264
|
-
|
|
264
|
+
"""Run one step of the model."""
|
|
265
|
+
self.active_agents.shuffle_do("step")
|
|
265
266
|
|
|
266
267
|
|
|
267
268
|
if __name__ == "__main__":
|
|
@@ -231,8 +231,9 @@ class WolfSheep(mesa.Model):
|
|
|
231
231
|
self.grid.place_agent(patch, pos)
|
|
232
232
|
|
|
233
233
|
def step(self):
|
|
234
|
-
|
|
235
|
-
self.
|
|
234
|
+
"""Perform one step of the model."""
|
|
235
|
+
self.agents_by_type[Sheep].shuffle_do("step")
|
|
236
|
+
self.agents_by_type[Wolf].shuffle_do("step")
|
|
236
237
|
|
|
237
238
|
|
|
238
239
|
if __name__ == "__main__":
|
mesa/experimental/jupyter_viz.py
CHANGED
|
@@ -143,14 +143,15 @@ def JupyterViz(
|
|
|
143
143
|
# otherwise, do nothing (do not draw space)
|
|
144
144
|
|
|
145
145
|
# 5. Plots
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
146
|
+
if measures:
|
|
147
|
+
for measure in measures:
|
|
148
|
+
if callable(measure):
|
|
149
|
+
# Is a custom object
|
|
150
|
+
measure(model)
|
|
151
|
+
else:
|
|
152
|
+
components_matplotlib.PlotMatplotlib(
|
|
153
|
+
model, measure, dependencies=dependencies
|
|
154
|
+
)
|
|
154
155
|
|
|
155
156
|
def render_in_browser():
|
|
156
157
|
# if space drawer is disabled, do not include it
|
mesa/model.py
CHANGED
|
@@ -8,10 +8,8 @@ Core Objects: Model
|
|
|
8
8
|
# Remove this __future__ import once the oldest supported Python is 3.10
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
-
import itertools
|
|
12
11
|
import random
|
|
13
12
|
import warnings
|
|
14
|
-
from collections import defaultdict
|
|
15
13
|
|
|
16
14
|
# mypy
|
|
17
15
|
from typing import Any, Union
|
|
@@ -33,20 +31,28 @@ class Model:
|
|
|
33
31
|
running: A boolean indicating if the model should continue running.
|
|
34
32
|
schedule: An object to manage the order and execution of agent steps.
|
|
35
33
|
current_id: A counter for assigning unique IDs to agents.
|
|
36
|
-
agents_: A defaultdict mapping each agent type to a dict of its instances.
|
|
37
|
-
This private attribute is used internally to manage agents.
|
|
38
34
|
|
|
39
35
|
Properties:
|
|
40
|
-
agents: An AgentSet containing all agents in the model
|
|
36
|
+
agents: An AgentSet containing all agents in the model
|
|
41
37
|
agent_types: A list of different agent types present in the model.
|
|
38
|
+
agents_by_type: A dictionary where the keys are agent types and the values are the corresponding AgentSets.
|
|
42
39
|
|
|
43
40
|
Methods:
|
|
44
41
|
get_agents_of_type: Returns an AgentSet of agents of the specified type.
|
|
42
|
+
Deprecated: Use agents_by_type[agenttype] instead.
|
|
45
43
|
run_model: Runs the model's simulation until a defined end condition is reached.
|
|
46
44
|
step: Executes a single step of the model's simulation process.
|
|
47
45
|
next_id: Generates and returns the next unique identifier for an agent.
|
|
48
46
|
reset_randomizer: Resets the model's random number generator with a new or existing seed.
|
|
49
47
|
initialize_data_collector: Sets up the data collector for the model, requiring an initialized scheduler and agents.
|
|
48
|
+
register_agent : register an agent with the model
|
|
49
|
+
deregister_agent : remove an agent from the model
|
|
50
|
+
|
|
51
|
+
Notes:
|
|
52
|
+
Model.agents returns the AgentSet containing all agents registered with the model. Changing
|
|
53
|
+
the content of the AgentSet directly can result in strange behavior. If you want change the
|
|
54
|
+
composition of this AgentSet, ensure you operate on a copy.
|
|
55
|
+
|
|
50
56
|
"""
|
|
51
57
|
|
|
52
58
|
def __new__(cls, *args: Any, **kwargs: Any) -> Any:
|
|
@@ -58,6 +64,7 @@ class Model:
|
|
|
58
64
|
# advance.
|
|
59
65
|
obj._seed = random.random()
|
|
60
66
|
obj.random = random.Random(obj._seed)
|
|
67
|
+
|
|
61
68
|
# TODO: Remove these 2 lines just before Mesa 3.0
|
|
62
69
|
obj._steps = 0
|
|
63
70
|
obj._time = 0
|
|
@@ -72,7 +79,8 @@ class Model:
|
|
|
72
79
|
self.running = True
|
|
73
80
|
self.schedule = None
|
|
74
81
|
self.current_id = 0
|
|
75
|
-
|
|
82
|
+
|
|
83
|
+
self._setup_agent_registration()
|
|
76
84
|
|
|
77
85
|
self._steps: int = 0
|
|
78
86
|
self._time: TimeT = 0 # the model's clock
|
|
@@ -80,33 +88,93 @@ class Model:
|
|
|
80
88
|
@property
|
|
81
89
|
def agents(self) -> AgentSet:
|
|
82
90
|
"""Provides an AgentSet of all agents in the model, combining agents from all types."""
|
|
83
|
-
|
|
84
|
-
if hasattr(self, "_agents"):
|
|
85
|
-
return self._agents
|
|
86
|
-
else:
|
|
87
|
-
all_agents = itertools.chain.from_iterable(self.agents_.values())
|
|
88
|
-
return AgentSet(all_agents, self)
|
|
91
|
+
return self._all_agents
|
|
89
92
|
|
|
90
93
|
@agents.setter
|
|
91
94
|
def agents(self, agents: Any) -> None:
|
|
92
95
|
warnings.warn(
|
|
93
|
-
"You are trying to set model.agents. In
|
|
94
|
-
"by
|
|
95
|
-
"Please adjust your code to use a different attribute name for custom agent storage",
|
|
96
|
+
"You are trying to set model.agents. In Mesa 3.0 and higher, this attribute is "
|
|
97
|
+
"used by Mesa itself, so you cannot use it directly anymore."
|
|
98
|
+
"Please adjust your code to use a different attribute name for custom agent storage.",
|
|
96
99
|
UserWarning,
|
|
97
100
|
stacklevel=2,
|
|
98
101
|
)
|
|
99
102
|
|
|
100
|
-
self._agents = agents
|
|
101
|
-
|
|
102
103
|
@property
|
|
103
104
|
def agent_types(self) -> list[type]:
|
|
104
|
-
"""Return a list of
|
|
105
|
-
return list(self.
|
|
105
|
+
"""Return a list of all unique agent types registered with the model."""
|
|
106
|
+
return list(self._agents_by_type.keys())
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def agents_by_type(self) -> dict[type[Agent], AgentSet]:
|
|
110
|
+
"""A dictionary where the keys are agent types and the values are the corresponding AgentSets."""
|
|
111
|
+
return self._agents_by_type
|
|
106
112
|
|
|
107
113
|
def get_agents_of_type(self, agenttype: type[Agent]) -> AgentSet:
|
|
108
|
-
"""Retrieves an AgentSet containing all agents of the specified type."""
|
|
109
|
-
|
|
114
|
+
"""Deprecated: Retrieves an AgentSet containing all agents of the specified type."""
|
|
115
|
+
warnings.warn(
|
|
116
|
+
f"Model.get_agents_of_type() is deprecated, please replace get_agents_of_type({agenttype})"
|
|
117
|
+
f"with the property agents_by_type[{agenttype}].",
|
|
118
|
+
DeprecationWarning,
|
|
119
|
+
stacklevel=2,
|
|
120
|
+
)
|
|
121
|
+
return self.agents_by_type[agenttype]
|
|
122
|
+
|
|
123
|
+
def _setup_agent_registration(self):
|
|
124
|
+
"""helper method to initialize the agent registration datastructures"""
|
|
125
|
+
self._agents = {} # the hard references to all agents in the model
|
|
126
|
+
self._agents_by_type: dict[
|
|
127
|
+
type[Agent], AgentSet
|
|
128
|
+
] = {} # a dict with an agentset for each class of agents
|
|
129
|
+
self._all_agents = AgentSet([], self) # an agenset with all agents
|
|
130
|
+
|
|
131
|
+
def register_agent(self, agent):
|
|
132
|
+
"""Register the agent with the model
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
agent: The agent to register.
|
|
136
|
+
|
|
137
|
+
Notes:
|
|
138
|
+
This method is called automatically by ``Agent.__init__``, so there is no need to use this
|
|
139
|
+
if you are subclassing Agent and calling its super in the ``__init__`` method.
|
|
140
|
+
|
|
141
|
+
"""
|
|
142
|
+
if not hasattr(self, "_agents"):
|
|
143
|
+
self._setup_agent_registration()
|
|
144
|
+
|
|
145
|
+
warnings.warn(
|
|
146
|
+
"The Mesa Model class was not initialized. In the future, you need to explicitly initialize "
|
|
147
|
+
"the Model by calling super().__init__() on initialization.",
|
|
148
|
+
FutureWarning,
|
|
149
|
+
stacklevel=2,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
self._agents[agent] = None
|
|
153
|
+
|
|
154
|
+
# because AgentSet requires model, we cannot use defaultdict
|
|
155
|
+
# tricks with a function won't work because model then cannot be pickled
|
|
156
|
+
try:
|
|
157
|
+
self._agents_by_type[type(agent)].add(agent)
|
|
158
|
+
except KeyError:
|
|
159
|
+
self._agents_by_type[type(agent)] = AgentSet(
|
|
160
|
+
[
|
|
161
|
+
agent,
|
|
162
|
+
],
|
|
163
|
+
self,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
self._all_agents.add(agent)
|
|
167
|
+
|
|
168
|
+
def deregister_agent(self, agent):
|
|
169
|
+
"""Deregister the agent with the model
|
|
170
|
+
|
|
171
|
+
Notes::
|
|
172
|
+
This method is called automatically by ``Agent.remove``
|
|
173
|
+
|
|
174
|
+
"""
|
|
175
|
+
del self._agents[agent]
|
|
176
|
+
self._agents_by_type[type(agent)].remove(agent)
|
|
177
|
+
self._all_agents.remove(agent)
|
|
110
178
|
|
|
111
179
|
def run_model(self) -> None:
|
|
112
180
|
"""Run the model until the end condition is reached. Overload as
|
|
@@ -144,6 +212,7 @@ class Model:
|
|
|
144
212
|
self,
|
|
145
213
|
model_reporters=None,
|
|
146
214
|
agent_reporters=None,
|
|
215
|
+
agenttype_reporters=None,
|
|
147
216
|
tables=None,
|
|
148
217
|
) -> None:
|
|
149
218
|
if not hasattr(self, "schedule") or self.schedule is None:
|
|
@@ -157,6 +226,7 @@ class Model:
|
|
|
157
226
|
self.datacollector = DataCollector(
|
|
158
227
|
model_reporters=model_reporters,
|
|
159
228
|
agent_reporters=agent_reporters,
|
|
229
|
+
agenttype_reporters=agenttype_reporters,
|
|
160
230
|
tables=tables,
|
|
161
231
|
)
|
|
162
232
|
# Collect data for the first time during initialization.
|
mesa/space.py
CHANGED
|
@@ -459,15 +459,21 @@ class _Grid:
|
|
|
459
459
|
elif selection == "closest":
|
|
460
460
|
current_pos = agent.pos
|
|
461
461
|
# Find the closest position without sorting all positions
|
|
462
|
-
|
|
462
|
+
# TODO: See if this method can be optimized further
|
|
463
|
+
closest_pos = []
|
|
463
464
|
min_distance = float("inf")
|
|
464
465
|
agent.random.shuffle(pos)
|
|
465
466
|
for p in pos:
|
|
466
467
|
distance = self._distance_squared(p, current_pos)
|
|
467
468
|
if distance < min_distance:
|
|
468
469
|
min_distance = distance
|
|
469
|
-
closest_pos
|
|
470
|
-
|
|
470
|
+
closest_pos.clear()
|
|
471
|
+
closest_pos.append(p)
|
|
472
|
+
elif distance == min_distance:
|
|
473
|
+
closest_pos.append(p)
|
|
474
|
+
|
|
475
|
+
chosen_pos = agent.random.choice(closest_pos)
|
|
476
|
+
|
|
471
477
|
else:
|
|
472
478
|
raise ValueError(
|
|
473
479
|
f"Invalid selection method {selection}. Choose 'random' or 'closest'."
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: Mesa
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: Agent-based modeling (ABM) in Python
|
|
5
5
|
Project-URL: homepage, https://github.com/projectmesa/mesa
|
|
6
6
|
Project-URL: repository, https://github.com/projectmesa/mesa
|
|
@@ -18,6 +18,7 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.10
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.11
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
22
|
Classifier: Topic :: Scientific/Engineering
|
|
22
23
|
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
23
24
|
Classifier: Topic :: Scientific/Engineering :: Artificial Life
|
|
@@ -55,7 +56,7 @@ Description-Content-Type: text/markdown
|
|
|
55
56
|
| Meta | [](https://github.com/astral-sh/ruff) [](https://github.com/psf/black) [](https://github.com/pypa/hatch) |
|
|
56
57
|
| Chat | [](https://matrix.to/#/#project-mesa:matrix.org) |
|
|
57
58
|
|
|
58
|
-
*This is the `2.
|
|
59
|
+
*This is the `2.4.x-maintenance` branch. Example models for Mesa 2.x can be found [here](https://github.com/projectmesa/mesa-examples/tree/mesa-2.x/examples).*
|
|
59
60
|
|
|
60
61
|
Mesa allows users to quickly create agent-based models using built-in
|
|
61
62
|
core components (such as spatial grids and agent schedulers) or
|
|
@@ -101,13 +102,13 @@ For resources or help on using Mesa, check out the following:
|
|
|
101
102
|
|
|
102
103
|
- [Intro to Mesa Tutorial](http://mesa.readthedocs.org/en/stable/tutorials/intro_tutorial.html) (An introductory model, the Boltzmann
|
|
103
104
|
Wealth Model, for beginners or those new to Mesa.)
|
|
104
|
-
- [Visualization Tutorial](https://mesa.readthedocs.io/
|
|
105
|
+
- [Visualization Tutorial](https://mesa.readthedocs.io/stable/tutorials/visualization_tutorial.html) (An introduction into our Solara visualization)
|
|
105
106
|
- [Complexity Explorer Tutorial](https://www.complexityexplorer.org/courses/172-agent-based-models-with-python-an-introduction-to-mesa) (An advanced-beginner model,
|
|
106
107
|
SugarScape with Traders, with instructional videos)
|
|
107
108
|
- [Mesa Examples](https://github.com/projectmesa/mesa-examples/tree/mesa-2.x/examples) (A repository of seminal ABMs using Mesa and
|
|
108
109
|
examples of employing specific Mesa Features)
|
|
109
110
|
- [Docs](http://mesa.readthedocs.org/) (Mesa's documentation, API and useful snippets)
|
|
110
|
-
- [Development version docs](https://mesa.readthedocs.io/
|
|
111
|
+
- [Development version docs](https://mesa.readthedocs.io/latest/) (the latest version docs if you're using a pre-release Mesa version)
|
|
111
112
|
- [Discussions](https://github.com/projectmesa/mesa/discussions) (GitHub threaded discussions about Mesa)
|
|
112
113
|
- [Matrix Chat](https://matrix.to/#/#project-mesa:matrix.org) (Chat Forum via Matrix to talk about Mesa)
|
|
113
114
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
mesa/__init__.py,sha256=
|
|
2
|
-
mesa/agent.py,sha256=
|
|
3
|
-
mesa/batchrunner.py,sha256=
|
|
4
|
-
mesa/datacollection.py,sha256=
|
|
1
|
+
mesa/__init__.py,sha256=s9A2haUb53qfSt3a1x5zpqN3aFxdw-s2SlQ5ctmYA3E,680
|
|
2
|
+
mesa/agent.py,sha256=Vz7RGwIuMSiOgI-CEO3N3cnHJRFp5CIHh5u4t-dWH08,23481
|
|
3
|
+
mesa/batchrunner.py,sha256=kyx0-0LjzoMA5vVMWP6nK9hYWK_M1EvKaAN0t_D9ej4,6133
|
|
4
|
+
mesa/datacollection.py,sha256=9yYiONDdNxltQE_6FUKkRgDkBAHFggJM9F_7sKR6CfU,16344
|
|
5
5
|
mesa/main.py,sha256=7MovfNz88VWNnfXP0kcERB6C3GfkVOh0hb0o32hM9LU,1602
|
|
6
|
-
mesa/model.py,sha256=
|
|
7
|
-
mesa/space.py,sha256=
|
|
6
|
+
mesa/model.py,sha256=J_zVRH2caf_LIx8I2fi5whyD6N6fTNfnvXVIMdDKzD0,8454
|
|
7
|
+
mesa/space.py,sha256=PI97fZTHnqNLBzVEIrQ88NMGKkTVWwzukiTewLE3bHU,62722
|
|
8
8
|
mesa/time.py,sha256=G83UKWeMFMnQV9-79Ps2gbD_Qz3hM07IFYLzf5Rvz1w,15243
|
|
9
9
|
mesa/cookiecutter-mesa/cookiecutter.json,sha256=tBSWli39fOWUXGfiDCTKd92M7uKaBIswXbkOdbUufYY,337
|
|
10
10
|
mesa/cookiecutter-mesa/hooks/post_gen_project.py,sha256=8JoXZKIioRYEWJURC0udj8WS3rg0c4So62sOZSGbrMY,294
|
|
@@ -16,7 +16,7 @@ mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/model.pytem
|
|
|
16
16
|
mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/server.pytemplate,sha256=nqi6cPjhiyrYw82_Y9hLSrfZtSVCGIhMLOXRB7kKTlQ,823
|
|
17
17
|
mesa/experimental/UserParam.py,sha256=kpFu5yH_RSTI4S2XQxYAGWJgGwQo6j-yCADYJHry4lE,1641
|
|
18
18
|
mesa/experimental/__init__.py,sha256=ncy-EG4IMoSzknYXLMt3Z61nkMwkrH96QbJpGUqOpxQ,168
|
|
19
|
-
mesa/experimental/jupyter_viz.py,sha256=
|
|
19
|
+
mesa/experimental/jupyter_viz.py,sha256=UzQjWIR_L_flAInphNzOaBtpl58Xo1JnvnuIE9Fwf78,13501
|
|
20
20
|
mesa/experimental/cell_space/__init__.py,sha256=trFVKf2l5RbkCUyxP09Kox_J3ak2YdM4o3t40Tsjjm4,628
|
|
21
21
|
mesa/experimental/cell_space/cell.py,sha256=AUnvVnXWhdgzr0bLKDRDO9c93v22Zkw6W-tWxhEhGdQ,4578
|
|
22
22
|
mesa/experimental/cell_space/cell_agent.py,sha256=G4u9ht4gW9ns1y2L7pFumF3K4HiP6ROuxwrxHZ-mL1M,1107
|
|
@@ -29,8 +29,8 @@ mesa/experimental/components/matplotlib.py,sha256=2toQmjvd1_xj_jxdQPRmpDLeuvWvZP
|
|
|
29
29
|
mesa/experimental/devs/__init__.py,sha256=CWam15vCj-RD_biMyqv4sJfos1fsL823P7MDEGrbwW8,174
|
|
30
30
|
mesa/experimental/devs/eventlist.py,sha256=H9hufe9VmwvlXQr146wCa7PgbzVvivG4Bk9rlEERZ7A,4880
|
|
31
31
|
mesa/experimental/devs/simulator.py,sha256=NQ3rtBIzykBtMWNslG_Fg04NQn2lYT8cmH-7ndr8v0Y,9530
|
|
32
|
-
mesa/experimental/devs/examples/epstein_civil_violence.py,sha256=
|
|
33
|
-
mesa/experimental/devs/examples/wolf_sheep.py,sha256=
|
|
32
|
+
mesa/experimental/devs/examples/epstein_civil_violence.py,sha256=vJo5Z2vlDe59V1CazJIgtZU4W22xTRphQE0KoZbOImc,9694
|
|
33
|
+
mesa/experimental/devs/examples/wolf_sheep.py,sha256=TbpLo37uk3JIH4MFQujnSmgqi3KPEVROD_DiSpRlv3I,7706
|
|
34
34
|
mesa/flat/__init__.py,sha256=hSqQDjkfIgDu7B3aYtjPeNEUXdlKPHQuNN8HEV0XR6s,218
|
|
35
35
|
mesa/flat/visualization.py,sha256=5aCm8xDCmZij3hoJZvOVmmpzU9ACXSSSvmQr51buLVg,290
|
|
36
36
|
mesa/visualization/ModularVisualization.py,sha256=gT-FYRnvTFFxtehu-N-8eBLShCF8t_Ut8Au4iVPZIlA,60
|
|
@@ -38,8 +38,8 @@ mesa/visualization/TextVisualization.py,sha256=BIP0XcmIdYhz0igqe8yRZXlXeOOqJZeu8
|
|
|
38
38
|
mesa/visualization/UserParam.py,sha256=D3qxoX-Cpqhyn06IdIO_C5s0u8nlhv3988lVwkBlcGo,49
|
|
39
39
|
mesa/visualization/__init__.py,sha256=5fwVAzgVsmxAzgoLxdC26l2ZE-m2bWj963xPNSDaQEQ,287
|
|
40
40
|
mesa/visualization/modules.py,sha256=pf6K3KECX51VNNqpFCm2EE5KV0A22UYmfXzTVXPnF_o,47
|
|
41
|
-
mesa-2.
|
|
42
|
-
mesa-2.
|
|
43
|
-
mesa-2.
|
|
44
|
-
mesa-2.
|
|
45
|
-
mesa-2.
|
|
41
|
+
mesa-2.4.0.dist-info/METADATA,sha256=pLU_lLJDgsowHvvfgh1n-beLMC406r_ArpWKwTyDbm8,8346
|
|
42
|
+
mesa-2.4.0.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
|
43
|
+
mesa-2.4.0.dist-info/entry_points.txt,sha256=IOcQtetGF8l4wHpOs_hGb19Rz-FS__BMXOJR10IBPsA,39
|
|
44
|
+
mesa-2.4.0.dist-info/licenses/LICENSE,sha256=OGUgret9fRrm8J3pdsPXETIjf0H8puK_Nmy970ZzT78,572
|
|
45
|
+
mesa-2.4.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|