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 CHANGED
@@ -26,7 +26,7 @@ __all__ = [
26
26
  ]
27
27
 
28
28
  __title__ = "mesa"
29
- __version__ = "2.3.4"
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
- # 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
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.agents_[type(self)].pop(self)
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
- n: int = 0,
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
- n (int, optional): The number of agents to select. If 0, all matching agents are selected. Defaults to 0.
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
- if filter_func is None and agent_type is None and n == 0:
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
- def agent_generator(filter_func=None, agent_type=None, n=0):
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, n)
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
- 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.
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 method calls if return_results is True, otherwise the AgentSet itself.
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
- res = [
246
- getattr(agent, method_name)(*args, **kwargs)
247
- for agentref in self._agents.keyrefs()
248
- if (agent := agentref()) is not None
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
- return res if return_results else self
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
- def get(self, attr_names: str | list[str]) -> list[Any]:
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 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
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 if an agent does not have the specified attribute(s)
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
- return [
273
- [getattr(agent, attr_name) for attr_name in attr_names]
274
- for agent in self._agents
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
- # consider adding for performance reasons
376
- # for Sequence: __reversed__, index, and count
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 three types of data: model-level data,
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 two dictionaries of reporter names and
10
- associated variable names or functions for each, one for model-level data and
11
- one for agent-level data; a third dictionary provides table names and columns.
12
- Variable names are converted into functions which retrieve attributes of that
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 agents id
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- and
50
- agent-level variables to collect, associated with attribute names or
51
- functions which actually collect them. When the collect(...) method is
52
- called, it collects these attributes and executes these functions one by
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
- Instantiate a DataCollector with lists of model and agent reporters.
64
- Both model_reporters and agent_reporters accept a dictionary mapping a
65
- variable name to either an attribute name, a function, a method of a class/instance,
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
- self.active_agents.shuffle(inplace=True).do("step")
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
- self.get_agents_of_type(Sheep).shuffle(inplace=True).do("step")
235
- self.get_agents_of_type(Wolf).shuffle(inplace=True).do("step")
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__":
@@ -143,14 +143,15 @@ def JupyterViz(
143
143
  # otherwise, do nothing (do not draw space)
144
144
 
145
145
  # 5. Plots
146
- for measure in measures:
147
- if callable(measure):
148
- # Is a custom object
149
- measure(model)
150
- else:
151
- components_matplotlib.PlotMatplotlib(
152
- model, measure, dependencies=dependencies
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, generated from the _agents attribute.
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
- self.agents_: defaultdict[type, dict] = defaultdict(dict)
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 a next release, this attribute is used "
94
- "by MESA itself so you cannot use it directly anymore."
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 different agent types."""
105
- return list(self.agents_.keys())
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
- return AgentSet(self.agents_[agenttype].keys(), self)
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
- closest_pos = None
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 = p
470
- chosen_pos = closest_pos
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.4
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 | [![linting - Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) [![code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![Hatch project](https://img.shields.io/badge/%F0%9F%A5%9A-Hatch-4051b5.svg)](https://github.com/pypa/hatch) |
56
57
  | Chat | [![chat](https://img.shields.io/matrix/project-mesa:matrix.org?label=chat&logo=Matrix)](https://matrix.to/#/#project-mesa:matrix.org) |
57
58
 
58
- *This is the `2.3.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
+ *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/en/stable/tutorials/visualization_tutorial.html) (An introduction into our Solara visualization)
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/en/latest/) (the latest version docs if you're using a pre-release Mesa version)
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=fbfWiSzuVuMgF_znRDKBOgMEBo-DGcrl_NS2ZrNWPwY,680
2
- mesa/agent.py,sha256=db7xGHY0mWgasp-zqC3NgCz_aQoBMHA8DuErK3o7hfk,13398
3
- mesa/batchrunner.py,sha256=G-sj2g6N6E0BsBikYq5yKsgTNnKHr4ADBkRWa9PXkl8,6055
4
- mesa/datacollection.py,sha256=wJwt-bOU8WuXZomUg8JJhtIg4KMhoqXS0Zvyjv9SCAE,11445
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=RxTCJUBfEgRIu3dXiMK9oMlxS3owwNQaQIrVRs6HsZY,5823
7
- mesa/space.py,sha256=luMN5D6rBluEpkKJGRwmueAgsf4_txzLfnLvMXk3k60,62486
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=q2Xn-QjH0nqJoxY1RV_SSHC_OgMFZyKWHJjrn5jHCRk,13444
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=KqH9KI-A_BYt7oWi9kaOhTzjrf2pETqzSpAQG8ewud0,9667
33
- mesa/experimental/devs/examples/wolf_sheep.py,sha256=h5z-eDqMpYeOjrq293N2BcQbs_LDVsgtg9vblXJM7XQ,7697
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.3.4.dist-info/METADATA,sha256=PTPfEpy2KJuMXXdLUAYn6qEm79MkRFEH9UwDDsIa508,8301
42
- mesa-2.3.4.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
43
- mesa-2.3.4.dist-info/entry_points.txt,sha256=IOcQtetGF8l4wHpOs_hGb19Rz-FS__BMXOJR10IBPsA,39
44
- mesa-2.3.4.dist-info/licenses/LICENSE,sha256=OGUgret9fRrm8J3pdsPXETIjf0H8puK_Nmy970ZzT78,572
45
- mesa-2.3.4.dist-info/RECORD,,
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