Mesa 2.3.4__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 +393 -116
  3. mesa/batchrunner.py +58 -31
  4. mesa/datacollection.py +141 -30
  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 +71 -39
  75. mesa/experimental/devs/examples/wolf_sheep.py +45 -45
  76. mesa/experimental/devs/simulator.py +57 -16
  77. mesa/experimental/{jupyter_viz.py → solara_viz.py} +151 -98
  78. mesa/model.py +212 -84
  79. mesa/space.py +217 -151
  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.3.4.dist-info → mesa-3.0.0.dist-info}/METADATA +65 -19
  90. mesa-3.0.0.dist-info/RECORD +95 -0
  91. mesa-3.0.0.dist-info/licenses/LICENSE +202 -0
  92. mesa-2.3.4.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.3.4.dist-info/RECORD +0 -45
  108. /mesa/{cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}} → examples/advanced}/__init__.py +0 -0
  109. {mesa-2.3.4.dist-info → mesa-3.0.0.dist-info}/WHEEL +0 -0
  110. {mesa-2.3.4.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.
@@ -459,15 +483,21 @@ class _Grid:
459
483
  elif selection == "closest":
460
484
  current_pos = agent.pos
461
485
  # Find the closest position without sorting all positions
462
- closest_pos = None
486
+ # TODO: See if this method can be optimized further
487
+ closest_pos = []
463
488
  min_distance = float("inf")
464
489
  agent.random.shuffle(pos)
465
490
  for p in pos:
466
491
  distance = self._distance_squared(p, current_pos)
467
492
  if distance < min_distance:
468
493
  min_distance = distance
469
- closest_pos = p
470
- chosen_pos = closest_pos
494
+ closest_pos.clear()
495
+ closest_pos.append(p)
496
+ elif distance == min_distance:
497
+ closest_pos.append(p)
498
+
499
+ chosen_pos = agent.random.choice(closest_pos)
500
+
471
501
  else:
472
502
  raise ValueError(
473
503
  f"Invalid selection method {selection}. Choose 'random' or 'closest'."
@@ -488,9 +518,7 @@ class _Grid:
488
518
  )
489
519
 
490
520
  def _distance_squared(self, pos1: Coordinate, pos2: Coordinate) -> float:
491
- """
492
- Calculate the squared Euclidean distance between two points for performance.
493
- """
521
+ """Calculate the squared Euclidean distance between two points for performance."""
494
522
  # Use squared Euclidean distance to avoid sqrt operation
495
523
  dx, dy = abs(pos1[0] - pos2[0]), abs(pos1[1] - pos2[1])
496
524
  if self.torus:
@@ -499,7 +527,7 @@ class _Grid:
499
527
  return dx**2 + dy**2
500
528
 
501
529
  def swap_pos(self, agent_a: Agent, agent_b: Agent) -> None:
502
- """Swap agents positions"""
530
+ """Swap agents positions."""
503
531
  agents_no_pos = []
504
532
  if (pos_a := agent_a.pos) is None:
505
533
  agents_no_pos.append(agent_a)
@@ -560,16 +588,16 @@ def is_single_argument_function(function):
560
588
  )
561
589
 
562
590
 
563
- def ufunc_requires_additional_input(ufunc):
591
+ def ufunc_requires_additional_input(ufunc): # noqa: D103
564
592
  # NumPy ufuncs have a 'nargs' attribute indicating the number of input arguments
565
593
  # For binary ufuncs (like np.add), nargs is 2
566
594
  return ufunc.nargs > 1
567
595
 
568
596
 
569
597
  class PropertyLayer:
570
- """
571
- A class representing a layer of properties in a two-dimensional grid. Each cell in the grid
572
- 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.
573
601
 
574
602
  Attributes:
575
603
  name (str): The name of the property layer.
@@ -577,13 +605,6 @@ class PropertyLayer:
577
605
  height (int): The height of the grid (number of rows).
578
606
  data (numpy.ndarray): A NumPy array representing the grid data.
579
607
 
580
- Methods:
581
- set_cell(position, value): Sets the value of a single cell.
582
- set_cells(value, condition=None): Sets the values of multiple cells, optionally based on a condition.
583
- modify_cell(position, operation, value): Modifies the value of a single cell using an operation.
584
- modify_cells(operation, value, condition_function): Modifies the values of multiple cells using an operation.
585
- select_cells(condition, return_list): Selects cells that meet a specified condition.
586
- aggregate_property(operation): Performs an aggregate operation over all cells.
587
608
  """
588
609
 
589
610
  propertylayer_experimental_warning_given = False
@@ -591,8 +612,7 @@ class PropertyLayer:
591
612
  def __init__(
592
613
  self, name: str, width: int, height: int, default_value, dtype=np.float64
593
614
  ):
594
- """
595
- Initializes a new PropertyLayer instance.
615
+ """Initializes a new PropertyLayer instance.
596
616
 
597
617
  Args:
598
618
  name (str): The name of the property layer.
@@ -643,14 +663,11 @@ class PropertyLayer:
643
663
  self.__class__.propertylayer_experimental_warning_given = True
644
664
 
645
665
  def set_cell(self, position: Coordinate, value):
646
- """
647
- Update a single cell's value in-place.
648
- """
666
+ """Update a single cell's value in-place."""
649
667
  self.data[position] = value
650
668
 
651
669
  def set_cells(self, value, condition=None):
652
- """
653
- 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.
654
671
 
655
672
  Args:
656
673
  value: The value to be used for the update.
@@ -679,8 +696,8 @@ class PropertyLayer:
679
696
  np.copyto(self.data, value, where=condition_result)
680
697
 
681
698
  def modify_cell(self, position: Coordinate, operation, value=None):
682
- """
683
- 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
+
684
701
  If a NumPy ufunc is used, an additional value should be provided.
685
702
 
686
703
  Args:
@@ -701,8 +718,8 @@ class PropertyLayer:
701
718
  raise ValueError("Invalid operation or missing value for NumPy ufunc.")
702
719
 
703
720
  def modify_cells(self, operation, value=None, condition_function=None):
704
- """
705
- 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
+
706
723
  If a NumPy ufunc is used, an additional value should be provided.
707
724
 
708
725
  Args:
@@ -736,8 +753,7 @@ class PropertyLayer:
736
753
  self.data = np.where(condition_array, modified_data, self.data)
737
754
 
738
755
  def select_cells(self, condition, return_list=True):
739
- """
740
- 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.
741
757
 
742
758
  Args:
743
759
  condition: A callable that returns a boolean array when applied to the data.
@@ -762,9 +778,9 @@ class PropertyLayer:
762
778
 
763
779
 
764
780
  class _PropertyGrid(_Grid):
765
- """
766
- A private subclass of _Grid that supports the addition of property layers, enabling
767
- 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
768
784
  intended for internal use within the Mesa framework and is currently utilized by SingleGrid
769
785
  and MultiGrid classes to provide enhanced grid functionality.
770
786
 
@@ -777,22 +793,15 @@ class _PropertyGrid(_Grid):
777
793
  properties (dict): A dictionary mapping property layer names to PropertyLayer instances.
778
794
  empty_mask (np.ndarray): A boolean array indicating empty cells on the grid.
779
795
 
780
- Methods:
781
- add_property_layer(property_layer): Adds a new property layer to the grid.
782
- remove_property_layer(property_name): Removes a property layer from the grid by its name.
783
- get_neighborhood_mask(pos, moore, include_center, radius): Generates a boolean mask of the neighborhood.
784
- select_cells(conditions, extreme_values, masks, only_empty, return_list): Selects cells based on multiple conditions,
785
- extreme values, masks, with an option to select only empty cells, returning either a list of coordinates or a mask.
786
-
787
- Mask Usage:
788
- Several methods in this class accept a mask as an input, which is a NumPy ndarray of boolean values. This mask
789
- specifies the cells to be considered (True) or ignored (False) in operations. Users can create custom masks,
790
- including neighborhood masks, to apply specific conditions or constraints. Additionally, methods that deal with
791
- cell selection or agent movement can return either a list of cell coordinates or a mask, based on the 'return_list'
792
- parameter. This flexibility allows for more nuanced control and customization of grid operations, catering to a wide
793
- range of modeling requirements and scenarios.
794
-
795
- 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:
796
805
  This class is not intended for direct use in user models but is currently used by the SingleGrid and MultiGrid.
797
806
  """
798
807
 
@@ -803,8 +812,7 @@ class _PropertyGrid(_Grid):
803
812
  torus: bool,
804
813
  property_layers: None | PropertyLayer | list[PropertyLayer] = None,
805
814
  ):
806
- """
807
- 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.
808
816
 
809
817
  Args:
810
818
  width (int): The width of the grid (number of columns).
@@ -833,15 +841,12 @@ class _PropertyGrid(_Grid):
833
841
 
834
842
  @property
835
843
  def empty_mask(self) -> np.ndarray:
836
- """
837
- Returns a boolean mask indicating empty cells on the grid.
838
- """
844
+ """Returns a boolean mask indicating empty cells on the grid."""
839
845
  return self._empty_mask
840
846
 
841
847
  # Add and remove properties to the grid
842
848
  def add_property_layer(self, property_layer: PropertyLayer):
843
- """
844
- Adds a new property layer to the grid.
849
+ """Adds a new property layer to the grid.
845
850
 
846
851
  Args:
847
852
  property_layer (PropertyLayer): The PropertyLayer instance to be added to the grid.
@@ -859,8 +864,7 @@ class _PropertyGrid(_Grid):
859
864
  self.properties[property_layer.name] = property_layer
860
865
 
861
866
  def remove_property_layer(self, property_name: str):
862
- """
863
- Removes a property layer from the grid by its name.
867
+ """Removes a property layer from the grid by its name.
864
868
 
865
869
  Args:
866
870
  property_name (str): The name of the property layer to be removed.
@@ -875,8 +879,8 @@ class _PropertyGrid(_Grid):
875
879
  def get_neighborhood_mask(
876
880
  self, pos: Coordinate, moore: bool, include_center: bool, radius: int
877
881
  ) -> np.ndarray:
878
- """
879
- Generate a boolean mask representing the neighborhood.
882
+ """Generate a boolean mask representing the neighborhood.
883
+
880
884
  Helper method for select_cells_multi_properties() and move_agent_to_random_cell()
881
885
 
882
886
  Args:
@@ -904,8 +908,7 @@ class _PropertyGrid(_Grid):
904
908
  only_empty: bool = False,
905
909
  return_list: bool = True,
906
910
  ) -> list[Coordinate] | np.ndarray:
907
- """
908
- 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.
909
912
 
910
913
  Args:
911
914
  conditions (dict): A dictionary where keys are property names and values are callables that return a boolean when applied.
@@ -1058,7 +1061,7 @@ class MultiGrid(_PropertyGrid):
1058
1061
  self._empty_mask[agent.pos] = False
1059
1062
  agent.pos = None
1060
1063
 
1061
- def iter_neighbors(
1064
+ def iter_neighbors( # noqa: D102
1062
1065
  self,
1063
1066
  pos: Coordinate,
1064
1067
  moore: bool,
@@ -1073,8 +1076,9 @@ class MultiGrid(_PropertyGrid):
1073
1076
  def iter_cell_list_contents(
1074
1077
  self, cell_list: Iterable[Coordinate]
1075
1078
  ) -> Iterator[Agent]:
1076
- """Returns an iterator of the agents contained in the cells identified
1077
- 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.
1078
1082
 
1079
1083
  Args:
1080
1084
  cell_list: Array-like of (x, y) tuples, or single tuple.
@@ -1112,9 +1116,9 @@ class _HexGrid:
1112
1116
  def get_neighborhood(
1113
1117
  self, pos: Coordinate, include_center: bool = False, radius: int = 1
1114
1118
  ) -> list[Coordinate]:
1115
- """Return a list of coordinates that are in the
1116
- neighborhood of a certain point. To calculate the neighborhood
1117
- 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
1118
1122
  important, the neighborhood can be sketched as:
1119
1123
 
1120
1124
  Always: (0,-), (0,+)
@@ -1200,8 +1204,7 @@ class _HexGrid:
1200
1204
  def iter_neighborhood(
1201
1205
  self, pos: Coordinate, include_center: bool = False, radius: int = 1
1202
1206
  ) -> Iterator[Coordinate]:
1203
- """Return an iterator over cell coordinates that are in the
1204
- neighborhood of a certain point.
1207
+ """Return an iterator over cell coordinates that are in the neighborhood of a certain point.
1205
1208
 
1206
1209
  Args:
1207
1210
  pos: Coordinate tuple for the neighborhood to get.
@@ -1251,8 +1254,7 @@ class _HexGrid:
1251
1254
 
1252
1255
 
1253
1256
  class HexSingleGrid(_HexGrid, SingleGrid):
1254
- """Hexagonal SingleGrid: a SingleGrid where neighbors are computed
1255
- 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.
1256
1258
 
1257
1259
  Functions according to odd-q rules.
1258
1260
  See http://www.redblobgames.com/grids/hexagons/#coordinates for more.
@@ -1268,8 +1270,7 @@ class HexSingleGrid(_HexGrid, SingleGrid):
1268
1270
 
1269
1271
 
1270
1272
  class HexMultiGrid(_HexGrid, MultiGrid):
1271
- """Hexagonal MultiGrid: a MultiGrid where neighbors are computed
1272
- 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.
1273
1274
 
1274
1275
  Functions according to odd-q rules.
1275
1276
  See http://www.redblobgames.com/grids/hexagons/#coordinates for more.
@@ -1286,30 +1287,6 @@ class HexMultiGrid(_HexGrid, MultiGrid):
1286
1287
  """
1287
1288
 
1288
1289
 
1289
- class HexGrid(HexSingleGrid):
1290
- """Hexagonal Grid: a Grid where neighbors are computed
1291
- according to a hexagonal tiling of the grid.
1292
-
1293
- Functions according to odd-q rules.
1294
- See http://www.redblobgames.com/grids/hexagons/#coordinates for more.
1295
-
1296
- Properties:
1297
- width, height: The grid's width and height.
1298
- torus: Boolean which determines whether to treat the grid as a torus.
1299
- """
1300
-
1301
- def __init__(self, width: int, height: int, torus: bool) -> None:
1302
- super().__init__(width, height, torus)
1303
- warn(
1304
- (
1305
- "HexGrid is being deprecated; use instead HexSingleGrid or HexMultiGrid "
1306
- "depending on your use case."
1307
- ),
1308
- DeprecationWarning,
1309
- stacklevel=2,
1310
- )
1311
-
1312
-
1313
1290
  class ContinuousSpace:
1314
1291
  """Continuous space where each agent can have an arbitrary position.
1315
1292
 
@@ -1335,11 +1312,13 @@ class ContinuousSpace:
1335
1312
  """Create a new continuous space.
1336
1313
 
1337
1314
  Args:
1338
- 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.
1339
1317
  torus: Boolean for whether the edges loop around.
1340
- x_min, y_min: (default 0) If provided, set the minimum x and y
1341
- coordinates for the space. Below them, values loop to
1342
- 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.
1343
1322
  """
1344
1323
  self.x_min = x_min
1345
1324
  self.x_max = x_max
@@ -1355,6 +1334,19 @@ class ContinuousSpace:
1355
1334
  self._index_to_agent: dict[int, Agent] = {}
1356
1335
  self._agent_to_index: dict[Agent, int | None] = {}
1357
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
+
1358
1350
  def _build_agent_cache(self):
1359
1351
  """Cache agents positions to speed up neighbors calculations."""
1360
1352
  self._index_to_agent = {}
@@ -1442,11 +1434,13 @@ class ContinuousSpace:
1442
1434
  self, pos_1: FloatCoordinate, pos_2: FloatCoordinate
1443
1435
  ) -> FloatCoordinate:
1444
1436
  """Get the heading vector between two points, accounting for toroidal space.
1437
+
1445
1438
  It is possible to calculate the heading angle by applying the atan2 function to the
1446
1439
  result.
1447
1440
 
1448
1441
  Args:
1449
- 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.
1450
1444
  """
1451
1445
  one = np.array(pos_1)
1452
1446
  two = np.array(pos_2)
@@ -1472,7 +1466,8 @@ class ContinuousSpace:
1472
1466
  """Get the distance between two point, accounting for toroidal space.
1473
1467
 
1474
1468
  Args:
1475
- pos_1, pos_2: Coordinate tuples for both points.
1469
+ pos_1: Coordinate tuples for point1.
1470
+ pos_2: Coordinate tuples for point2.
1476
1471
  """
1477
1472
  x1, y1 = pos_1
1478
1473
  x2, y2 = pos_2
@@ -1519,12 +1514,33 @@ class NetworkGrid:
1519
1514
  """Create a new network.
1520
1515
 
1521
1516
  Args:
1522
- G: a NetworkX graph instance.
1517
+ g: a NetworkX graph instance.
1523
1518
  """
1524
1519
  self.G = g
1525
1520
  for node_id in self.G.nodes:
1526
1521
  g.nodes[node_id]["agent"] = self.default_val()
1527
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
+
1528
1544
  @staticmethod
1529
1545
  def default_val() -> list:
1530
1546
  """Default value for a new node."""
@@ -1539,7 +1555,16 @@ class NetworkGrid:
1539
1555
  def get_neighborhood(
1540
1556
  self, node_id: int, include_center: bool = False, radius: int = 1
1541
1557
  ) -> list[int]:
1542
- """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
+ """
1543
1568
  if radius == 1:
1544
1569
  neighborhood = list(self.G.neighbors(node_id))
1545
1570
  if include_center:
@@ -1556,28 +1581,61 @@ class NetworkGrid:
1556
1581
  def get_neighbors(
1557
1582
  self, node_id: int, include_center: bool = False, radius: int = 1
1558
1583
  ) -> list[Agent]:
1559
- """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
+ """
1560
1594
  neighborhood = self.get_neighborhood(node_id, include_center, radius)
1561
1595
  return self.get_cell_list_contents(neighborhood)
1562
1596
 
1563
1597
  def move_agent(self, agent: Agent, node_id: int) -> None:
1564
- """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
+ """
1565
1605
  self.remove_agent(agent)
1566
1606
  self.place_agent(agent, node_id)
1567
1607
 
1568
1608
  def remove_agent(self, agent: Agent) -> None:
1569
- """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
+ """
1570
1615
  node_id = agent.pos
1571
1616
  self.G.nodes[node_id]["agent"].remove(agent)
1572
1617
  agent.pos = None
1573
1618
 
1574
1619
  def is_cell_empty(self, node_id: int) -> bool:
1575
- """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
+ """
1576
1626
  return self.G.nodes[node_id]["agent"] == self.default_val()
1577
1627
 
1578
1628
  def get_cell_list_contents(self, cell_list: list[int]) -> list[Agent]:
1579
- """Returns a list of the agents contained in the nodes identified
1580
- 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
+
1581
1639
  """
1582
1640
  return list(self.iter_cell_list_contents(cell_list))
1583
1641
 
@@ -1586,8 +1644,16 @@ class NetworkGrid:
1586
1644
  return self.get_cell_list_contents(self.G)
1587
1645
 
1588
1646
  def iter_cell_list_contents(self, cell_list: list[int]) -> Iterator[Agent]:
1589
- """Returns an iterator of the agents contained in the nodes identified
1590
- 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
+
1591
1657
  """
1592
1658
  return itertools.chain.from_iterable(
1593
1659
  self.G.nodes[node_id]["agent"]