Mesa 2.4.0__py3-none-any.whl → 3.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of Mesa might be problematic. Click here for more details.

Files changed (110) hide show
  1. mesa/__init__.py +3 -5
  2. mesa/agent.py +105 -92
  3. mesa/batchrunner.py +55 -31
  4. mesa/datacollection.py +10 -14
  5. mesa/examples/README.md +37 -0
  6. mesa/examples/__init__.py +21 -0
  7. mesa/examples/advanced/epstein_civil_violence/Epstein Civil Violence.ipynb +116 -0
  8. mesa/examples/advanced/epstein_civil_violence/Readme.md +34 -0
  9. mesa/examples/advanced/epstein_civil_violence/__init__.py +0 -0
  10. mesa/examples/advanced/epstein_civil_violence/agents.py +164 -0
  11. mesa/examples/advanced/epstein_civil_violence/app.py +73 -0
  12. mesa/examples/advanced/epstein_civil_violence/model.py +114 -0
  13. mesa/examples/advanced/pd_grid/Readme.md +43 -0
  14. mesa/examples/advanced/pd_grid/__init__.py +0 -0
  15. mesa/examples/advanced/pd_grid/agents.py +50 -0
  16. mesa/examples/advanced/pd_grid/analysis.ipynb +228 -0
  17. mesa/examples/advanced/pd_grid/app.py +54 -0
  18. mesa/examples/advanced/pd_grid/model.py +71 -0
  19. mesa/examples/advanced/sugarscape_g1mt/Readme.md +64 -0
  20. mesa/examples/advanced/sugarscape_g1mt/__init__.py +0 -0
  21. mesa/examples/advanced/sugarscape_g1mt/agents.py +344 -0
  22. mesa/examples/advanced/sugarscape_g1mt/app.py +62 -0
  23. mesa/examples/advanced/sugarscape_g1mt/model.py +180 -0
  24. mesa/examples/advanced/sugarscape_g1mt/sugar-map.txt +50 -0
  25. mesa/examples/advanced/sugarscape_g1mt/tests.py +69 -0
  26. mesa/examples/advanced/wolf_sheep/Readme.md +57 -0
  27. mesa/examples/advanced/wolf_sheep/__init__.py +0 -0
  28. mesa/examples/advanced/wolf_sheep/agents.py +102 -0
  29. mesa/examples/advanced/wolf_sheep/app.py +84 -0
  30. mesa/examples/advanced/wolf_sheep/model.py +137 -0
  31. mesa/examples/basic/__init__.py +0 -0
  32. mesa/examples/basic/boid_flockers/Readme.md +22 -0
  33. mesa/examples/basic/boid_flockers/__init__.py +0 -0
  34. mesa/examples/basic/boid_flockers/agents.py +71 -0
  35. mesa/examples/basic/boid_flockers/app.py +58 -0
  36. mesa/examples/basic/boid_flockers/model.py +69 -0
  37. mesa/examples/basic/boltzmann_wealth_model/Readme.md +56 -0
  38. mesa/examples/basic/boltzmann_wealth_model/__init__.py +0 -0
  39. mesa/examples/basic/boltzmann_wealth_model/agents.py +31 -0
  40. mesa/examples/basic/boltzmann_wealth_model/app.py +74 -0
  41. mesa/examples/basic/boltzmann_wealth_model/model.py +43 -0
  42. mesa/examples/basic/boltzmann_wealth_model/st_app.py +115 -0
  43. mesa/examples/basic/conways_game_of_life/Readme.md +39 -0
  44. mesa/examples/basic/conways_game_of_life/__init__.py +0 -0
  45. mesa/examples/basic/conways_game_of_life/agents.py +47 -0
  46. mesa/examples/basic/conways_game_of_life/app.py +51 -0
  47. mesa/examples/basic/conways_game_of_life/model.py +31 -0
  48. mesa/examples/basic/conways_game_of_life/st_app.py +72 -0
  49. mesa/examples/basic/schelling/Readme.md +40 -0
  50. mesa/examples/basic/schelling/__init__.py +0 -0
  51. mesa/examples/basic/schelling/agents.py +26 -0
  52. mesa/examples/basic/schelling/analysis.ipynb +205 -0
  53. mesa/examples/basic/schelling/app.py +42 -0
  54. mesa/examples/basic/schelling/model.py +59 -0
  55. mesa/examples/basic/virus_on_network/Readme.md +61 -0
  56. mesa/examples/basic/virus_on_network/__init__.py +0 -0
  57. mesa/examples/basic/virus_on_network/agents.py +69 -0
  58. mesa/examples/basic/virus_on_network/app.py +114 -0
  59. mesa/examples/basic/virus_on_network/model.py +96 -0
  60. mesa/experimental/UserParam.py +18 -7
  61. mesa/experimental/__init__.py +10 -2
  62. mesa/experimental/cell_space/__init__.py +16 -1
  63. mesa/experimental/cell_space/cell.py +93 -23
  64. mesa/experimental/cell_space/cell_agent.py +117 -21
  65. mesa/experimental/cell_space/cell_collection.py +56 -19
  66. mesa/experimental/cell_space/discrete_space.py +92 -8
  67. mesa/experimental/cell_space/grid.py +33 -9
  68. mesa/experimental/cell_space/network.py +15 -10
  69. mesa/experimental/cell_space/voronoi.py +257 -0
  70. mesa/experimental/components/altair.py +11 -2
  71. mesa/experimental/components/matplotlib.py +132 -26
  72. mesa/experimental/devs/__init__.py +2 -0
  73. mesa/experimental/devs/eventlist.py +54 -15
  74. mesa/experimental/devs/examples/epstein_civil_violence.py +69 -38
  75. mesa/experimental/devs/examples/wolf_sheep.py +42 -43
  76. mesa/experimental/devs/simulator.py +57 -16
  77. mesa/experimental/{jupyter_viz.py → solara_viz.py} +151 -99
  78. mesa/model.py +136 -78
  79. mesa/space.py +208 -148
  80. mesa/time.py +63 -80
  81. mesa/visualization/__init__.py +25 -6
  82. mesa/visualization/components/__init__.py +83 -0
  83. mesa/visualization/components/altair_components.py +188 -0
  84. mesa/visualization/components/matplotlib_components.py +175 -0
  85. mesa/visualization/mpl_space_drawing.py +593 -0
  86. mesa/visualization/solara_viz.py +458 -0
  87. mesa/visualization/user_param.py +69 -0
  88. mesa/visualization/utils.py +9 -0
  89. {mesa-2.4.0.dist-info → mesa-3.0.0.dist-info}/METADATA +62 -17
  90. mesa-3.0.0.dist-info/RECORD +95 -0
  91. mesa-3.0.0.dist-info/licenses/LICENSE +202 -0
  92. mesa-2.4.0.dist-info/licenses/LICENSE → mesa-3.0.0.dist-info/licenses/NOTICE +2 -2
  93. mesa/cookiecutter-mesa/cookiecutter.json +0 -8
  94. mesa/cookiecutter-mesa/hooks/post_gen_project.py +0 -11
  95. mesa/cookiecutter-mesa/{{cookiecutter.snake}}/README.md +0 -4
  96. mesa/cookiecutter-mesa/{{cookiecutter.snake}}/run.pytemplate +0 -3
  97. mesa/cookiecutter-mesa/{{cookiecutter.snake}}/setup.pytemplate +0 -11
  98. mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/model.pytemplate +0 -60
  99. mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/server.pytemplate +0 -36
  100. mesa/flat/__init__.py +0 -6
  101. mesa/flat/visualization.py +0 -5
  102. mesa/main.py +0 -63
  103. mesa/visualization/ModularVisualization.py +0 -1
  104. mesa/visualization/TextVisualization.py +0 -1
  105. mesa/visualization/UserParam.py +0 -1
  106. mesa/visualization/modules.py +0 -1
  107. mesa-2.4.0.dist-info/RECORD +0 -45
  108. /mesa/{cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}} → examples/advanced}/__init__.py +0 -0
  109. {mesa-2.4.0.dist-info → mesa-3.0.0.dist-info}/WHEEL +0 -0
  110. {mesa-2.4.0.dist-info → mesa-3.0.0.dist-info}/entry_points.txt +0 -0
mesa/space.py CHANGED
@@ -1,16 +1,24 @@
1
- """
2
- Mesa Space Module
3
- =================
1
+ """Mesa Space Module.
4
2
 
5
3
  Objects used to add a spatial component to a model.
6
4
 
7
- Grid: base grid, which creates a rectangular grid.
8
- SingleGrid: extension to Grid which strictly enforces one agent per cell.
9
- MultiGrid: extension to Grid where each cell can contain a set of agents.
10
- HexGrid: extension to Grid to handle hexagonal neighbors.
11
- ContinuousSpace: a two-dimensional space where each agent has an arbitrary
12
- position of `float`'s.
13
- NetworkGrid: a network where each node contains zero or more agents.
5
+ .. note::
6
+ All Grid classes (:class:`_Grid`, :class:`SingleGrid`, :class:`MultiGrid`,
7
+ :class:`HexGrid`, etc.) are now in maintenance-only mode. While these classes remain
8
+ fully supported, new development occurs in the experimental cell space module
9
+ (:mod:`mesa.experimental.cell_space`).
10
+
11
+ The :class:`PropertyLayer` and :class:`ContinuousSpace` classes remain fully supported
12
+ and actively developed.
13
+
14
+ Classes
15
+ -------
16
+ * PropertyLayer: A data layer that can be added to Grids to store cell properties
17
+ * SingleGrid: a Grid which strictly enforces one agent per cell.
18
+ * MultiGrid: a Grid where each cell can contain a set of agents.
19
+ * HexGrid: a Grid to handle hexagonal neighbors.
20
+ * ContinuousSpace: a two-dimensional space where each agent has an arbitrary position of `float`'s.
21
+ * NetworkGrid: a network where each node contains zero or more agents.
14
22
  """
15
23
 
16
24
  # Mypy; for the `|` operator purpose
@@ -23,9 +31,9 @@ import inspect
23
31
  import itertools
24
32
  import math
25
33
  import warnings
26
- from collections.abc import Iterable, Iterator, Sequence
34
+ from collections.abc import Callable, Iterable, Iterator, Sequence
27
35
  from numbers import Real
28
- from typing import Any, Callable, TypeVar, Union, cast, overload
36
+ from typing import Any, TypeVar, cast, overload
29
37
  from warnings import warn
30
38
 
31
39
  with contextlib.suppress(ImportError):
@@ -35,28 +43,29 @@ import numpy as np
35
43
  import numpy.typing as npt
36
44
 
37
45
  # For Mypy
38
- from .agent import Agent
46
+ from .agent import Agent, AgentSet
39
47
 
40
48
  # for better performance, we calculate the tuple to use in the is_integer function
41
49
  _types_integer = (int, np.integer)
42
50
 
43
51
  Coordinate = tuple[int, int]
44
52
  # used in ContinuousSpace
45
- FloatCoordinate = Union[tuple[float, float], npt.NDArray[float]]
53
+ FloatCoordinate = tuple[float, float] | npt.NDArray[float]
46
54
  NetworkCoordinate = int
47
55
 
48
- Position = Union[Coordinate, FloatCoordinate, NetworkCoordinate]
56
+ Position = Coordinate | FloatCoordinate | NetworkCoordinate
49
57
 
50
- GridContent = Union[Agent, None]
58
+ GridContent = Agent | None
51
59
  MultiGridContent = list[Agent]
52
60
 
53
61
  F = TypeVar("F", bound=Callable[..., Any])
54
62
 
55
63
 
56
64
  def accept_tuple_argument(wrapped_function: F) -> F:
57
- """Decorator to allow grid methods that take a list of (x, y) coord tuples
58
- to also handle a single position, by automatically wrapping tuple in
59
- single-item list rather than forcing user to do it."""
65
+ """Decorator to allow grid methods that take a list of (x, y) coord tuples to also handle a single position.
66
+
67
+ Tuples are wrapped in a single-item list rather than forcing user to do it.
68
+ """
60
69
 
61
70
  def wrapper(grid_instance, positions) -> Any:
62
71
  if len(positions) == 2 and not isinstance(positions[0], tuple):
@@ -67,11 +76,13 @@ def accept_tuple_argument(wrapped_function: F) -> F:
67
76
 
68
77
 
69
78
  def is_integer(x: Real) -> bool:
70
- # Check if x is either a CPython integer or Numpy integer.
79
+ """Check if x is either a CPython integer or Numpy integer."""
71
80
  return isinstance(x, _types_integer)
72
81
 
73
82
 
74
83
  def warn_if_agent_has_position_already(placement_func):
84
+ """Decorator to give warning if agent has position already set."""
85
+
75
86
  def wrapper(self, agent, *args, **kwargs):
76
87
  if agent.pos is not None:
77
88
  warnings.warn(
@@ -102,7 +113,8 @@ class _Grid:
102
113
  """Create a new grid.
103
114
 
104
115
  Args:
105
- width, height: The width and height of the grid
116
+ width: The grid's width.
117
+ height: The grid's height.
106
118
  torus: Boolean whether the grid wraps or not.
107
119
  """
108
120
  self.height = height
@@ -152,6 +164,26 @@ class _Grid:
152
164
  @overload
153
165
  def __getitem__(self, index: int | Sequence[Coordinate]) -> list[GridContent]: ...
154
166
 
167
+ @property
168
+ def agents(self) -> AgentSet:
169
+ """Return an AgentSet with the agents in the space."""
170
+ agents = []
171
+ for entry in self:
172
+ if not entry:
173
+ continue
174
+ if not isinstance(entry, list):
175
+ entry = [entry] # noqa PLW2901
176
+ for agent in entry:
177
+ agents.append(agent)
178
+
179
+ # getting the rng is a bit hacky because old style spaces don't have the rng
180
+ try:
181
+ rng = agents[0].random
182
+ except IndexError:
183
+ # there are no agents in the space
184
+ rng = None
185
+ return AgentSet(agents, random=rng)
186
+
155
187
  @overload
156
188
  def __getitem__(
157
189
  self, index: tuple[int | slice, int | slice]
@@ -159,7 +191,6 @@ class _Grid:
159
191
 
160
192
  def __getitem__(self, index):
161
193
  """Access contents from the grid."""
162
-
163
194
  if isinstance(index, int):
164
195
  # grid[x]
165
196
  return self._grid[index]
@@ -192,8 +223,7 @@ class _Grid:
192
223
  return [cell for rows in self._grid[x] for cell in rows[y]]
193
224
 
194
225
  def __iter__(self) -> Iterator[GridContent]:
195
- """Create an iterator that chains the rows of the grid together
196
- as if it is one list:"""
226
+ """Create an iterator that chains the rows of the grid together as if it is one list."""
197
227
  return itertools.chain(*self._grid)
198
228
 
199
229
  def coord_iter(self) -> Iterator[tuple[GridContent, Coordinate]]:
@@ -209,8 +239,7 @@ class _Grid:
209
239
  include_center: bool = False,
210
240
  radius: int = 1,
211
241
  ) -> Iterator[Coordinate]:
212
- """Return an iterator over cell coordinates that are in the
213
- neighborhood of a certain point.
242
+ """Return an iterator over cell coordinates that are in the neighborhood of a certain point.
214
243
 
215
244
  Args:
216
245
  pos: Coordinate tuple for the neighborhood to get.
@@ -237,8 +266,7 @@ class _Grid:
237
266
  include_center: bool = False,
238
267
  radius: int = 1,
239
268
  ) -> Sequence[Coordinate]:
240
- """Return a list of cells that are in the neighborhood of a
241
- certain point.
269
+ """Return a list of cells that are in the neighborhood of a certain point.
242
270
 
243
271
  Args:
244
272
  pos: Coordinate tuple for the neighborhood to get.
@@ -381,8 +409,7 @@ class _Grid:
381
409
  return pos[0] % self.width, pos[1] % self.height
382
410
 
383
411
  def out_of_bounds(self, pos: Coordinate) -> bool:
384
- """Determines whether position is off the grid, returns the out of
385
- bounds coordinate."""
412
+ """Determines whether position is off the grid, returns the out of bounds coordinate."""
386
413
  x, y = pos
387
414
  return x < 0 or x >= self.width or y < 0 or y >= self.height
388
415
 
@@ -390,8 +417,7 @@ class _Grid:
390
417
  def iter_cell_list_contents(
391
418
  self, cell_list: Iterable[Coordinate]
392
419
  ) -> Iterator[Agent]:
393
- """Returns an iterator of the agents contained in the cells identified
394
- in `cell_list`; cells with empty content are excluded.
420
+ """Returns an iterator of the agents contained in the cells identified in `cell_list`; cells with empty content are excluded.
395
421
 
396
422
  Args:
397
423
  cell_list: Array-like of (x, y) tuples, or single tuple.
@@ -407,8 +433,7 @@ class _Grid:
407
433
 
408
434
  @accept_tuple_argument
409
435
  def get_cell_list_contents(self, cell_list: Iterable[Coordinate]) -> list[Agent]:
410
- """Returns an iterator of the agents contained in the cells identified
411
- in `cell_list`; cells with empty content are excluded.
436
+ """Returns an iterator of the agents contained in the cells identified in `cell_list`; cells with empty content are excluded.
412
437
 
413
438
  Args:
414
439
  cell_list: Array-like of (x, y) tuples, or single tuple.
@@ -441,8 +466,7 @@ class _Grid:
441
466
  selection: str = "random",
442
467
  handle_empty: str | None = None,
443
468
  ) -> None:
444
- """
445
- Move an agent to one of the given positions.
469
+ """Move an agent to one of the given positions.
446
470
 
447
471
  Args:
448
472
  agent: Agent object to move. Assumed to have its current location stored in a 'pos' tuple.
@@ -494,9 +518,7 @@ class _Grid:
494
518
  )
495
519
 
496
520
  def _distance_squared(self, pos1: Coordinate, pos2: Coordinate) -> float:
497
- """
498
- Calculate the squared Euclidean distance between two points for performance.
499
- """
521
+ """Calculate the squared Euclidean distance between two points for performance."""
500
522
  # Use squared Euclidean distance to avoid sqrt operation
501
523
  dx, dy = abs(pos1[0] - pos2[0]), abs(pos1[1] - pos2[1])
502
524
  if self.torus:
@@ -505,7 +527,7 @@ class _Grid:
505
527
  return dx**2 + dy**2
506
528
 
507
529
  def swap_pos(self, agent_a: Agent, agent_b: Agent) -> None:
508
- """Swap agents positions"""
530
+ """Swap agents positions."""
509
531
  agents_no_pos = []
510
532
  if (pos_a := agent_a.pos) is None:
511
533
  agents_no_pos.append(agent_a)
@@ -566,16 +588,16 @@ def is_single_argument_function(function):
566
588
  )
567
589
 
568
590
 
569
- def ufunc_requires_additional_input(ufunc):
591
+ def ufunc_requires_additional_input(ufunc): # noqa: D103
570
592
  # NumPy ufuncs have a 'nargs' attribute indicating the number of input arguments
571
593
  # For binary ufuncs (like np.add), nargs is 2
572
594
  return ufunc.nargs > 1
573
595
 
574
596
 
575
597
  class PropertyLayer:
576
- """
577
- A class representing a layer of properties in a two-dimensional grid. Each cell in the grid
578
- can store a value of a specified data type.
598
+ """A class representing a layer of properties in a two-dimensional grid.
599
+
600
+ Each cell in the grid can store a value of a specified data type.
579
601
 
580
602
  Attributes:
581
603
  name (str): The name of the property layer.
@@ -583,13 +605,6 @@ class PropertyLayer:
583
605
  height (int): The height of the grid (number of rows).
584
606
  data (numpy.ndarray): A NumPy array representing the grid data.
585
607
 
586
- Methods:
587
- set_cell(position, value): Sets the value of a single cell.
588
- set_cells(value, condition=None): Sets the values of multiple cells, optionally based on a condition.
589
- modify_cell(position, operation, value): Modifies the value of a single cell using an operation.
590
- modify_cells(operation, value, condition_function): Modifies the values of multiple cells using an operation.
591
- select_cells(condition, return_list): Selects cells that meet a specified condition.
592
- aggregate_property(operation): Performs an aggregate operation over all cells.
593
608
  """
594
609
 
595
610
  propertylayer_experimental_warning_given = False
@@ -597,8 +612,7 @@ class PropertyLayer:
597
612
  def __init__(
598
613
  self, name: str, width: int, height: int, default_value, dtype=np.float64
599
614
  ):
600
- """
601
- Initializes a new PropertyLayer instance.
615
+ """Initializes a new PropertyLayer instance.
602
616
 
603
617
  Args:
604
618
  name (str): The name of the property layer.
@@ -649,14 +663,11 @@ class PropertyLayer:
649
663
  self.__class__.propertylayer_experimental_warning_given = True
650
664
 
651
665
  def set_cell(self, position: Coordinate, value):
652
- """
653
- Update a single cell's value in-place.
654
- """
666
+ """Update a single cell's value in-place."""
655
667
  self.data[position] = value
656
668
 
657
669
  def set_cells(self, value, condition=None):
658
- """
659
- Perform a batch update either on the entire grid or conditionally, in-place.
670
+ """Perform a batch update either on the entire grid or conditionally, in-place.
660
671
 
661
672
  Args:
662
673
  value: The value to be used for the update.
@@ -685,8 +696,8 @@ class PropertyLayer:
685
696
  np.copyto(self.data, value, where=condition_result)
686
697
 
687
698
  def modify_cell(self, position: Coordinate, operation, value=None):
688
- """
689
- Modify a single cell using an operation, which can be a lambda function or a NumPy ufunc.
699
+ """Modify a single cell using an operation, which can be a lambda function or a NumPy ufunc.
700
+
690
701
  If a NumPy ufunc is used, an additional value should be provided.
691
702
 
692
703
  Args:
@@ -707,8 +718,8 @@ class PropertyLayer:
707
718
  raise ValueError("Invalid operation or missing value for NumPy ufunc.")
708
719
 
709
720
  def modify_cells(self, operation, value=None, condition_function=None):
710
- """
711
- Modify cells using an operation, which can be a lambda function or a NumPy ufunc.
721
+ """Modify cells using an operation, which can be a lambda function or a NumPy ufunc.
722
+
712
723
  If a NumPy ufunc is used, an additional value should be provided.
713
724
 
714
725
  Args:
@@ -742,8 +753,7 @@ class PropertyLayer:
742
753
  self.data = np.where(condition_array, modified_data, self.data)
743
754
 
744
755
  def select_cells(self, condition, return_list=True):
745
- """
746
- Find cells that meet a specified condition using NumPy's boolean indexing, in-place.
756
+ """Find cells that meet a specified condition using NumPy's boolean indexing, in-place.
747
757
 
748
758
  Args:
749
759
  condition: A callable that returns a boolean array when applied to the data.
@@ -768,9 +778,9 @@ class PropertyLayer:
768
778
 
769
779
 
770
780
  class _PropertyGrid(_Grid):
771
- """
772
- A private subclass of _Grid that supports the addition of property layers, enabling
773
- the representation and manipulation of additional data layers on the grid. This class is
781
+ """A private subclass of _Grid that supports the addition of property layers.
782
+
783
+ This enables the representation and manipulation of additional data layers on the grid. This class is
774
784
  intended for internal use within the Mesa framework and is currently utilized by SingleGrid
775
785
  and MultiGrid classes to provide enhanced grid functionality.
776
786
 
@@ -783,22 +793,15 @@ class _PropertyGrid(_Grid):
783
793
  properties (dict): A dictionary mapping property layer names to PropertyLayer instances.
784
794
  empty_mask (np.ndarray): A boolean array indicating empty cells on the grid.
785
795
 
786
- Methods:
787
- add_property_layer(property_layer): Adds a new property layer to the grid.
788
- remove_property_layer(property_name): Removes a property layer from the grid by its name.
789
- get_neighborhood_mask(pos, moore, include_center, radius): Generates a boolean mask of the neighborhood.
790
- select_cells(conditions, extreme_values, masks, only_empty, return_list): Selects cells based on multiple conditions,
791
- extreme values, masks, with an option to select only empty cells, returning either a list of coordinates or a mask.
792
-
793
- Mask Usage:
794
- Several methods in this class accept a mask as an input, which is a NumPy ndarray of boolean values. This mask
795
- specifies the cells to be considered (True) or ignored (False) in operations. Users can create custom masks,
796
- including neighborhood masks, to apply specific conditions or constraints. Additionally, methods that deal with
797
- cell selection or agent movement can return either a list of cell coordinates or a mask, based on the 'return_list'
798
- parameter. This flexibility allows for more nuanced control and customization of grid operations, catering to a wide
799
- range of modeling requirements and scenarios.
800
-
801
- Note:
796
+
797
+ Several methods in this class accept a mask as an input, which is a NumPy ndarray of boolean values. This mask
798
+ specifies the cells to be considered (True) or ignored (False) in operations. Users can create custom masks,
799
+ including neighborhood masks, to apply specific conditions or constraints. Additionally, methods that deal with
800
+ cell selection or agent movement can return either a list of cell coordinates or a mask, based on the 'return_list'
801
+ parameter. This flexibility allows for more nuanced control and customization of grid operations, catering to a wide
802
+ range of modeling requirements and scenarios.
803
+
804
+ Notes:
802
805
  This class is not intended for direct use in user models but is currently used by the SingleGrid and MultiGrid.
803
806
  """
804
807
 
@@ -809,8 +812,7 @@ class _PropertyGrid(_Grid):
809
812
  torus: bool,
810
813
  property_layers: None | PropertyLayer | list[PropertyLayer] = None,
811
814
  ):
812
- """
813
- Initializes a new _PropertyGrid instance with specified dimensions and optional property layers.
815
+ """Initializes a new _PropertyGrid instance with specified dimensions and optional property layers.
814
816
 
815
817
  Args:
816
818
  width (int): The width of the grid (number of columns).
@@ -839,15 +841,12 @@ class _PropertyGrid(_Grid):
839
841
 
840
842
  @property
841
843
  def empty_mask(self) -> np.ndarray:
842
- """
843
- Returns a boolean mask indicating empty cells on the grid.
844
- """
844
+ """Returns a boolean mask indicating empty cells on the grid."""
845
845
  return self._empty_mask
846
846
 
847
847
  # Add and remove properties to the grid
848
848
  def add_property_layer(self, property_layer: PropertyLayer):
849
- """
850
- Adds a new property layer to the grid.
849
+ """Adds a new property layer to the grid.
851
850
 
852
851
  Args:
853
852
  property_layer (PropertyLayer): The PropertyLayer instance to be added to the grid.
@@ -865,8 +864,7 @@ class _PropertyGrid(_Grid):
865
864
  self.properties[property_layer.name] = property_layer
866
865
 
867
866
  def remove_property_layer(self, property_name: str):
868
- """
869
- Removes a property layer from the grid by its name.
867
+ """Removes a property layer from the grid by its name.
870
868
 
871
869
  Args:
872
870
  property_name (str): The name of the property layer to be removed.
@@ -881,8 +879,8 @@ class _PropertyGrid(_Grid):
881
879
  def get_neighborhood_mask(
882
880
  self, pos: Coordinate, moore: bool, include_center: bool, radius: int
883
881
  ) -> np.ndarray:
884
- """
885
- Generate a boolean mask representing the neighborhood.
882
+ """Generate a boolean mask representing the neighborhood.
883
+
886
884
  Helper method for select_cells_multi_properties() and move_agent_to_random_cell()
887
885
 
888
886
  Args:
@@ -910,8 +908,7 @@ class _PropertyGrid(_Grid):
910
908
  only_empty: bool = False,
911
909
  return_list: bool = True,
912
910
  ) -> list[Coordinate] | np.ndarray:
913
- """
914
- Select cells based on property conditions, extreme values, and/or masks, with an option to only select empty cells.
911
+ """Select cells based on property conditions, extreme values, and/or masks, with an option to only select empty cells.
915
912
 
916
913
  Args:
917
914
  conditions (dict): A dictionary where keys are property names and values are callables that return a boolean when applied.
@@ -1064,7 +1061,7 @@ class MultiGrid(_PropertyGrid):
1064
1061
  self._empty_mask[agent.pos] = False
1065
1062
  agent.pos = None
1066
1063
 
1067
- def iter_neighbors(
1064
+ def iter_neighbors( # noqa: D102
1068
1065
  self,
1069
1066
  pos: Coordinate,
1070
1067
  moore: bool,
@@ -1079,8 +1076,9 @@ class MultiGrid(_PropertyGrid):
1079
1076
  def iter_cell_list_contents(
1080
1077
  self, cell_list: Iterable[Coordinate]
1081
1078
  ) -> Iterator[Agent]:
1082
- """Returns an iterator of the agents contained in the cells identified
1083
- in `cell_list`; cells with empty content are excluded.
1079
+ """Returns an iterator of the agents contained in the cells identified in `cell_list`.
1080
+
1081
+ Cells with empty content are excluded.
1084
1082
 
1085
1083
  Args:
1086
1084
  cell_list: Array-like of (x, y) tuples, or single tuple.
@@ -1118,9 +1116,9 @@ class _HexGrid:
1118
1116
  def get_neighborhood(
1119
1117
  self, pos: Coordinate, include_center: bool = False, radius: int = 1
1120
1118
  ) -> list[Coordinate]:
1121
- """Return a list of coordinates that are in the
1122
- neighborhood of a certain point. To calculate the neighborhood
1123
- for a HexGrid the parity of the x coordinate of the point is
1119
+ """Return a list of coordinates that are in the neighborhood of a certain point.
1120
+
1121
+ To calculate the neighborhood for a HexGrid the parity of the x coordinate of the point is
1124
1122
  important, the neighborhood can be sketched as:
1125
1123
 
1126
1124
  Always: (0,-), (0,+)
@@ -1206,8 +1204,7 @@ class _HexGrid:
1206
1204
  def iter_neighborhood(
1207
1205
  self, pos: Coordinate, include_center: bool = False, radius: int = 1
1208
1206
  ) -> Iterator[Coordinate]:
1209
- """Return an iterator over cell coordinates that are in the
1210
- neighborhood of a certain point.
1207
+ """Return an iterator over cell coordinates that are in the neighborhood of a certain point.
1211
1208
 
1212
1209
  Args:
1213
1210
  pos: Coordinate tuple for the neighborhood to get.
@@ -1257,8 +1254,7 @@ class _HexGrid:
1257
1254
 
1258
1255
 
1259
1256
  class HexSingleGrid(_HexGrid, SingleGrid):
1260
- """Hexagonal SingleGrid: a SingleGrid where neighbors are computed
1261
- according to a hexagonal tiling of the grid.
1257
+ """Hexagonal SingleGrid: a SingleGrid where neighbors are computed according to a hexagonal tiling of the grid.
1262
1258
 
1263
1259
  Functions according to odd-q rules.
1264
1260
  See http://www.redblobgames.com/grids/hexagons/#coordinates for more.
@@ -1274,8 +1270,7 @@ class HexSingleGrid(_HexGrid, SingleGrid):
1274
1270
 
1275
1271
 
1276
1272
  class HexMultiGrid(_HexGrid, MultiGrid):
1277
- """Hexagonal MultiGrid: a MultiGrid where neighbors are computed
1278
- according to a hexagonal tiling of the grid.
1273
+ """Hexagonal MultiGrid: a MultiGrid where neighbors are computed according to a hexagonal tiling of the grid.
1279
1274
 
1280
1275
  Functions according to odd-q rules.
1281
1276
  See http://www.redblobgames.com/grids/hexagons/#coordinates for more.
@@ -1292,30 +1287,6 @@ class HexMultiGrid(_HexGrid, MultiGrid):
1292
1287
  """
1293
1288
 
1294
1289
 
1295
- class HexGrid(HexSingleGrid):
1296
- """Hexagonal Grid: a Grid where neighbors are computed
1297
- according to a hexagonal tiling of the grid.
1298
-
1299
- Functions according to odd-q rules.
1300
- See http://www.redblobgames.com/grids/hexagons/#coordinates for more.
1301
-
1302
- Properties:
1303
- width, height: The grid's width and height.
1304
- torus: Boolean which determines whether to treat the grid as a torus.
1305
- """
1306
-
1307
- def __init__(self, width: int, height: int, torus: bool) -> None:
1308
- super().__init__(width, height, torus)
1309
- warn(
1310
- (
1311
- "HexGrid is being deprecated; use instead HexSingleGrid or HexMultiGrid "
1312
- "depending on your use case."
1313
- ),
1314
- DeprecationWarning,
1315
- stacklevel=2,
1316
- )
1317
-
1318
-
1319
1290
  class ContinuousSpace:
1320
1291
  """Continuous space where each agent can have an arbitrary position.
1321
1292
 
@@ -1341,11 +1312,13 @@ class ContinuousSpace:
1341
1312
  """Create a new continuous space.
1342
1313
 
1343
1314
  Args:
1344
- x_max, y_max: Maximum x and y coordinates for the space.
1315
+ x_max: the maximum x-coordinate
1316
+ y_max: the maximum y-coordinate.
1345
1317
  torus: Boolean for whether the edges loop around.
1346
- x_min, y_min: (default 0) If provided, set the minimum x and y
1347
- coordinates for the space. Below them, values loop to
1348
- the other edge (if torus=True) or raise an exception.
1318
+ x_min: (default 0) If provided, set the minimum x -coordinate for the space. Below them, values loop to
1319
+ the other edge (if torus=True) or raise an exception.
1320
+ y_min: (default 0) If provided, set the minimum y -coordinate for the space. Below them, values loop to
1321
+ the other edge (if torus=True) or raise an exception.
1349
1322
  """
1350
1323
  self.x_min = x_min
1351
1324
  self.x_max = x_max
@@ -1361,6 +1334,19 @@ class ContinuousSpace:
1361
1334
  self._index_to_agent: dict[int, Agent] = {}
1362
1335
  self._agent_to_index: dict[Agent, int | None] = {}
1363
1336
 
1337
+ @property
1338
+ def agents(self) -> AgentSet:
1339
+ """Return an AgentSet with the agents in the space."""
1340
+ agents = list(self._agent_to_index)
1341
+
1342
+ # getting the rng is a bit hacky because old style spaces don't have the rng
1343
+ try:
1344
+ rng = agents[0].random
1345
+ except IndexError:
1346
+ # there are no agents in the space
1347
+ rng = None
1348
+ return AgentSet(agents, random=rng)
1349
+
1364
1350
  def _build_agent_cache(self):
1365
1351
  """Cache agents positions to speed up neighbors calculations."""
1366
1352
  self._index_to_agent = {}
@@ -1448,11 +1434,13 @@ class ContinuousSpace:
1448
1434
  self, pos_1: FloatCoordinate, pos_2: FloatCoordinate
1449
1435
  ) -> FloatCoordinate:
1450
1436
  """Get the heading vector between two points, accounting for toroidal space.
1437
+
1451
1438
  It is possible to calculate the heading angle by applying the atan2 function to the
1452
1439
  result.
1453
1440
 
1454
1441
  Args:
1455
- pos_1, pos_2: Coordinate tuples for both points.
1442
+ pos_1: Coordinate tuples for both points.
1443
+ pos_2: Coordinate tuples for both points.
1456
1444
  """
1457
1445
  one = np.array(pos_1)
1458
1446
  two = np.array(pos_2)
@@ -1478,7 +1466,8 @@ class ContinuousSpace:
1478
1466
  """Get the distance between two point, accounting for toroidal space.
1479
1467
 
1480
1468
  Args:
1481
- pos_1, pos_2: Coordinate tuples for both points.
1469
+ pos_1: Coordinate tuples for point1.
1470
+ pos_2: Coordinate tuples for point2.
1482
1471
  """
1483
1472
  x1, y1 = pos_1
1484
1473
  x2, y2 = pos_2
@@ -1525,12 +1514,33 @@ class NetworkGrid:
1525
1514
  """Create a new network.
1526
1515
 
1527
1516
  Args:
1528
- G: a NetworkX graph instance.
1517
+ g: a NetworkX graph instance.
1529
1518
  """
1530
1519
  self.G = g
1531
1520
  for node_id in self.G.nodes:
1532
1521
  g.nodes[node_id]["agent"] = self.default_val()
1533
1522
 
1523
+ @property
1524
+ def agents(self) -> AgentSet:
1525
+ """Return an AgentSet with the agents in the space."""
1526
+ agents = []
1527
+ for node_id in self.G.nodes:
1528
+ entry = self.G.nodes[node_id]["agent"]
1529
+ if not entry:
1530
+ continue
1531
+ if not isinstance(entry, list):
1532
+ entry = [entry]
1533
+ for agent in entry:
1534
+ agents.append(agent)
1535
+
1536
+ # getting the rng is a bit hacky because old style spaces don't have the rng
1537
+ try:
1538
+ rng = agents[0].random
1539
+ except IndexError:
1540
+ # there are no agents in the space
1541
+ rng = None
1542
+ return AgentSet(agents, random=rng)
1543
+
1534
1544
  @staticmethod
1535
1545
  def default_val() -> list:
1536
1546
  """Default value for a new node."""
@@ -1545,7 +1555,16 @@ class NetworkGrid:
1545
1555
  def get_neighborhood(
1546
1556
  self, node_id: int, include_center: bool = False, radius: int = 1
1547
1557
  ) -> list[int]:
1548
- """Get all adjacent nodes within a certain radius"""
1558
+ """Get all adjacent nodes within a certain radius.
1559
+
1560
+ Args:
1561
+ node_id: node id for which to get neighborhood
1562
+ include_center: boolean to include node itself or not
1563
+ radius: size of neighborhood
1564
+
1565
+ Returns:
1566
+ a list
1567
+ """
1549
1568
  if radius == 1:
1550
1569
  neighborhood = list(self.G.neighbors(node_id))
1551
1570
  if include_center:
@@ -1562,28 +1581,61 @@ class NetworkGrid:
1562
1581
  def get_neighbors(
1563
1582
  self, node_id: int, include_center: bool = False, radius: int = 1
1564
1583
  ) -> list[Agent]:
1565
- """Get all agents in adjacent nodes (within a certain radius)."""
1584
+ """Get all agents in adjacent nodes (within a certain radius).
1585
+
1586
+ Args:
1587
+ node_id: node id for which to get neighbors
1588
+ include_center: whether to include node itself or not
1589
+ radius: size of neighborhood in which to find neighbors
1590
+
1591
+ Returns:
1592
+ list of agents in neighborhood.
1593
+ """
1566
1594
  neighborhood = self.get_neighborhood(node_id, include_center, radius)
1567
1595
  return self.get_cell_list_contents(neighborhood)
1568
1596
 
1569
1597
  def move_agent(self, agent: Agent, node_id: int) -> None:
1570
- """Move an agent from its current node to a new node."""
1598
+ """Move an agent from its current node to a new node.
1599
+
1600
+ Args:
1601
+ agent: agent instance
1602
+ node_id: id of node
1603
+
1604
+ """
1571
1605
  self.remove_agent(agent)
1572
1606
  self.place_agent(agent, node_id)
1573
1607
 
1574
1608
  def remove_agent(self, agent: Agent) -> None:
1575
- """Remove the agent from the network and set its pos attribute to None."""
1609
+ """Remove the agent from the network and set its pos attribute to None.
1610
+
1611
+ Args:
1612
+ agent: agent instance
1613
+
1614
+ """
1576
1615
  node_id = agent.pos
1577
1616
  self.G.nodes[node_id]["agent"].remove(agent)
1578
1617
  agent.pos = None
1579
1618
 
1580
1619
  def is_cell_empty(self, node_id: int) -> bool:
1581
- """Returns a bool of the contents of a cell."""
1620
+ """Returns a bool of the contents of a cell.
1621
+
1622
+ Args:
1623
+ node_id: id of node
1624
+
1625
+ """
1582
1626
  return self.G.nodes[node_id]["agent"] == self.default_val()
1583
1627
 
1584
1628
  def get_cell_list_contents(self, cell_list: list[int]) -> list[Agent]:
1585
- """Returns a list of the agents contained in the nodes identified
1586
- in `cell_list`; nodes with empty content are excluded.
1629
+ """Returns a list of the agents contained in the nodes identified in `cell_list`.
1630
+
1631
+ Nodes with empty content are excluded.
1632
+
1633
+ Args:
1634
+ cell_list: list of cell ids.
1635
+
1636
+ Returns:
1637
+ list of the agents contained in the nodes identified in `cell_list`.
1638
+
1587
1639
  """
1588
1640
  return list(self.iter_cell_list_contents(cell_list))
1589
1641
 
@@ -1592,8 +1644,16 @@ class NetworkGrid:
1592
1644
  return self.get_cell_list_contents(self.G)
1593
1645
 
1594
1646
  def iter_cell_list_contents(self, cell_list: list[int]) -> Iterator[Agent]:
1595
- """Returns an iterator of the agents contained in the nodes identified
1596
- in `cell_list`; nodes with empty content are excluded.
1647
+ """Returns an iterator of the agents contained in the nodes identified in `cell_list`.
1648
+
1649
+ Nodes with empty content are excluded.
1650
+
1651
+ Args:
1652
+ cell_list: list of cell ids.
1653
+
1654
+ Returns:
1655
+ iterator of the agents contained in the nodes identified in `cell_list`.
1656
+
1597
1657
  """
1598
1658
  return itertools.chain.from_iterable(
1599
1659
  self.G.nodes[node_id]["agent"]