Mesa 3.0.0a1__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.0a1"
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
@@ -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."""
@@ -100,8 +87,6 @@ class AgentSet(MutableSet, Sequence):
100
87
  which means that agents not referenced elsewhere in the program may be automatically removed from the AgentSet.
101
88
  """
102
89
 
103
- agentset_experimental_warning_given = False
104
-
105
90
  def __init__(self, agents: Iterable[Agent], model: Model):
106
91
  """
107
92
  Initializes the AgentSet with a collection of agents and a reference to the model.
@@ -129,9 +114,10 @@ class AgentSet(MutableSet, Sequence):
129
114
  def select(
130
115
  self,
131
116
  filter_func: Callable[[Agent], bool] | None = None,
132
- n: int = 0,
117
+ at_most: int | float = float("inf"),
133
118
  inplace: bool = False,
134
119
  agent_type: type[Agent] | None = None,
120
+ n: int | None = None,
135
121
  ) -> AgentSet:
136
122
  """
137
123
  Select a subset of agents from the AgentSet based on a filter function and/or quantity limit.
@@ -139,29 +125,47 @@ class AgentSet(MutableSet, Sequence):
139
125
  Args:
140
126
  filter_func (Callable[[Agent], bool], optional): A function that takes an Agent and returns True if the
141
127
  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.
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.
143
131
  inplace (bool, optional): If True, modifies the current AgentSet; otherwise, returns a new AgentSet. Defaults to False.
144
132
  agent_type (type[Agent], optional): The class type of the agents to select. Defaults to None, meaning no type filtering is applied.
145
133
 
146
134
  Returns:
147
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.
148
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
149
148
 
150
- 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:
151
151
  return self if inplace else copy.copy(self)
152
152
 
153
- 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):
154
158
  count = 0
155
159
  for agent in self:
160
+ if count >= at_most:
161
+ break
156
162
  if (not filter_func or filter_func(agent)) and (
157
163
  not agent_type or isinstance(agent, agent_type)
158
164
  ):
159
165
  yield agent
160
166
  count += 1
161
- if 0 < n <= count:
162
- break
163
167
 
164
- agents = agent_generator(filter_func, agent_type, n)
168
+ agents = agent_generator(filter_func, agent_type, at_most)
165
169
 
166
170
  return AgentSet(agents, self.model) if not inplace else self._update(agents)
167
171
 
@@ -226,29 +230,79 @@ class AgentSet(MutableSet, Sequence):
226
230
  self._agents = weakref.WeakKeyDictionary({agent: None for agent in agents})
227
231
  return self
228
232
 
229
- def do(
230
- self, method_name: str, *args, return_results: bool = False, **kwargs
231
- ) -> AgentSet | list[Any]:
233
+ def do(self, method: str | Callable, *args, **kwargs) -> AgentSet:
232
234
  """
233
- Invoke a method on each agent in the AgentSet.
235
+ Invoke a method or function on each agent in the AgentSet.
234
236
 
235
237
  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.
238
+ method (str, callable): the callable to do on each agent
239
+
240
+ * in case of str, the name of the method to call on each agent.
241
+ * in case of callable, the function to be called with each agent as first argument
242
+
243
+ *args: Variable length argument list passed to the callable being called.
244
+ **kwargs: Arbitrary keyword arguments passed to the callable being called.
240
245
 
241
246
  Returns:
242
- AgentSet | list[Any]: The results of the method calls if return_results is True, otherwise the AgentSet itself.
247
+ AgentSet | list[Any]: The results of the callable calls if return_results is True, otherwise the AgentSet itself.
243
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
+
244
263
  # 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
- ]
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)
250
272
 
251
- return res if return_results else self
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
+ """
291
+ # we iterate over the actual weakref keys and check if weakref is alive before calling the method
292
+ if isinstance(method, str):
293
+ res = [
294
+ getattr(agent, method)(*args, **kwargs)
295
+ for agentref in self._agents.keyrefs()
296
+ if (agent := agentref()) is not None
297
+ ]
298
+ else:
299
+ res = [
300
+ method(agent, *args, **kwargs)
301
+ for agentref in self._agents.keyrefs()
302
+ if (agent := agentref()) is not None
303
+ ]
304
+
305
+ return res
252
306
 
253
307
  def get(self, attr_names: str | list[str]) -> list[Any]:
254
308
  """
@@ -274,6 +328,21 @@ class AgentSet(MutableSet, Sequence):
274
328
  for agent in self._agents
275
329
  ]
276
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
+
277
346
  def __getitem__(self, item: int | slice) -> Agent:
278
347
  """
279
348
  Retrieve an agent or a slice of agents from the AgentSet.
@@ -356,7 +425,116 @@ class AgentSet(MutableSet, Sequence):
356
425
  """
357
426
  return self.model.random
358
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())
359
538
 
360
- # consider adding for performance reasons
361
- # for Sequence: __reversed__, index, and count
362
- # 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
@@ -155,6 +155,17 @@ class EventList:
155
155
  def __len__(self) -> int:
156
156
  return len(self._events)
157
157
 
158
+ def __repr__(self) -> str:
159
+ """Return a string representation of the event list"""
160
+ events_str = ", ".join(
161
+ [
162
+ f"Event(time={e.time}, priority={e.priority}, id={e.unique_id})"
163
+ for e in self._events
164
+ if not e.CANCELED
165
+ ]
166
+ )
167
+ return f"EventList([{events_str}])"
168
+
158
169
  def remove(self, event: SimulationEvent) -> None:
159
170
  """remove an event from the event list"""
160
171
  # we cannot simply remove items from _eventlist because this breaks
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
@@ -19,8 +17,6 @@ from typing import Any
19
17
  from mesa.agent import Agent, AgentSet
20
18
  from mesa.datacollection import DataCollector
21
19
 
22
- TimeT = float | int
23
-
24
20
 
25
21
  class Model:
26
22
  """Base class for models in the Mesa ABM library.
@@ -33,12 +29,12 @@ class Model:
33
29
  running: A boolean indicating if the model should continue running.
34
30
  schedule: An object to manage the order and execution of agent steps.
35
31
  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
32
 
39
33
  Properties:
40
- agents: An AgentSet containing all agents in the model, generated from the _agents attribute.
34
+ agents: An AgentSet containing all agents in the model
41
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.
42
38
 
43
39
  Methods:
44
40
  get_agents_of_type: Returns an AgentSet of agents of the specified type.
@@ -47,6 +43,14 @@ class Model:
47
43
  next_id: Generates and returns the next unique identifier for an agent.
48
44
  reset_randomizer: Resets the model's random number generator with a new or existing seed.
49
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
+
50
54
  """
51
55
 
52
56
  def __new__(cls, *args: Any, **kwargs: Any) -> Any:
@@ -58,9 +62,6 @@ class Model:
58
62
  # advance.
59
63
  obj._seed = random.random()
60
64
  obj.random = random.Random(obj._seed)
61
- # TODO: Remove these 2 lines just before Mesa 3.0
62
- obj._steps = 0
63
- obj._time = 0
64
65
  return obj
65
66
 
66
67
  def __init__(self, *args: Any, **kwargs: Any) -> None:
@@ -72,41 +73,107 @@ class Model:
72
73
  self.running = True
73
74
  self.schedule = None
74
75
  self.current_id = 0
75
- self.agents_: defaultdict[type, dict] = defaultdict(dict)
76
+ self.steps: int = 0
77
+
78
+ self._setup_agent_registration()
76
79
 
77
- self._steps: int = 0
78
- 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)
79
90
 
80
91
  @property
81
92
  def agents(self) -> AgentSet:
82
93
  """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)
94
+ return self._all_agents
89
95
 
90
96
  @agents.setter
91
97
  def agents(self, agents: Any) -> None:
92
- 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
- UserWarning,
97
- stacklevel=2,
98
+ raise AttributeError(
99
+ "You are trying to set model.agents. In Mesa 3.0 and higher, this attribute is "
100
+ "used by Mesa itself, so you cannot use it directly anymore."
101
+ "Please adjust your code to use a different attribute name for custom agent storage."
98
102
  )
99
103
 
100
- self._agents = agents
101
-
102
104
  @property
103
105
  def agent_types(self) -> list[type]:
104
- """Return a list of different agent types."""
105
- 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())
106
108
 
107
109
  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)
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)
110
177
 
111
178
  def run_model(self) -> None:
112
179
  """Run the model until the end condition is reached. Overload as
@@ -118,11 +185,6 @@ class Model:
118
185
  def step(self) -> None:
119
186
  """A single step. Fill in here."""
120
187
 
121
- def _advance_time(self, deltat: TimeT = 1):
122
- """Increment the model's steps counter and clock."""
123
- self._steps += 1
124
- self._time += deltat
125
-
126
188
  def next_id(self) -> int:
127
189
  """Return the next unique ID for agents, increment current_id"""
128
190
  self.current_id += 1
mesa/space.py CHANGED
@@ -586,7 +586,7 @@ class PropertyLayer:
586
586
  aggregate_property(operation): Performs an aggregate operation over all cells.
587
587
  """
588
588
 
589
- agentset_experimental_warning_given = False
589
+ propertylayer_experimental_warning_given = False
590
590
 
591
591
  def __init__(
592
592
  self, name: str, width: int, height: int, default_value, dtype=np.float64
@@ -633,14 +633,14 @@ class PropertyLayer:
633
633
 
634
634
  self.data = np.full((width, height), default_value, dtype=dtype)
635
635
 
636
- if not self.__class__.agentset_experimental_warning_given:
636
+ if not self.__class__.propertylayer_experimental_warning_given:
637
637
  warnings.warn(
638
638
  "The new PropertyLayer and _PropertyGrid classes experimental. It may be changed or removed in any and all future releases, including patch releases.\n"
639
639
  "We would love to hear what you think about this new feature. If you have any thoughts, share them with us here: https://github.com/projectmesa/mesa/discussions/1932",
640
640
  FutureWarning,
641
641
  stacklevel=2,
642
642
  )
643
- self.__class__.agentset_experimental_warning_given = True
643
+ self.__class__.propertylayer_experimental_warning_given = True
644
644
 
645
645
  def set_cell(self, position: Coordinate, value):
646
646
  """
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)
@@ -1,9 +1,12 @@
1
+ from collections import defaultdict
2
+
1
3
  import networkx as nx
2
4
  import solara
3
5
  from matplotlib.figure import Figure
4
6
  from matplotlib.ticker import MaxNLocator
5
7
 
6
8
  import mesa
9
+ from mesa.experimental.cell_space import VoronoiGrid
7
10
 
8
11
 
9
12
  @solara.component
@@ -18,17 +21,51 @@ def SpaceMatplotlib(model, agent_portrayal, dependencies: list[any] | None = Non
18
21
  _draw_network_grid(space, space_ax, agent_portrayal)
19
22
  elif isinstance(space, mesa.space.ContinuousSpace):
20
23
  _draw_continuous_space(space, space_ax, agent_portrayal)
24
+ elif isinstance(space, VoronoiGrid):
25
+ _draw_voronoi(space, space_ax, agent_portrayal)
21
26
  else:
22
27
  _draw_grid(space, space_ax, agent_portrayal)
23
28
  solara.FigureMatplotlib(space_fig, format="png", dependencies=dependencies)
24
29
 
25
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
+
26
62
  def _draw_grid(space, space_ax, agent_portrayal):
27
63
  def portray(g):
28
64
  x = []
29
65
  y = []
30
66
  s = [] # size
31
67
  c = [] # color
68
+ m = [] # shape
32
69
  for i in range(g.width):
33
70
  for j in range(g.height):
34
71
  content = g._grid[i][j]
@@ -41,23 +78,23 @@ def _draw_grid(space, space_ax, agent_portrayal):
41
78
  data = agent_portrayal(agent)
42
79
  x.append(i)
43
80
  y.append(j)
44
- if "size" in data:
45
- s.append(data["size"])
46
- if "color" in data:
47
- c.append(data["color"])
48
- out = {"x": x, "y": y}
49
- # This is the default value for the marker size, which auto-scales
50
- # according to the grid area.
51
- out["s"] = (180 / max(g.width, g.height)) ** 2
52
- if len(s) > 0:
53
- out["s"] = s
54
- if len(c) > 0:
55
- out["c"] = c
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}
56
93
  return out
57
94
 
58
95
  space_ax.set_xlim(-1, space.width)
59
96
  space_ax.set_ylim(-1, space.height)
60
- space_ax.scatter(**portray(space))
97
+ _split_and_scatter(portray(space), space_ax)
61
98
 
62
99
 
63
100
  def _draw_network_grid(space, space_ax, agent_portrayal):
@@ -77,20 +114,23 @@ def _draw_continuous_space(space, space_ax, agent_portrayal):
77
114
  y = []
78
115
  s = [] # size
79
116
  c = [] # color
117
+ m = [] # shape
80
118
  for agent in space._agent_to_index:
81
119
  data = agent_portrayal(agent)
82
120
  _x, _y = agent.pos
83
121
  x.append(_x)
84
122
  y.append(_y)
85
- if "size" in data:
86
- s.append(data["size"])
87
- if "color" in data:
88
- c.append(data["color"])
89
- out = {"x": x, "y": y}
90
- if len(s) > 0:
91
- out["s"] = s
92
- if len(c) > 0:
93
- out["c"] = c
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}
94
134
  return out
95
135
 
96
136
  # Determine border style based on space.torus
@@ -110,8 +150,58 @@ def _draw_continuous_space(space, space_ax, agent_portrayal):
110
150
  space_ax.set_ylim(space.y_min - y_padding, space.y_max + y_padding)
111
151
 
112
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)
113
194
  space_ax.scatter(**portray(space))
114
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
+
115
205
 
116
206
  @solara.component
117
207
  def PlotMatplotlib(model, measure, dependencies: list[any] | None = None):
@@ -70,7 +70,7 @@ def Card(
70
70
  )
71
71
  elif space_drawer:
72
72
  # if specified, draw agent space with an alternate renderer
73
- space_drawer(model, agent_portrayal)
73
+ space_drawer(model, agent_portrayal, dependencies=dependencies)
74
74
  elif "Measure" in layout_type:
75
75
  rv.CardTitle(children=["Measure"])
76
76
  measure = measures[layout_type["Measure"]]
@@ -103,7 +103,8 @@ def SolaraViz(
103
103
  model_params: Parameters for initializing the model
104
104
  measures: List of callables or data attributes to plot
105
105
  name: Name for display
106
- agent_portrayal: Options for rendering agents (dictionary)
106
+ agent_portrayal: Options for rendering agents (dictionary);
107
+ Default drawer supports custom `"size"`, `"color"`, and `"shape"`.
107
108
  space_drawer: Method to render the agent space for
108
109
  the model; default implementation is the `SpaceMatplotlib` component;
109
110
  simulations with no space to visualize should
@@ -157,7 +158,11 @@ def SolaraViz(
157
158
  """Update the random seed for the model."""
158
159
  reactive_seed.value = model.random.random()
159
160
 
160
- dependencies = [current_step.value, reactive_seed.value]
161
+ dependencies = [
162
+ *list(model_parameters.values()),
163
+ current_step.value,
164
+ reactive_seed.value,
165
+ ]
161
166
 
162
167
  # if space drawer is disabled, do not include it
163
168
  layout_types = [{"Space": "default"}] if space_drawer else []
@@ -239,7 +244,7 @@ def ModelController(model, play_interval, current_step, reset_counter):
239
244
  """Advance the model by one step."""
240
245
  model.step()
241
246
  previous_step.value = current_step.value
242
- current_step.value = model._steps
247
+ current_step.value = model.steps
243
248
 
244
249
  def do_play():
245
250
  """Run the model continuously."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: Mesa
3
- Version: 3.0.0a1
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
@@ -30,7 +30,6 @@ Requires-Dist: pandas
30
30
  Requires-Dist: solara
31
31
  Requires-Dist: tqdm
32
32
  Provides-Extra: dev
33
- Requires-Dist: coverage; extra == 'dev'
34
33
  Requires-Dist: pytest-cov; extra == 'dev'
35
34
  Requires-Dist: pytest-mock; extra == 'dev'
36
35
  Requires-Dist: pytest>=4.6; extra == 'dev'
@@ -43,6 +42,9 @@ Requires-Dist: myst-parser; extra == 'docs'
43
42
  Requires-Dist: pydata-sphinx-theme; extra == 'docs'
44
43
  Requires-Dist: seaborn; extra == 'docs'
45
44
  Requires-Dist: sphinx; extra == 'docs'
45
+ Provides-Extra: examples
46
+ Requires-Dist: pytest>=4.6; extra == 'examples'
47
+ Requires-Dist: scipy; extra == 'examples'
46
48
  Description-Content-Type: text/markdown
47
49
 
48
50
  # Mesa: Agent-based modeling in Python
@@ -75,13 +77,19 @@ can be displayed in browser windows or Jupyter.*
75
77
 
76
78
  ## Using Mesa
77
79
 
78
- Getting started quickly:
80
+ To install our latest stable release (2.3.x), run:
79
81
 
80
82
  ``` bash
81
- pip install mesa
83
+ pip install -U mesa
82
84
  ```
83
85
 
84
- You can also use `pip` to install the github version:
86
+ To install our latest pre-release (3.0.0 alpha), run:
87
+
88
+ ``` bash
89
+ pip install -U --pre mesa
90
+ ```
91
+
92
+ You can also use `pip` to install the latest GitHub version:
85
93
 
86
94
  ``` bash
87
95
  pip install -U -e git+https://github.com/projectmesa/mesa@main#egg=mesa
@@ -93,15 +101,18 @@ Or any other (development) branch on this repo or your own fork:
93
101
  pip install -U -e git+https://github.com/YOUR_FORK/mesa@YOUR_BRANCH#egg=mesa
94
102
  ```
95
103
 
104
+ ## Resources
96
105
  For resources or help on using Mesa, check out the following:
97
106
 
98
107
  - [Intro to Mesa Tutorial](http://mesa.readthedocs.org/en/stable/tutorials/intro_tutorial.html) (An introductory model, the Boltzmann
99
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)
100
110
  - [Complexity Explorer Tutorial](https://www.complexityexplorer.org/courses/172-agent-based-models-with-python-an-introduction-to-mesa) (An advanced-beginner model,
101
111
  SugarScape with Traders, with instructional videos)
102
112
  - [Mesa Examples](https://github.com/projectmesa/mesa-examples/tree/main/examples) (A repository of seminal ABMs using Mesa and
103
113
  examples of employing specific Mesa Features)
104
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)
105
116
  - [Discussions](https://github.com/projectmesa/mesa/discussions) (GitHub threaded discussions about Mesa)
106
117
  - [Matrix Chat](https://matrix.to/#/#project-mesa:matrix.org) (Chat Forum via Matrix to talk about Mesa)
107
118
 
@@ -1,11 +1,11 @@
1
- mesa/__init__.py,sha256=XNwJOFa_LglQJTMbYPWHXvartntKeOrSn9DGSbXj1rc,618
2
- mesa/agent.py,sha256=fx_h8RnX5DJCmfJtloIb_fprXXp8bFzC3_RnLOLlOvY,12902
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=GqayRWhohSS96kMwHCNGI7XvEkwI8GHS2SRL6SZ9N5E,5810
7
- mesa/space.py,sha256=9eDEUQBcck8QYWvRn3fDw2zS2bO1Yjc7VjvvrMikzPE,62447
8
- mesa/time.py,sha256=9gNoyUqYkt_gUPFBMhm38pK87mcntwAZ1lJzxqW3BSA,15211
6
+ mesa/model.py,sha256=kyWzDnnV_RI4Kef_p70Zz5ikxlFD7NeJl1OaS9hjh2w,8049
7
+ mesa/space.py,sha256=zC96qoNjhLmBXY2ZaEQmF7w30WZAtPpEY4mwcet-Dio,62462
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,25 +14,26 @@ 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
- mesa/experimental/devs/eventlist.py,sha256=AM-gpivXQ889Ewt66T_ai6Yy6ldx0G69Unu1lasSNxI,4907
26
+ mesa/experimental/devs/eventlist.py,sha256=nyUFNDWnnSPQnrMtj7Qj1PexxKyOwSJuIGBoxtSwVI0,5269
26
27
  mesa/experimental/devs/simulator.py,sha256=0SMC7daIOyL2rYfoQOOTaTOYDos0gLeBUbU1Krd42HA,9557
27
28
  mesa/experimental/devs/examples/epstein_civil_violence.py,sha256=KqH9KI-A_BYt7oWi9kaOhTzjrf2pETqzSpAQG8ewud0,9667
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=POus4i1k2Z8fJpEXiXQvGupRsrRLRiG5qndwkaEQ53Y,15085
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=lB9QKo6i_mI2iKCksyakOStqY8I6B3sv8SXcpmPgWEc,4289
34
- mesa-3.0.0a1.dist-info/METADATA,sha256=QTL6KViiX07VnrkXi5hqG0nYYN_hyZaWo3_SckVvbIA,7771
35
- mesa-3.0.0a1.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
36
- mesa-3.0.0a1.dist-info/entry_points.txt,sha256=IOcQtetGF8l4wHpOs_hGb19Rz-FS__BMXOJR10IBPsA,39
37
- mesa-3.0.0a1.dist-info/licenses/LICENSE,sha256=OGUgret9fRrm8J3pdsPXETIjf0H8puK_Nmy970ZzT78,572
38
- mesa-3.0.0a1.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