Mesa 3.0.0a3__py3-none-any.whl → 3.0.0a4__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
@@ -24,7 +24,7 @@ __all__ = [
24
24
  ]
25
25
 
26
26
  __title__ = "mesa"
27
- __version__ = "3.0.0a3"
27
+ __version__ = "3.0.0a4"
28
28
  __license__ = "Apache 2.0"
29
29
  _this_year = datetime.datetime.now(tz=datetime.timezone.utc).date().year
30
30
  __copyright__ = f"Copyright {_this_year} Project Mesa Team"
mesa/agent.py CHANGED
@@ -10,6 +10,8 @@ from __future__ import annotations
10
10
 
11
11
  import contextlib
12
12
  import copy
13
+ import functools
14
+ import itertools
13
15
  import operator
14
16
  import warnings
15
17
  import weakref
@@ -18,7 +20,7 @@ from collections.abc import Callable, Iterable, Iterator, MutableSet, Sequence
18
20
  from random import Random
19
21
 
20
22
  # mypy
21
- from typing import TYPE_CHECKING, Any
23
+ from typing import TYPE_CHECKING, Any, Literal
22
24
 
23
25
  if TYPE_CHECKING:
24
26
  # We ensure that these are not imported during runtime to prevent cyclic
@@ -32,21 +34,49 @@ class Agent:
32
34
  Base class for a model agent in Mesa.
33
35
 
34
36
  Attributes:
35
- unique_id (int): A unique identifier for this agent.
36
37
  model (Model): A reference to the model instance.
37
- self.pos: Position | None = None
38
+ unique_id (int): A unique identifier for this agent.
39
+ pos (Position): A reference to the position where this agent is located.
40
+
41
+ Notes:
42
+ unique_id is unique relative to a model instance and starts from 1
43
+
38
44
  """
39
45
 
40
- def __init__(self, unique_id: int, model: Model) -> None:
46
+ # this is a class level attribute
47
+ # it is a dictionary, indexed by model instance
48
+ # so, unique_id is unique relative to a model, and counting starts from 1
49
+ _ids = defaultdict(functools.partial(itertools.count, 1))
50
+
51
+ def __init__(self, *args, **kwargs) -> None:
41
52
  """
42
53
  Create a new agent.
43
54
 
44
55
  Args:
45
- unique_id (int): A unique identifier for this agent.
46
56
  model (Model): The model instance in which the agent exists.
47
57
  """
48
- self.unique_id = unique_id
49
- self.model = model
58
+ # TODO: Cleanup in future Mesa version (3.1+)
59
+ match args:
60
+ # Case 1: Only the model is provided. The new correct behavior.
61
+ case [model]:
62
+ self.model = model
63
+ self.unique_id = next(self._ids[model])
64
+ # Case 2: Both unique_id and model are provided, deprecated
65
+ case [_, model]:
66
+ warnings.warn(
67
+ "unique ids are assigned automatically to Agents in Mesa 3. The use of custom unique_id is "
68
+ "deprecated. Only input a model when calling `super()__init__(model)`. The unique_id inputted is not used.",
69
+ DeprecationWarning,
70
+ stacklevel=2,
71
+ )
72
+ self.model = model
73
+ self.unique_id = next(self._ids[model])
74
+ # Case 3: Anything else, raise an error
75
+ case _:
76
+ raise ValueError(
77
+ "Invalid arguments provided to initialize the Agent. Only input a model: `super()__init__(model)`."
78
+ )
79
+
50
80
  self.pos: Position | None = None
51
81
 
52
82
  self.model.register_agent(self)
@@ -304,29 +334,72 @@ class AgentSet(MutableSet, Sequence):
304
334
 
305
335
  return res
306
336
 
307
- def get(self, attr_names: str | list[str]) -> list[Any]:
337
+ def agg(self, attribute: str, func: Callable) -> Any:
338
+ """
339
+ Aggregate an attribute of all agents in the AgentSet using a specified function.
340
+
341
+ Args:
342
+ attribute (str): The name of the attribute to aggregate.
343
+ func (Callable): The function to apply to the attribute values (e.g., min, max, sum, np.mean).
344
+
345
+ Returns:
346
+ Any: The result of applying the function to the attribute values. Often a single value.
347
+ """
348
+ values = self.get(attribute)
349
+ return func(values)
350
+
351
+ def get(
352
+ self,
353
+ attr_names: str | list[str],
354
+ handle_missing: Literal["error", "default"] = "error",
355
+ default_value: Any = None,
356
+ ) -> list[Any] | list[list[Any]]:
308
357
  """
309
358
  Retrieve the specified attribute(s) from each agent in the AgentSet.
310
359
 
311
360
  Args:
312
361
  attr_names (str | list[str]): The name(s) of the attribute(s) to retrieve from each agent.
362
+ handle_missing (str, optional): How to handle missing attributes. Can be:
363
+ - 'error' (default): raises an AttributeError if attribute is missing.
364
+ - 'default': returns the specified default_value.
365
+ default_value (Any, optional): The default value to return if 'handle_missing' is set to 'default'
366
+ and the agent does not have the attribute.
313
367
 
314
368
  Returns:
315
- list[Any]: A list with the attribute value for each agent in the set if attr_names is a str
316
- 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
369
+ list[Any]: A list with the attribute value for each agent if attr_names is a str.
370
+ list[list[Any]]: A list with a lists of attribute values for each agent if attr_names is a list of str.
317
371
 
318
372
  Raises:
319
- AttributeError if an agent does not have the specified attribute(s)
320
-
321
- """
373
+ AttributeError: If 'handle_missing' is 'error' and the agent does not have the specified attribute(s).
374
+ ValueError: If an unknown 'handle_missing' option is provided.
375
+ """
376
+ is_single_attr = isinstance(attr_names, str)
377
+
378
+ if handle_missing == "error":
379
+ if is_single_attr:
380
+ return [getattr(agent, attr_names) for agent in self._agents]
381
+ else:
382
+ return [
383
+ [getattr(agent, attr) for attr in attr_names]
384
+ for agent in self._agents
385
+ ]
386
+
387
+ elif handle_missing == "default":
388
+ if is_single_attr:
389
+ return [
390
+ getattr(agent, attr_names, default_value) for agent in self._agents
391
+ ]
392
+ else:
393
+ return [
394
+ [getattr(agent, attr, default_value) for attr in attr_names]
395
+ for agent in self._agents
396
+ ]
322
397
 
323
- if isinstance(attr_names, str):
324
- return [getattr(agent, attr_names) for agent in self._agents]
325
398
  else:
326
- return [
327
- [getattr(agent, attr_name) for attr_name in attr_names]
328
- for agent in self._agents
329
- ]
399
+ raise ValueError(
400
+ f"Unknown handle_missing option: {handle_missing}, "
401
+ "should be one of 'error' or 'default'"
402
+ )
330
403
 
331
404
  def set(self, attr_name: str, value: Any) -> AgentSet:
332
405
  """
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],
@@ -0,0 +1,56 @@
1
+ class UserParam:
2
+ _ERROR_MESSAGE = "Missing or malformed inputs for '{}' Option '{}'"
3
+
4
+ def maybe_raise_error(self, param_type, valid):
5
+ if valid:
6
+ return
7
+ msg = self._ERROR_MESSAGE.format(param_type, self.label)
8
+ raise ValueError(msg)
9
+
10
+
11
+ class Slider(UserParam):
12
+ """
13
+ A number-based slider input with settable increment.
14
+
15
+ Example:
16
+
17
+ slider_option = Slider("My Slider", value=123, min=10, max=200, step=0.1)
18
+
19
+ Args:
20
+ label: The displayed label in the UI
21
+ value: The initial value of the slider
22
+ min: The minimum possible value of the slider
23
+ max: The maximum possible value of the slider
24
+ step: The step between min and max for a range of possible values
25
+ dtype: either int or float
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ label="",
31
+ value=None,
32
+ min=None,
33
+ max=None,
34
+ step=1,
35
+ dtype=None,
36
+ ):
37
+ self.label = label
38
+ self.value = value
39
+ self.min = min
40
+ self.max = max
41
+ self.step = step
42
+
43
+ # Validate option type to make sure values are supplied properly
44
+ valid = not (self.value is None or self.min is None or self.max is None)
45
+ self.maybe_raise_error("slider", valid)
46
+
47
+ if dtype is None:
48
+ self.is_float_slider = self._check_values_are_float(value, min, max, step)
49
+ else:
50
+ self.is_float_slider = dtype is float
51
+
52
+ def _check_values_are_float(self, value, min, max, step):
53
+ return any(isinstance(n, float) for n in (value, min, max, step))
54
+
55
+ def get(self, attr):
56
+ return getattr(self, attr)
@@ -1,3 +1,5 @@
1
1
  from mesa.experimental import cell_space
2
2
 
3
- __all__ = ["cell_space"]
3
+ from .solara_viz import JupyterViz, Slider, SolaraViz, make_text
4
+
5
+ __all__ = ["cell_space", "JupyterViz", "SolaraViz", "make_text", "Slider"]
@@ -19,7 +19,7 @@ class CellAgent(Agent):
19
19
  cell: (Cell | None): the cell which the agent occupies
20
20
  """
21
21
 
22
- def __init__(self, unique_id: int, model: Model) -> None:
22
+ def __init__(self, model: Model) -> None:
23
23
  """
24
24
  Create a new agent.
25
25
 
@@ -27,7 +27,7 @@ class CellAgent(Agent):
27
27
  unique_id (int): A unique identifier for this agent.
28
28
  model (Model): The model instance in which the agent exists.
29
29
  """
30
- super().__init__(unique_id, model)
30
+ super().__init__(model)
31
31
  self.cell: Cell | None = None
32
32
 
33
33
  def move_to(self, cell) -> None:
@@ -0,0 +1,71 @@
1
+ import contextlib
2
+
3
+ import solara
4
+
5
+ with contextlib.suppress(ImportError):
6
+ import altair as alt
7
+
8
+
9
+ @solara.component
10
+ def SpaceAltair(model, agent_portrayal, dependencies: list[any] | None = None):
11
+ space = getattr(model, "grid", None)
12
+ if space is None:
13
+ # Sometimes the space is defined as model.space instead of model.grid
14
+ space = model.space
15
+ chart = _draw_grid(space, agent_portrayal)
16
+ solara.FigureAltair(chart)
17
+
18
+
19
+ def _draw_grid(space, agent_portrayal):
20
+ def portray(g):
21
+ all_agent_data = []
22
+ for content, (x, y) in g.coord_iter():
23
+ if not content:
24
+ continue
25
+ if not hasattr(content, "__iter__"):
26
+ # Is a single grid
27
+ content = [content] # noqa: PLW2901
28
+ for agent in content:
29
+ # use all data from agent portrayal, and add x,y coordinates
30
+ agent_data = agent_portrayal(agent)
31
+ agent_data["x"] = x
32
+ agent_data["y"] = y
33
+ all_agent_data.append(agent_data)
34
+ return all_agent_data
35
+
36
+ all_agent_data = portray(space)
37
+ invalid_tooltips = ["color", "size", "x", "y"]
38
+
39
+ encoding_dict = {
40
+ # no x-axis label
41
+ "x": alt.X("x", axis=None, type="ordinal"),
42
+ # no y-axis label
43
+ "y": alt.Y("y", axis=None, type="ordinal"),
44
+ "tooltip": [
45
+ alt.Tooltip(key, type=alt.utils.infer_vegalite_type([value]))
46
+ for key, value in all_agent_data[0].items()
47
+ if key not in invalid_tooltips
48
+ ],
49
+ }
50
+ has_color = "color" in all_agent_data[0]
51
+ if has_color:
52
+ encoding_dict["color"] = alt.Color("color", type="nominal")
53
+ has_size = "size" in all_agent_data[0]
54
+ if has_size:
55
+ encoding_dict["size"] = alt.Size("size", type="quantitative")
56
+
57
+ chart = (
58
+ alt.Chart(
59
+ alt.Data(values=all_agent_data), encoding=alt.Encoding(**encoding_dict)
60
+ )
61
+ .mark_point(filled=True)
62
+ .properties(width=280, height=280)
63
+ # .configure_view(strokeOpacity=0) # hide grid/chart lines
64
+ )
65
+ # This is the default value for the marker size, which auto-scales
66
+ # according to the grid area.
67
+ if not has_size:
68
+ length = min(space.width, space.height)
69
+ chart = chart.mark_point(size=30000 / length**2, filled=True)
70
+
71
+ return chart
@@ -0,0 +1,224 @@
1
+ from collections import defaultdict
2
+
3
+ import networkx as nx
4
+ import solara
5
+ from matplotlib.figure import Figure
6
+ from matplotlib.ticker import MaxNLocator
7
+
8
+ import mesa
9
+ from mesa.experimental.cell_space import VoronoiGrid
10
+
11
+
12
+ @solara.component
13
+ def SpaceMatplotlib(model, agent_portrayal, dependencies: list[any] | None = None):
14
+ space_fig = Figure()
15
+ space_ax = space_fig.subplots()
16
+ space = getattr(model, "grid", None)
17
+ if space is None:
18
+ # Sometimes the space is defined as model.space instead of model.grid
19
+ space = model.space
20
+ if isinstance(space, mesa.space.NetworkGrid):
21
+ _draw_network_grid(space, space_ax, agent_portrayal)
22
+ elif isinstance(space, mesa.space.ContinuousSpace):
23
+ _draw_continuous_space(space, space_ax, agent_portrayal)
24
+ elif isinstance(space, VoronoiGrid):
25
+ _draw_voronoi(space, space_ax, agent_portrayal)
26
+ else:
27
+ _draw_grid(space, space_ax, agent_portrayal)
28
+ solara.FigureMatplotlib(space_fig, format="png", dependencies=dependencies)
29
+
30
+
31
+ # matplotlib scatter does not allow for multiple shapes in one call
32
+ def _split_and_scatter(portray_data, space_ax):
33
+ grouped_data = defaultdict(lambda: {"x": [], "y": [], "s": [], "c": []})
34
+
35
+ # Extract data from the dictionary
36
+ x = portray_data["x"]
37
+ y = portray_data["y"]
38
+ s = portray_data["s"]
39
+ c = portray_data["c"]
40
+ m = portray_data["m"]
41
+
42
+ if not (len(x) == len(y) == len(s) == len(c) == len(m)):
43
+ raise ValueError(
44
+ "Length mismatch in portrayal data lists: "
45
+ f"x: {len(x)}, y: {len(y)}, size: {len(s)}, "
46
+ f"color: {len(c)}, marker: {len(m)}"
47
+ )
48
+
49
+ # Group the data by marker
50
+ for i in range(len(x)):
51
+ marker = m[i]
52
+ grouped_data[marker]["x"].append(x[i])
53
+ grouped_data[marker]["y"].append(y[i])
54
+ grouped_data[marker]["s"].append(s[i])
55
+ grouped_data[marker]["c"].append(c[i])
56
+
57
+ # Plot each group with the same marker
58
+ for marker, data in grouped_data.items():
59
+ space_ax.scatter(data["x"], data["y"], s=data["s"], c=data["c"], marker=marker)
60
+
61
+
62
+ def _draw_grid(space, space_ax, agent_portrayal):
63
+ def portray(g):
64
+ x = []
65
+ y = []
66
+ s = [] # size
67
+ c = [] # color
68
+ m = [] # shape
69
+ for i in range(g.width):
70
+ for j in range(g.height):
71
+ content = g._grid[i][j]
72
+ if not content:
73
+ continue
74
+ if not hasattr(content, "__iter__"):
75
+ # Is a single grid
76
+ content = [content]
77
+ for agent in content:
78
+ data = agent_portrayal(agent)
79
+ x.append(i)
80
+ y.append(j)
81
+
82
+ # This is the default value for the marker size, which auto-scales
83
+ # according to the grid area.
84
+ default_size = (180 / max(g.width, g.height)) ** 2
85
+ # establishing a default prevents misalignment if some agents are not given size, color, etc.
86
+ size = data.get("size", default_size)
87
+ s.append(size)
88
+ color = data.get("color", "tab:blue")
89
+ c.append(color)
90
+ mark = data.get("shape", "o")
91
+ m.append(mark)
92
+ out = {"x": x, "y": y, "s": s, "c": c, "m": m}
93
+ return out
94
+
95
+ space_ax.set_xlim(-1, space.width)
96
+ space_ax.set_ylim(-1, space.height)
97
+ _split_and_scatter(portray(space), space_ax)
98
+
99
+
100
+ def _draw_network_grid(space, space_ax, agent_portrayal):
101
+ graph = space.G
102
+ pos = nx.spring_layout(graph, seed=0)
103
+ nx.draw(
104
+ graph,
105
+ ax=space_ax,
106
+ pos=pos,
107
+ **agent_portrayal(graph),
108
+ )
109
+
110
+
111
+ def _draw_continuous_space(space, space_ax, agent_portrayal):
112
+ def portray(space):
113
+ x = []
114
+ y = []
115
+ s = [] # size
116
+ c = [] # color
117
+ m = [] # shape
118
+ for agent in space._agent_to_index:
119
+ data = agent_portrayal(agent)
120
+ _x, _y = agent.pos
121
+ x.append(_x)
122
+ y.append(_y)
123
+
124
+ # This is matplotlib's default marker size
125
+ default_size = 20
126
+ # establishing a default prevents misalignment if some agents are not given size, color, etc.
127
+ size = data.get("size", default_size)
128
+ s.append(size)
129
+ color = data.get("color", "tab:blue")
130
+ c.append(color)
131
+ mark = data.get("shape", "o")
132
+ m.append(mark)
133
+ out = {"x": x, "y": y, "s": s, "c": c, "m": m}
134
+ return out
135
+
136
+ # Determine border style based on space.torus
137
+ border_style = "solid" if not space.torus else (0, (5, 10))
138
+
139
+ # Set the border of the plot
140
+ for spine in space_ax.spines.values():
141
+ spine.set_linewidth(1.5)
142
+ spine.set_color("black")
143
+ spine.set_linestyle(border_style)
144
+
145
+ width = space.x_max - space.x_min
146
+ x_padding = width / 20
147
+ height = space.y_max - space.y_min
148
+ y_padding = height / 20
149
+ space_ax.set_xlim(space.x_min - x_padding, space.x_max + x_padding)
150
+ space_ax.set_ylim(space.y_min - y_padding, space.y_max + y_padding)
151
+
152
+ # Portray and scatter the agents in the space
153
+ _split_and_scatter(portray(space), space_ax)
154
+
155
+
156
+ def _draw_voronoi(space, space_ax, agent_portrayal):
157
+ def portray(g):
158
+ x = []
159
+ y = []
160
+ s = [] # size
161
+ c = [] # color
162
+
163
+ for cell in g.all_cells:
164
+ for agent in cell.agents:
165
+ data = agent_portrayal(agent)
166
+ x.append(cell.coordinate[0])
167
+ y.append(cell.coordinate[1])
168
+ if "size" in data:
169
+ s.append(data["size"])
170
+ if "color" in data:
171
+ c.append(data["color"])
172
+ out = {"x": x, "y": y}
173
+ # This is the default value for the marker size, which auto-scales
174
+ # according to the grid area.
175
+ out["s"] = s
176
+ if len(c) > 0:
177
+ out["c"] = c
178
+
179
+ return out
180
+
181
+ x_list = [i[0] for i in space.centroids_coordinates]
182
+ y_list = [i[1] for i in space.centroids_coordinates]
183
+ x_max = max(x_list)
184
+ x_min = min(x_list)
185
+ y_max = max(y_list)
186
+ y_min = min(y_list)
187
+
188
+ width = x_max - x_min
189
+ x_padding = width / 20
190
+ height = y_max - y_min
191
+ y_padding = height / 20
192
+ space_ax.set_xlim(x_min - x_padding, x_max + x_padding)
193
+ space_ax.set_ylim(y_min - y_padding, y_max + y_padding)
194
+ space_ax.scatter(**portray(space))
195
+
196
+ for cell in space.all_cells:
197
+ polygon = cell.properties["polygon"]
198
+ space_ax.fill(
199
+ *zip(*polygon),
200
+ alpha=min(1, cell.properties[space.cell_coloring_property]),
201
+ c="red",
202
+ ) # Plot filled polygon
203
+ space_ax.plot(*zip(*polygon), color="black") # Plot polygon edges in red
204
+
205
+
206
+ @solara.component
207
+ def PlotMatplotlib(model, measure, dependencies: list[any] | None = None):
208
+ fig = Figure()
209
+ ax = fig.subplots()
210
+ df = model.datacollector.get_model_vars_dataframe()
211
+ if isinstance(measure, str):
212
+ ax.plot(df.loc[:, measure])
213
+ ax.set_ylabel(measure)
214
+ elif isinstance(measure, dict):
215
+ for m, color in measure.items():
216
+ ax.plot(df.loc[:, m], label=m, color=color)
217
+ fig.legend()
218
+ elif isinstance(measure, list | tuple):
219
+ for m in measure:
220
+ ax.plot(df.loc[:, m], label=m)
221
+ fig.legend()
222
+ # Set integer x axis
223
+ ax.xaxis.set_major_locator(MaxNLocator(integer=True))
224
+ solara.FigureMatplotlib(fig, dependencies=dependencies)
@@ -7,8 +7,8 @@ from mesa.space import SingleGrid
7
7
 
8
8
 
9
9
  class EpsteinAgent(Agent):
10
- def __init__(self, unique_id, model, vision, movement):
11
- super().__init__(unique_id, model)
10
+ def __init__(self, model, vision, movement):
11
+ super().__init__(model)
12
12
  self.vision = vision
13
13
  self.movement = movement
14
14
 
@@ -46,7 +46,6 @@ class Citizen(EpsteinAgent):
46
46
 
47
47
  def __init__(
48
48
  self,
49
- unique_id,
50
49
  model,
51
50
  vision,
52
51
  movement,
@@ -59,7 +58,6 @@ class Citizen(EpsteinAgent):
59
58
  """
60
59
  Create a new Citizen.
61
60
  Args:
62
- unique_id: unique int
63
61
  model : model instance
64
62
  hardship: Agent's 'perceived hardship (i.e., physical or economic
65
63
  privation).' Exogenous, drawn from U(0,1).
@@ -71,7 +69,7 @@ class Citizen(EpsteinAgent):
71
69
  vision: number of cells in each direction (N, S, E and W) that
72
70
  agent can inspect. Exogenous.
73
71
  """
74
- super().__init__(unique_id, model, vision, movement)
72
+ super().__init__(model, vision, movement)
75
73
  self.hardship = hardship
76
74
  self.regime_legitimacy = regime_legitimacy
77
75
  self.risk_aversion = risk_aversion
@@ -144,8 +142,8 @@ class Cop(EpsteinAgent):
144
142
  able to inspect
145
143
  """
146
144
 
147
- def __init__(self, unique_id, model, vision, movement, max_jail_term):
148
- super().__init__(unique_id, model, vision, movement)
145
+ def __init__(self, model, vision, movement, max_jail_term):
146
+ super().__init__(model, vision, movement)
149
147
  self.max_jail_term = max_jail_term
150
148
 
151
149
  def step(self):
@@ -236,7 +234,6 @@ class EpsteinCivilViolence(Model):
236
234
  for _, pos in self.grid.coord_iter():
237
235
  if self.random.random() < self.cop_density:
238
236
  agent = Cop(
239
- self.next_id(),
240
237
  self,
241
238
  cop_vision,
242
239
  movement,
@@ -244,7 +241,6 @@ class EpsteinCivilViolence(Model):
244
241
  )
245
242
  elif self.random.random() < (self.cop_density + self.citizen_density):
246
243
  agent = Citizen(
247
- self.next_id(),
248
244
  self,
249
245
  citizen_vision,
250
246
  movement,
@@ -270,4 +266,4 @@ if __name__ == "__main__":
270
266
 
271
267
  simulator.setup(model)
272
268
 
273
- simulator.run(time_delta=100)
269
+ simulator.run_for(time_delta=100)