Mesa 3.0.0a2__py3-none-any.whl → 3.0.0a3__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.0a2"
27
+ __version__ = "3.0.0a3"
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
@@ -11,7 +11,9 @@ from __future__ import annotations
11
11
  import contextlib
12
12
  import copy
13
13
  import operator
14
+ import warnings
14
15
  import weakref
16
+ from collections import defaultdict
15
17
  from collections.abc import Callable, Iterable, Iterator, MutableSet, Sequence
16
18
  from random import Random
17
19
 
@@ -47,19 +49,12 @@ class Agent:
47
49
  self.model = model
48
50
  self.pos: Position | None = None
49
51
 
50
- # register agent
51
- try:
52
- self.model.agents_[type(self)][self] = None
53
- except AttributeError as err:
54
- # model super has not been called
55
- raise RuntimeError(
56
- "The Mesa Model class was not initialized. You must explicitly initialize the Model by calling super().__init__() on initialization."
57
- ) from err
52
+ self.model.register_agent(self)
58
53
 
59
54
  def remove(self) -> None:
60
55
  """Remove and delete the agent from the model."""
61
56
  with contextlib.suppress(KeyError):
62
- self.model.agents_[type(self)].pop(self)
57
+ self.model.deregister_agent(self)
63
58
 
64
59
  def step(self) -> None:
65
60
  """A single step of the agent."""
@@ -119,9 +114,10 @@ class AgentSet(MutableSet, Sequence):
119
114
  def select(
120
115
  self,
121
116
  filter_func: Callable[[Agent], bool] | None = None,
122
- n: int = 0,
117
+ at_most: int | float = float("inf"),
123
118
  inplace: bool = False,
124
119
  agent_type: type[Agent] | None = None,
120
+ n: int | None = None,
125
121
  ) -> AgentSet:
126
122
  """
127
123
  Select a subset of agents from the AgentSet based on a filter function and/or quantity limit.
@@ -129,29 +125,47 @@ class AgentSet(MutableSet, Sequence):
129
125
  Args:
130
126
  filter_func (Callable[[Agent], bool], optional): A function that takes an Agent and returns True if the
131
127
  agent should be included in the result. Defaults to None, meaning no filtering is applied.
132
- n (int, optional): The number of agents to select. If 0, all matching agents are selected. Defaults to 0.
128
+ at_most (int | float, optional): The maximum amount of agents to select. Defaults to infinity.
129
+ - If an integer, at most the first number of matching agents are selected.
130
+ - If a float between 0 and 1, at most that fraction of original the agents are selected.
133
131
  inplace (bool, optional): If True, modifies the current AgentSet; otherwise, returns a new AgentSet. Defaults to False.
134
132
  agent_type (type[Agent], optional): The class type of the agents to select. Defaults to None, meaning no type filtering is applied.
135
133
 
136
134
  Returns:
137
135
  AgentSet: A new AgentSet containing the selected agents, unless inplace is True, in which case the current AgentSet is updated.
136
+
137
+ Notes:
138
+ - at_most just return the first n or fraction of agents. To take a random sample, shuffle() beforehand.
139
+ - at_most is an upper limit. When specifying other criteria, the number of agents returned can be smaller.
138
140
  """
141
+ if n is not None:
142
+ warnings.warn(
143
+ "The parameter 'n' is deprecated. Use 'at_most' instead.",
144
+ DeprecationWarning,
145
+ stacklevel=2,
146
+ )
147
+ at_most = n
139
148
 
140
- if filter_func is None and agent_type is None and n == 0:
149
+ inf = float("inf")
150
+ if filter_func is None and agent_type is None and at_most == inf:
141
151
  return self if inplace else copy.copy(self)
142
152
 
143
- def agent_generator(filter_func=None, agent_type=None, n=0):
153
+ # Check if at_most is of type float
154
+ if at_most <= 1.0 and isinstance(at_most, float):
155
+ at_most = int(len(self) * at_most) # Note that it rounds down (floor)
156
+
157
+ def agent_generator(filter_func, agent_type, at_most):
144
158
  count = 0
145
159
  for agent in self:
160
+ if count >= at_most:
161
+ break
146
162
  if (not filter_func or filter_func(agent)) and (
147
163
  not agent_type or isinstance(agent, agent_type)
148
164
  ):
149
165
  yield agent
150
166
  count += 1
151
- if 0 < n <= count:
152
- break
153
167
 
154
- agents = agent_generator(filter_func, agent_type, n)
168
+ agents = agent_generator(filter_func, agent_type, at_most)
155
169
 
156
170
  return AgentSet(agents, self.model) if not inplace else self._update(agents)
157
171
 
@@ -216,25 +230,64 @@ class AgentSet(MutableSet, Sequence):
216
230
  self._agents = weakref.WeakKeyDictionary({agent: None for agent in agents})
217
231
  return self
218
232
 
219
- def do(
220
- self, method: str | Callable, *args, return_results: bool = False, **kwargs
221
- ) -> AgentSet | list[Any]:
233
+ def do(self, method: str | Callable, *args, **kwargs) -> AgentSet:
222
234
  """
223
235
  Invoke a method or function on each agent in the AgentSet.
224
236
 
225
237
  Args:
226
- method (str, callable): the callable to do on each agents
238
+ method (str, callable): the callable to do on each agent
227
239
 
228
240
  * in case of str, the name of the method to call on each agent.
229
241
  * in case of callable, the function to be called with each agent as first argument
230
242
 
231
- 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.
232
243
  *args: Variable length argument list passed to the callable being called.
233
244
  **kwargs: Arbitrary keyword arguments passed to the callable being called.
234
245
 
235
246
  Returns:
236
247
  AgentSet | list[Any]: The results of the callable calls if return_results is True, otherwise the AgentSet itself.
237
248
  """
249
+ try:
250
+ return_results = kwargs.pop("return_results")
251
+ except KeyError:
252
+ return_results = False
253
+ else:
254
+ warnings.warn(
255
+ "Using return_results is deprecated. Use AgenSet.do in case of return_results=False, and "
256
+ "AgentSet.map in case of return_results=True",
257
+ stacklevel=2,
258
+ )
259
+
260
+ if return_results:
261
+ return self.map(method, *args, **kwargs)
262
+
263
+ # we iterate over the actual weakref keys and check if weakref is alive before calling the method
264
+ if isinstance(method, str):
265
+ for agentref in self._agents.keyrefs():
266
+ if (agent := agentref()) is not None:
267
+ getattr(agent, method)(*args, **kwargs)
268
+ else:
269
+ for agentref in self._agents.keyrefs():
270
+ if (agent := agentref()) is not None:
271
+ method(agent, *args, **kwargs)
272
+
273
+ return self
274
+
275
+ def map(self, method: str | Callable, *args, **kwargs) -> list[Any]:
276
+ """
277
+ Invoke a method or function on each agent in the AgentSet and return the results.
278
+
279
+ Args:
280
+ method (str, callable): the callable to apply on each agent
281
+
282
+ * in case of str, the name of the method to call on each agent.
283
+ * in case of callable, the function to be called with each agent as first argument
284
+
285
+ *args: Variable length argument list passed to the callable being called.
286
+ **kwargs: Arbitrary keyword arguments passed to the callable being called.
287
+
288
+ Returns:
289
+ list[Any]: The results of the callable calls
290
+ """
238
291
  # we iterate over the actual weakref keys and check if weakref is alive before calling the method
239
292
  if isinstance(method, str):
240
293
  res = [
@@ -249,7 +302,7 @@ class AgentSet(MutableSet, Sequence):
249
302
  if (agent := agentref()) is not None
250
303
  ]
251
304
 
252
- return res if return_results else self
305
+ return res
253
306
 
254
307
  def get(self, attr_names: str | list[str]) -> list[Any]:
255
308
  """
@@ -275,6 +328,21 @@ class AgentSet(MutableSet, Sequence):
275
328
  for agent in self._agents
276
329
  ]
277
330
 
331
+ def set(self, attr_name: str, value: Any) -> AgentSet:
332
+ """
333
+ Set a specified attribute to a given value for all agents in the AgentSet.
334
+
335
+ Args:
336
+ attr_name (str): The name of the attribute to set.
337
+ value (Any): The value to set the attribute to.
338
+
339
+ Returns:
340
+ AgentSet: The AgentSet instance itself, after setting the attribute.
341
+ """
342
+ for agent in self:
343
+ setattr(agent, attr_name, value)
344
+ return self
345
+
278
346
  def __getitem__(self, item: int | slice) -> Agent:
279
347
  """
280
348
  Retrieve an agent or a slice of agents from the AgentSet.
@@ -357,7 +425,116 @@ class AgentSet(MutableSet, Sequence):
357
425
  """
358
426
  return self.model.random
359
427
 
428
+ def groupby(self, by: Callable | str, result_type: str = "agentset") -> GroupBy:
429
+ """
430
+ Group agents by the specified attribute or return from the callable
431
+
432
+ Args:
433
+ by (Callable, str): used to determine what to group agents by
434
+
435
+ * if ``by`` is a callable, it will be called for each agent and the return is used
436
+ for grouping
437
+ * if ``by`` is a str, it should refer to an attribute on the agent and the value
438
+ of this attribute will be used for grouping
439
+ result_type (str, optional): The datatype for the resulting groups {"agentset", "list"}
440
+ Returns:
441
+ GroupBy
442
+
443
+
444
+ Notes:
445
+ There might be performance benefits to using `result_type='list'` if you don't need the advanced functionality
446
+ of an AgentSet.
447
+
448
+ """
449
+ groups = defaultdict(list)
450
+
451
+ if isinstance(by, Callable):
452
+ for agent in self:
453
+ groups[by(agent)].append(agent)
454
+ else:
455
+ for agent in self:
456
+ groups[getattr(agent, by)].append(agent)
457
+
458
+ if result_type == "agentset":
459
+ return GroupBy(
460
+ {k: AgentSet(v, model=self.model) for k, v in groups.items()}
461
+ )
462
+ else:
463
+ return GroupBy(groups)
464
+
465
+ # consider adding for performance reasons
466
+ # for Sequence: __reversed__, index, and count
467
+ # for MutableSet clear, pop, remove, __ior__, __iand__, __ixor__, and __isub__
468
+
469
+
470
+ class GroupBy:
471
+ """Helper class for AgentSet.groupby
472
+
473
+
474
+ Attributes:
475
+ groups (dict): A dictionary with the group_name as key and group as values
476
+
477
+ """
478
+
479
+ def __init__(self, groups: dict[Any, list | AgentSet]):
480
+ self.groups: dict[Any, list | AgentSet] = groups
481
+
482
+ def map(self, method: Callable | str, *args, **kwargs) -> dict[Any, Any]:
483
+ """Apply the specified callable to each group and return the results.
484
+
485
+ Args:
486
+ method (Callable, str): The callable to apply to each group,
487
+
488
+ * if ``method`` is a callable, it will be called it will be called with the group as first argument
489
+ * if ``method`` is a str, it should refer to a method on the group
490
+
491
+ Additional arguments and keyword arguments will be passed on to the callable.
492
+
493
+ Returns:
494
+ dict with group_name as key and the return of the method as value
495
+
496
+ Notes:
497
+ this method is useful for methods or functions that do return something. It
498
+ will break method chaining. For that, use ``do`` instead.
499
+
500
+ """
501
+ if isinstance(method, str):
502
+ return {
503
+ k: getattr(v, method)(*args, **kwargs) for k, v in self.groups.items()
504
+ }
505
+ else:
506
+ return {k: method(v, *args, **kwargs) for k, v in self.groups.items()}
507
+
508
+ def do(self, method: Callable | str, *args, **kwargs) -> GroupBy:
509
+ """Apply the specified callable to each group
510
+
511
+ Args:
512
+ method (Callable, str): The callable to apply to each group,
513
+
514
+ * if ``method`` is a callable, it will be called it will be called with the group as first argument
515
+ * if ``method`` is a str, it should refer to a method on the group
516
+
517
+ Additional arguments and keyword arguments will be passed on to the callable.
518
+
519
+ Returns:
520
+ the original GroupBy instance
521
+
522
+ Notes:
523
+ this method is useful for methods or functions that don't return anything and/or
524
+ if you want to chain multiple do calls
525
+
526
+ """
527
+ if isinstance(method, str):
528
+ for v in self.groups.values():
529
+ getattr(v, method)(*args, **kwargs)
530
+ else:
531
+ for v in self.groups.values():
532
+ method(v, *args, **kwargs)
533
+
534
+ return self
535
+
536
+ def __iter__(self):
537
+ return iter(self.groups.items())
360
538
 
361
- # consider adding for performance reasons
362
- # for Sequence: __reversed__, index, and count
363
- # for MutableSet clear, pop, remove, __ior__, __iand__, __ixor__, and __isub__
539
+ def __len__(self):
540
+ return len(self.groups)
mesa/batchrunner.py CHANGED
@@ -132,14 +132,14 @@ def _model_run_func(
132
132
  """
133
133
  run_id, iteration, kwargs = run
134
134
  model = model_cls(**kwargs)
135
- while model.running and model._steps <= max_steps:
135
+ while model.running and model.steps <= max_steps:
136
136
  model.step()
137
137
 
138
138
  data = []
139
139
 
140
- steps = list(range(0, model._steps, data_collection_period))
141
- if not steps or steps[-1] != model._steps - 1:
142
- steps.append(model._steps - 1)
140
+ steps = list(range(0, model.steps, data_collection_period))
141
+ if not steps or steps[-1] != model.steps - 1:
142
+ steps.append(model.steps - 1)
143
143
 
144
144
  for step in steps:
145
145
  model_data, all_agents_data = _collect_data(model, step)
mesa/datacollection.py CHANGED
@@ -180,7 +180,7 @@ class DataCollector:
180
180
  rep_funcs = self.agent_reporters.values()
181
181
 
182
182
  def get_reports(agent):
183
- _prefix = (agent.model._steps, agent.unique_id)
183
+ _prefix = (agent.model.steps, agent.unique_id)
184
184
  reports = tuple(rep(agent) for rep in rep_funcs)
185
185
  return _prefix + reports
186
186
 
@@ -216,7 +216,7 @@ class DataCollector:
216
216
 
217
217
  if self.agent_reporters:
218
218
  agent_records = self._record_agents(model)
219
- self._agent_records[model._steps] = list(agent_records)
219
+ self._agent_records[model.steps] = list(agent_records)
220
220
 
221
221
  def add_table_row(self, table_name, row, ignore_missing=False):
222
222
  """Add a row dictionary to a specific table.
@@ -9,6 +9,7 @@ from mesa.experimental.cell_space.grid import (
9
9
  OrthogonalVonNeumannGrid,
10
10
  )
11
11
  from mesa.experimental.cell_space.network import Network
12
+ from mesa.experimental.cell_space.voronoi import VoronoiGrid
12
13
 
13
14
  __all__ = [
14
15
  "CellCollection",
@@ -20,4 +21,5 @@ __all__ = [
20
21
  "OrthogonalMooreGrid",
21
22
  "OrthogonalVonNeumannGrid",
22
23
  "Network",
24
+ "VoronoiGrid",
23
25
  ]
@@ -0,0 +1,264 @@
1
+ from collections.abc import Sequence
2
+ from itertools import combinations
3
+ from random import Random
4
+
5
+ import numpy as np
6
+
7
+ from mesa.experimental.cell_space.cell import Cell
8
+ from mesa.experimental.cell_space.discrete_space import DiscreteSpace
9
+
10
+
11
+ class Delaunay:
12
+ """
13
+ Class to compute a Delaunay triangulation in 2D
14
+ ref: http://github.com/jmespadero/pyDelaunay2D
15
+ """
16
+
17
+ def __init__(self, center: tuple = (0, 0), radius: int = 9999) -> None:
18
+ """
19
+ Init and create a new frame to contain the triangulation
20
+ center: Optional position for the center of the frame. Default (0,0)
21
+ radius: Optional distance from corners to the center.
22
+ """
23
+ center = np.asarray(center)
24
+ # Create coordinates for the corners of the frame
25
+ self.coords = [
26
+ center + radius * np.array((-1, -1)),
27
+ center + radius * np.array((+1, -1)),
28
+ center + radius * np.array((+1, +1)),
29
+ center + radius * np.array((-1, +1)),
30
+ ]
31
+
32
+ # Create two dicts to store triangle neighbours and circumcircles.
33
+ self.triangles = {}
34
+ self.circles = {}
35
+
36
+ # Create two CCW triangles for the frame
37
+ triangle1 = (0, 1, 3)
38
+ triangle2 = (2, 3, 1)
39
+ self.triangles[triangle1] = [triangle2, None, None]
40
+ self.triangles[triangle2] = [triangle1, None, None]
41
+
42
+ # Compute circumcenters and circumradius for each triangle
43
+ for t in self.triangles:
44
+ self.circles[t] = self._circumcenter(t)
45
+
46
+ def _circumcenter(self, triangle: list) -> tuple:
47
+ """
48
+ Compute circumcenter and circumradius of a triangle in 2D.
49
+ """
50
+ points = np.asarray([self.coords[v] for v in triangle])
51
+ points2 = np.dot(points, points.T)
52
+ a = np.bmat([[2 * points2, [[1], [1], [1]]], [[[1, 1, 1, 0]]]])
53
+
54
+ b = np.hstack((np.sum(points * points, axis=1), [1]))
55
+ x = np.linalg.solve(a, b)
56
+ bary_coords = x[:-1]
57
+ center = np.dot(bary_coords, points)
58
+
59
+ radius = np.sum(np.square(points[0] - center)) # squared distance
60
+ return (center, radius)
61
+
62
+ def _in_circle(self, triangle: list, point: list) -> bool:
63
+ """
64
+ Check if point p is inside of precomputed circumcircle of triangle.
65
+ """
66
+ center, radius = self.circles[triangle]
67
+ return np.sum(np.square(center - point)) <= radius
68
+
69
+ def add_point(self, point: Sequence) -> None:
70
+ """
71
+ Add a point to the current DT, and refine it using Bowyer-Watson.
72
+ """
73
+ point_index = len(self.coords)
74
+ self.coords.append(np.asarray(point))
75
+
76
+ bad_triangles = []
77
+ for triangle in self.triangles:
78
+ if self._in_circle(triangle, point):
79
+ bad_triangles.append(triangle)
80
+
81
+ boundary = []
82
+ triangle = bad_triangles[0]
83
+ edge = 0
84
+
85
+ while True:
86
+ opposite_triangle = self.triangles[triangle][edge]
87
+ if opposite_triangle not in bad_triangles:
88
+ boundary.append(
89
+ (
90
+ triangle[(edge + 1) % 3],
91
+ triangle[(edge - 1) % 3],
92
+ opposite_triangle,
93
+ )
94
+ )
95
+ edge = (edge + 1) % 3
96
+ if boundary[0][0] == boundary[-1][1]:
97
+ break
98
+ else:
99
+ edge = (self.triangles[opposite_triangle].index(triangle) + 1) % 3
100
+ triangle = opposite_triangle
101
+
102
+ for triangle in bad_triangles:
103
+ del self.triangles[triangle]
104
+ del self.circles[triangle]
105
+
106
+ new_triangles = []
107
+ for e0, e1, opposite_triangle in boundary:
108
+ triangle = (point_index, e0, e1)
109
+ self.circles[triangle] = self._circumcenter(triangle)
110
+ self.triangles[triangle] = [opposite_triangle, None, None]
111
+ if opposite_triangle:
112
+ for i, neighbor in enumerate(self.triangles[opposite_triangle]):
113
+ if neighbor and e1 in neighbor and e0 in neighbor:
114
+ self.triangles[opposite_triangle][i] = triangle
115
+
116
+ new_triangles.append(triangle)
117
+
118
+ n = len(new_triangles)
119
+ for i, triangle in enumerate(new_triangles):
120
+ self.triangles[triangle][1] = new_triangles[(i + 1) % n] # next
121
+ self.triangles[triangle][2] = new_triangles[(i - 1) % n] # previous
122
+
123
+ def export_triangles(self) -> list:
124
+ """
125
+ Export the current list of Delaunay triangles
126
+ """
127
+ triangles_list = [
128
+ (a - 4, b - 4, c - 4)
129
+ for (a, b, c) in self.triangles
130
+ if a > 3 and b > 3 and c > 3
131
+ ]
132
+ return triangles_list
133
+
134
+ def export_voronoi_regions(self):
135
+ """
136
+ Export coordinates and regions of Voronoi diagram as indexed data.
137
+ """
138
+ use_vertex = {i: [] for i in range(len(self.coords))}
139
+ vor_coors = []
140
+ index = {}
141
+ for triangle_index, (a, b, c) in enumerate(sorted(self.triangles)):
142
+ vor_coors.append(self.circles[(a, b, c)][0])
143
+ use_vertex[a] += [(b, c, a)]
144
+ use_vertex[b] += [(c, a, b)]
145
+ use_vertex[c] += [(a, b, c)]
146
+
147
+ index[(a, b, c)] = triangle_index
148
+ index[(c, a, b)] = triangle_index
149
+ index[(b, c, a)] = triangle_index
150
+
151
+ regions = {}
152
+ for i in range(4, len(self.coords)):
153
+ vertex = use_vertex[i][0][0]
154
+ region = []
155
+ for _ in range(len(use_vertex[i])):
156
+ triangle = next(
157
+ triangle for triangle in use_vertex[i] if triangle[0] == vertex
158
+ )
159
+ region.append(index[triangle])
160
+ vertex = triangle[1]
161
+ regions[i - 4] = region
162
+
163
+ return vor_coors, regions
164
+
165
+
166
+ def round_float(x: float) -> int:
167
+ return int(x * 500)
168
+
169
+
170
+ class VoronoiGrid(DiscreteSpace):
171
+ triangulation: Delaunay
172
+ voronoi_coordinates: list
173
+ regions: list
174
+
175
+ def __init__(
176
+ self,
177
+ centroids_coordinates: Sequence[Sequence[float]],
178
+ capacity: float | None = None,
179
+ random: Random | None = None,
180
+ cell_klass: type[Cell] = Cell,
181
+ capacity_function: callable = round_float,
182
+ cell_coloring_property: str | None = None,
183
+ ) -> None:
184
+ """
185
+ A Voronoi Tessellation Grid.
186
+
187
+ Given a set of points, this class creates a grid where a cell is centered in each point,
188
+ its neighbors are given by Voronoi Tessellation cells neighbors
189
+ and the capacity by the polygon area.
190
+
191
+ Args:
192
+ centroids_coordinates: coordinates of centroids to build the tessellation space
193
+ capacity (int) : capacity of the cells in the discrete space
194
+ random (Random): random number generator
195
+ CellKlass (type[Cell]): type of cell class
196
+ capacity_function (Callable): function to compute (int) capacity according to (float) area
197
+ cell_coloring_property (str): voronoi visualization polygon fill property
198
+ """
199
+ super().__init__(capacity=capacity, random=random, cell_klass=cell_klass)
200
+ self.centroids_coordinates = centroids_coordinates
201
+ self._validate_parameters()
202
+
203
+ self._cells = {
204
+ i: cell_klass(self.centroids_coordinates[i], capacity, random=self.random)
205
+ for i in range(len(self.centroids_coordinates))
206
+ }
207
+
208
+ self.regions = None
209
+ self.triangulation = None
210
+ self.voronoi_coordinates = None
211
+ self.capacity_function = capacity_function
212
+ self.cell_coloring_property = cell_coloring_property
213
+
214
+ self._connect_cells()
215
+ self._build_cell_polygons()
216
+
217
+ def _connect_cells(self) -> None:
218
+ """
219
+ Connect cells to neighbors based on given centroids and using Delaunay Triangulation
220
+ """
221
+ self.triangulation = Delaunay()
222
+ for centroid in self.centroids_coordinates:
223
+ self.triangulation.add_point(centroid)
224
+
225
+ for point in self.triangulation.export_triangles():
226
+ for i, j in combinations(point, 2):
227
+ self._cells[i].connect(self._cells[j])
228
+ self._cells[j].connect(self._cells[i])
229
+
230
+ def _validate_parameters(self) -> None:
231
+ if self.capacity is not None and not isinstance(self.capacity, float | int):
232
+ raise ValueError("Capacity must be a number or None.")
233
+ if not isinstance(self.centroids_coordinates, Sequence) or not isinstance(
234
+ self.centroids_coordinates[0], Sequence
235
+ ):
236
+ raise ValueError("Centroids should be a list of lists")
237
+ dimension_1 = len(self.centroids_coordinates[0])
238
+ for coordinate in self.centroids_coordinates:
239
+ if dimension_1 != len(coordinate):
240
+ raise ValueError("Centroid coordinates should be a homogeneous array")
241
+
242
+ def _get_voronoi_regions(self) -> tuple:
243
+ if self.voronoi_coordinates is None or self.regions is None:
244
+ self.voronoi_coordinates, self.regions = (
245
+ self.triangulation.export_voronoi_regions()
246
+ )
247
+ return self.voronoi_coordinates, self.regions
248
+
249
+ @staticmethod
250
+ def _compute_polygon_area(polygon: list) -> float:
251
+ polygon = np.array(polygon)
252
+ x = polygon[:, 0]
253
+ y = polygon[:, 1]
254
+ return 0.5 * np.abs(np.dot(x, np.roll(y, 1)) - np.dot(y, np.roll(x, 1)))
255
+
256
+ def _build_cell_polygons(self):
257
+ coordinates, regions = self._get_voronoi_regions()
258
+ for region in regions:
259
+ polygon = [coordinates[i] for i in regions[region]]
260
+ self._cells[region].properties["polygon"] = polygon
261
+ polygon_area = self._compute_polygon_area(polygon)
262
+ self._cells[region].properties["area"] = polygon_area
263
+ self._cells[region].capacity = self.capacity_function(polygon_area)
264
+ self._cells[region].properties[self.cell_coloring_property] = 0
mesa/model.py CHANGED
@@ -8,9 +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
- from collections import defaultdict
12
+ import warnings
14
13
 
15
14
  # mypy
16
15
  from typing import Any
@@ -18,8 +17,6 @@ from typing import Any
18
17
  from mesa.agent import Agent, AgentSet
19
18
  from mesa.datacollection import DataCollector
20
19
 
21
- TimeT = float | int
22
-
23
20
 
24
21
  class Model:
25
22
  """Base class for models in the Mesa ABM library.
@@ -32,12 +29,12 @@ class Model:
32
29
  running: A boolean indicating if the model should continue running.
33
30
  schedule: An object to manage the order and execution of agent steps.
34
31
  current_id: A counter for assigning unique IDs to agents.
35
- agents_: A defaultdict mapping each agent type to a dict of its instances.
36
- This private attribute is used internally to manage agents.
37
32
 
38
33
  Properties:
39
- agents: An AgentSet containing all agents in the model, generated from the _agents attribute.
34
+ agents: An AgentSet containing all agents in the model
40
35
  agent_types: A list of different agent types present in the model.
36
+ steps: An integer representing the number of steps the model has taken.
37
+ It increases automatically at the start of each step() call.
41
38
 
42
39
  Methods:
43
40
  get_agents_of_type: Returns an AgentSet of agents of the specified type.
@@ -46,6 +43,14 @@ class Model:
46
43
  next_id: Generates and returns the next unique identifier for an agent.
47
44
  reset_randomizer: Resets the model's random number generator with a new or existing seed.
48
45
  initialize_data_collector: Sets up the data collector for the model, requiring an initialized scheduler and agents.
46
+ register_agent : register an agent with the model
47
+ deregister_agent : remove an agent from the model
48
+
49
+ Notes:
50
+ Model.agents returns the AgentSet containing all agents registered with the model. Changing
51
+ the content of the AgentSet directly can result in strange behavior. If you want change the
52
+ composition of this AgentSet, ensure you operate on a copy.
53
+
49
54
  """
50
55
 
51
56
  def __new__(cls, *args: Any, **kwargs: Any) -> Any:
@@ -57,9 +62,6 @@ class Model:
57
62
  # advance.
58
63
  obj._seed = random.random()
59
64
  obj.random = random.Random(obj._seed)
60
- # TODO: Remove these 2 lines just before Mesa 3.0
61
- obj._steps = 0
62
- obj._time = 0
63
65
  return obj
64
66
 
65
67
  def __init__(self, *args: Any, **kwargs: Any) -> None:
@@ -71,39 +73,107 @@ class Model:
71
73
  self.running = True
72
74
  self.schedule = None
73
75
  self.current_id = 0
74
- self.agents_: defaultdict[type, dict] = defaultdict(dict)
76
+ self.steps: int = 0
77
+
78
+ self._setup_agent_registration()
75
79
 
76
- self._steps: int = 0
77
- self._time: TimeT = 0 # the model's clock
80
+ # Wrap the user-defined step method
81
+ self._user_step = self.step
82
+ self.step = self._wrapped_step
83
+
84
+ def _wrapped_step(self, *args: Any, **kwargs: Any) -> None:
85
+ """Automatically increments time and steps after calling the user's step method."""
86
+ # Automatically increment time and step counters
87
+ self.steps += 1
88
+ # Call the original user-defined step method
89
+ self._user_step(*args, **kwargs)
78
90
 
79
91
  @property
80
92
  def agents(self) -> AgentSet:
81
93
  """Provides an AgentSet of all agents in the model, combining agents from all types."""
82
-
83
- if hasattr(self, "_agents"):
84
- return self._agents
85
- else:
86
- all_agents = itertools.chain.from_iterable(self.agents_.values())
87
- return AgentSet(all_agents, self)
94
+ return self._all_agents
88
95
 
89
96
  @agents.setter
90
97
  def agents(self, agents: Any) -> None:
91
98
  raise AttributeError(
92
- "You are trying to set model.agents. In Mesa 3.0 and higher, this attribute will be "
99
+ "You are trying to set model.agents. In Mesa 3.0 and higher, this attribute is "
93
100
  "used by Mesa itself, so you cannot use it directly anymore."
94
101
  "Please adjust your code to use a different attribute name for custom agent storage."
95
102
  )
96
103
 
97
- self._agents = agents
98
-
99
104
  @property
100
105
  def agent_types(self) -> list[type]:
101
- """Return a list of different agent types."""
102
- return list(self.agents_.keys())
106
+ """Return a list of all unique agent types registered with the model."""
107
+ return list(self._agents_by_type.keys())
103
108
 
104
109
  def get_agents_of_type(self, agenttype: type[Agent]) -> AgentSet:
105
- """Retrieves an AgentSet containing all agents of the specified type."""
106
- return AgentSet(self.agents_[agenttype].keys(), self)
110
+ """Retrieves an AgentSet containing all agents of the specified type.
111
+
112
+ Args:
113
+ agenttype: The type of agent to retrieve.
114
+
115
+ Raises:
116
+ KeyError: If agenttype does not exist
117
+
118
+
119
+ """
120
+ return self._agents_by_type[agenttype]
121
+
122
+ def _setup_agent_registration(self):
123
+ """helper method to initialize the agent registration datastructures"""
124
+ self._agents = {} # the hard references to all agents in the model
125
+ self._agents_by_type: dict[
126
+ type, AgentSet
127
+ ] = {} # a dict with an agentset for each class of agents
128
+ self._all_agents = AgentSet([], self) # an agenset with all agents
129
+
130
+ def register_agent(self, agent):
131
+ """Register the agent with the model
132
+
133
+ Args:
134
+ agent: The agent to register.
135
+
136
+ Notes:
137
+ This method is called automatically by ``Agent.__init__``, so there is no need to use this
138
+ if you are subclassing Agent and calling its super in the ``__init__`` method.
139
+
140
+ """
141
+ if not hasattr(self, "_agents"):
142
+ self._setup_agent_registration()
143
+
144
+ warnings.warn(
145
+ "The Mesa Model class was not initialized. In the future, you need to explicitly initialize "
146
+ "the Model by calling super().__init__() on initialization.",
147
+ FutureWarning,
148
+ stacklevel=2,
149
+ )
150
+
151
+ self._agents[agent] = None
152
+
153
+ # because AgentSet requires model, we cannot use defaultdict
154
+ # tricks with a function won't work because model then cannot be pickled
155
+ try:
156
+ self._agents_by_type[type(agent)].add(agent)
157
+ except KeyError:
158
+ self._agents_by_type[type(agent)] = AgentSet(
159
+ [
160
+ agent,
161
+ ],
162
+ self,
163
+ )
164
+
165
+ self._all_agents.add(agent)
166
+
167
+ def deregister_agent(self, agent):
168
+ """Deregister the agent with the model
169
+
170
+ Notes::
171
+ This method is called automatically by ``Agent.remove``
172
+
173
+ """
174
+ del self._agents[agent]
175
+ self._agents_by_type[type(agent)].remove(agent)
176
+ self._all_agents.remove(agent)
107
177
 
108
178
  def run_model(self) -> None:
109
179
  """Run the model until the end condition is reached. Overload as
@@ -115,11 +185,6 @@ class Model:
115
185
  def step(self) -> None:
116
186
  """A single step. Fill in here."""
117
187
 
118
- def _advance_time(self, deltat: TimeT = 1):
119
- """Increment the model's steps counter and clock."""
120
- self._steps += 1
121
- self._time += deltat
122
-
123
188
  def next_id(self) -> int:
124
189
  """Return the next unique ID for agents, increment current_id"""
125
190
  self.current_id += 1
mesa/time.py CHANGED
@@ -69,8 +69,6 @@ class BaseScheduler:
69
69
  self.model = model
70
70
  self.steps = 0
71
71
  self.time: TimeT = 0
72
- self._original_step = self.step
73
- self.step = self._wrapped_step
74
72
 
75
73
  if agents is None:
76
74
  agents = []
@@ -113,11 +111,6 @@ class BaseScheduler:
113
111
  self.steps += 1
114
112
  self.time += 1
115
113
 
116
- def _wrapped_step(self):
117
- """Wrapper for the step method to include time and step updating."""
118
- self._original_step()
119
- self.model._advance_time()
120
-
121
114
  def get_agent_count(self) -> int:
122
115
  """Returns the current number of agents in the queue."""
123
116
  return len(self._agents)
@@ -6,6 +6,7 @@ from matplotlib.figure import Figure
6
6
  from matplotlib.ticker import MaxNLocator
7
7
 
8
8
  import mesa
9
+ from mesa.experimental.cell_space import VoronoiGrid
9
10
 
10
11
 
11
12
  @solara.component
@@ -20,6 +21,8 @@ def SpaceMatplotlib(model, agent_portrayal, dependencies: list[any] | None = Non
20
21
  _draw_network_grid(space, space_ax, agent_portrayal)
21
22
  elif isinstance(space, mesa.space.ContinuousSpace):
22
23
  _draw_continuous_space(space, space_ax, agent_portrayal)
24
+ elif isinstance(space, VoronoiGrid):
25
+ _draw_voronoi(space, space_ax, agent_portrayal)
23
26
  else:
24
27
  _draw_grid(space, space_ax, agent_portrayal)
25
28
  solara.FigureMatplotlib(space_fig, format="png", dependencies=dependencies)
@@ -150,6 +153,56 @@ def _draw_continuous_space(space, space_ax, agent_portrayal):
150
153
  _split_and_scatter(portray(space), space_ax)
151
154
 
152
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
+
153
206
  @solara.component
154
207
  def PlotMatplotlib(model, measure, dependencies: list[any] | None = None):
155
208
  fig = Figure()
@@ -158,7 +158,11 @@ def SolaraViz(
158
158
  """Update the random seed for the model."""
159
159
  reactive_seed.value = model.random.random()
160
160
 
161
- dependencies = [current_step.value, reactive_seed.value]
161
+ dependencies = [
162
+ *list(model_parameters.values()),
163
+ current_step.value,
164
+ reactive_seed.value,
165
+ ]
162
166
 
163
167
  # if space drawer is disabled, do not include it
164
168
  layout_types = [{"Space": "default"}] if space_drawer else []
@@ -240,7 +244,7 @@ def ModelController(model, play_interval, current_step, reset_counter):
240
244
  """Advance the model by one step."""
241
245
  model.step()
242
246
  previous_step.value = current_step.value
243
- current_step.value = model._steps
247
+ current_step.value = model.steps
244
248
 
245
249
  def do_play():
246
250
  """Run the model continuously."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: Mesa
3
- Version: 3.0.0a2
3
+ Version: 3.0.0a3
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
@@ -42,6 +42,9 @@ Requires-Dist: myst-parser; extra == 'docs'
42
42
  Requires-Dist: pydata-sphinx-theme; extra == 'docs'
43
43
  Requires-Dist: seaborn; extra == 'docs'
44
44
  Requires-Dist: sphinx; extra == 'docs'
45
+ Provides-Extra: examples
46
+ Requires-Dist: pytest>=4.6; extra == 'examples'
47
+ Requires-Dist: scipy; extra == 'examples'
45
48
  Description-Content-Type: text/markdown
46
49
 
47
50
  # Mesa: Agent-based modeling in Python
@@ -98,15 +101,18 @@ Or any other (development) branch on this repo or your own fork:
98
101
  pip install -U -e git+https://github.com/YOUR_FORK/mesa@YOUR_BRANCH#egg=mesa
99
102
  ```
100
103
 
104
+ ## Resources
101
105
  For resources or help on using Mesa, check out the following:
102
106
 
103
107
  - [Intro to Mesa Tutorial](http://mesa.readthedocs.org/en/stable/tutorials/intro_tutorial.html) (An introductory model, the Boltzmann
104
108
  Wealth Model, for beginners or those new to Mesa.)
109
+ - [Visualization Tutorial](https://mesa.readthedocs.io/en/stable/tutorials/visualization_tutorial.html) (An introduction into our Solara visualization)
105
110
  - [Complexity Explorer Tutorial](https://www.complexityexplorer.org/courses/172-agent-based-models-with-python-an-introduction-to-mesa) (An advanced-beginner model,
106
111
  SugarScape with Traders, with instructional videos)
107
112
  - [Mesa Examples](https://github.com/projectmesa/mesa-examples/tree/main/examples) (A repository of seminal ABMs using Mesa and
108
113
  examples of employing specific Mesa Features)
109
114
  - [Docs](http://mesa.readthedocs.org/) (Mesa's documentation, API and useful snippets)
115
+ - [Development version docs](https://mesa.readthedocs.io/en/latest/) (the latest version docs if you're using a pre-release Mesa version)
110
116
  - [Discussions](https://github.com/projectmesa/mesa/discussions) (GitHub threaded discussions about Mesa)
111
117
  - [Matrix Chat](https://matrix.to/#/#project-mesa:matrix.org) (Chat Forum via Matrix to talk about Mesa)
112
118
 
@@ -1,11 +1,11 @@
1
- mesa/__init__.py,sha256=MvmhX7S8OWq9FLM52bF7lnSUTTNZegQPsSheI37L2og,618
2
- mesa/agent.py,sha256=xqpjpMHii82nts-gO5DPpcUjujHQoaJB2fKi0VrrWHs,13067
3
- mesa/batchrunner.py,sha256=92MabDDR38XGTZw_IB7nNDNH0PX7zL_jGyZJ2grisaY,6023
4
- mesa/datacollection.py,sha256=CQ2QsW-mkEVbDVTsOkLy8NAQEKeoILdLB0zWS2sxnyk,11444
1
+ mesa/__init__.py,sha256=fHbxauJqwzvD_Y--JHDC7qj52vdMSltmq1EdOf8963E,618
2
+ mesa/agent.py,sha256=0V10JPHfgKmwduPi8eX6CrtJ2ergcT6Lw8uSLn-IcY8,19647
3
+ mesa/batchrunner.py,sha256=iwsfitWFo-m7dm2hNFB8xFOlnXa43URypbxF-zebnws,6019
4
+ mesa/datacollection.py,sha256=WUpZoFC2ZdLtKZ0oTwZTqraoP_yNx_yQY9pxO0TR8y0,11442
5
5
  mesa/main.py,sha256=7MovfNz88VWNnfXP0kcERB6C3GfkVOh0hb0o32hM9LU,1602
6
- mesa/model.py,sha256=Jqj6ob-zuTwAWwYAkbEsoC-bTA7ptoAD9qDvPfIX-80,5761
6
+ mesa/model.py,sha256=kyWzDnnV_RI4Kef_p70Zz5ikxlFD7NeJl1OaS9hjh2w,8049
7
7
  mesa/space.py,sha256=zC96qoNjhLmBXY2ZaEQmF7w30WZAtPpEY4mwcet-Dio,62462
8
- mesa/time.py,sha256=9gNoyUqYkt_gUPFBMhm38pK87mcntwAZ1lJzxqW3BSA,15211
8
+ mesa/time.py,sha256=53VX0x8zujaq32R6w_aBv1NmLpWO_h5K5BQTaK4mO3Q,14960
9
9
  mesa/cookiecutter-mesa/cookiecutter.json,sha256=tBSWli39fOWUXGfiDCTKd92M7uKaBIswXbkOdbUufYY,337
10
10
  mesa/cookiecutter-mesa/hooks/post_gen_project.py,sha256=8JoXZKIioRYEWJURC0udj8WS3rg0c4So62sOZSGbrMY,294
11
11
  mesa/cookiecutter-mesa/{{cookiecutter.snake}}/README.md,sha256=Yji4lGY-NtQSnW-oBj0_Jhs-XhCfZA8R1mBBM_IllGs,80
@@ -14,13 +14,14 @@ mesa/cookiecutter-mesa/{{cookiecutter.snake}}/setup.pytemplate,sha256=UtRpLM_Cke
14
14
  mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
15
  mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/model.pytemplate,sha256=Aml4Z6E1yj7E7DtHNSUqnKNRUdkxG9WWtJyW8fkxCng,1870
16
16
  mesa/experimental/__init__.py,sha256=MaSRE9cTFIWwMZsbRKfnCiCBkhvtzJdgWlg3Dls7Unw,67
17
- mesa/experimental/cell_space/__init__.py,sha256=trFVKf2l5RbkCUyxP09Kox_J3ak2YdM4o3t40Tsjjm4,628
17
+ mesa/experimental/cell_space/__init__.py,sha256=gXntv_Ie13SIegBumJNaIPcTUmTQLb4LlGd1JxoIn9M,708
18
18
  mesa/experimental/cell_space/cell.py,sha256=AUnvVnXWhdgzr0bLKDRDO9c93v22Zkw6W-tWxhEhGdQ,4578
19
19
  mesa/experimental/cell_space/cell_agent.py,sha256=G4u9ht4gW9ns1y2L7pFumF3K4HiP6ROuxwrxHZ-mL1M,1107
20
20
  mesa/experimental/cell_space/cell_collection.py,sha256=4FmfDEg9LoFiJ0mF_nC8KUt9fCJ7Q21erjWPeBTQ_lw,2293
21
21
  mesa/experimental/cell_space/discrete_space.py,sha256=ta__YojsrrhWL4DgMzUqZpSgbeexKMrA6bxlYPJGfK0,1921
22
22
  mesa/experimental/cell_space/grid.py,sha256=gYDExuFBMF3OThUkhbXmolQFKBOqTukcibjfgXicP00,6948
23
23
  mesa/experimental/cell_space/network.py,sha256=mAaFHBdd4s9kxUWHbViovLW2-pU2yXH0dtY_vF8sCJg,1179
24
+ mesa/experimental/cell_space/voronoi.py,sha256=swwfV1Hfi7wp3XfM-rb9Lq5H0yAPH9zohZnXzU8SiHM,9997
24
25
  mesa/experimental/devs/__init__.py,sha256=CWam15vCj-RD_biMyqv4sJfos1fsL823P7MDEGrbwW8,174
25
26
  mesa/experimental/devs/eventlist.py,sha256=nyUFNDWnnSPQnrMtj7Qj1PexxKyOwSJuIGBoxtSwVI0,5269
26
27
  mesa/experimental/devs/simulator.py,sha256=0SMC7daIOyL2rYfoQOOTaTOYDos0gLeBUbU1Krd42HA,9557
@@ -28,11 +29,11 @@ mesa/experimental/devs/examples/epstein_civil_violence.py,sha256=KqH9KI-A_BYt7oW
28
29
  mesa/experimental/devs/examples/wolf_sheep.py,sha256=h5z-eDqMpYeOjrq293N2BcQbs_LDVsgtg9vblXJM7XQ,7697
29
30
  mesa/visualization/UserParam.py,sha256=WgnY3Q0padtGqUCaezgYzd6cZ7LziuIQnGKP3DBuHZY,1641
30
31
  mesa/visualization/__init__.py,sha256=zsAzEY3-0O9CZUfiUL6p8zCR1mvvL5Sai2WzoiQ2pmY,127
31
- mesa/visualization/solara_viz.py,sha256=GuhDFcsHp6rSjaYMM7lgB-sWJGfEqJrYnmMxncnZF8A,15192
32
+ mesa/visualization/solara_viz.py,sha256=hT-w4N32x5HUkk7AVYN6Ps_5JWrcaQWiyIo5ZyvYoJA,15256
32
33
  mesa/visualization/components/altair.py,sha256=V2CQ-Zr7PeijgWtYBNH3VklGVfrf1ee70XVh0DBBONQ,2366
33
- mesa/visualization/components/matplotlib.py,sha256=gUTteu_VEA4J2je760aq_esYto0Uj1DZP9gjev8TlcU,5851
34
- mesa-3.0.0a2.dist-info/METADATA,sha256=zAMPJhtCHtWiXvm6dl5iCqU048qhtZpBkGsKO6RtaEM,7862
35
- mesa-3.0.0a2.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
36
- mesa-3.0.0a2.dist-info/entry_points.txt,sha256=IOcQtetGF8l4wHpOs_hGb19Rz-FS__BMXOJR10IBPsA,39
37
- mesa-3.0.0a2.dist-info/licenses/LICENSE,sha256=OGUgret9fRrm8J3pdsPXETIjf0H8puK_Nmy970ZzT78,572
38
- mesa-3.0.0a2.dist-info/RECORD,,
34
+ mesa/visualization/components/matplotlib.py,sha256=J61gHkXd37XyZiNLGF1NaQPOYqDdh9tpSfbOzMqK3dI,7570
35
+ mesa-3.0.0a3.dist-info/METADATA,sha256=r4qETS44Ho4RD37R5yxmUDeTE0LZXfQHrAmT2hRNioI,8288
36
+ mesa-3.0.0a3.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
37
+ mesa-3.0.0a3.dist-info/entry_points.txt,sha256=IOcQtetGF8l4wHpOs_hGb19Rz-FS__BMXOJR10IBPsA,39
38
+ mesa-3.0.0a3.dist-info/licenses/LICENSE,sha256=OGUgret9fRrm8J3pdsPXETIjf0H8puK_Nmy970ZzT78,572
39
+ mesa-3.0.0a3.dist-info/RECORD,,
File without changes