Mesa 3.0.0__py3-none-any.whl → 3.0.0a1__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 (104) hide show
  1. mesa/__init__.py +3 -3
  2. mesa/agent.py +114 -406
  3. mesa/batchrunner.py +27 -54
  4. mesa/cookiecutter-mesa/cookiecutter.json +8 -0
  5. mesa/cookiecutter-mesa/hooks/post_gen_project.py +11 -0
  6. mesa/cookiecutter-mesa/{{cookiecutter.snake}}/README.md +4 -0
  7. mesa/cookiecutter-mesa/{{cookiecutter.snake}}/app.pytemplate +27 -0
  8. mesa/cookiecutter-mesa/{{cookiecutter.snake}}/setup.pytemplate +11 -0
  9. mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/model.pytemplate +60 -0
  10. mesa/datacollection.py +29 -140
  11. mesa/experimental/__init__.py +1 -11
  12. mesa/experimental/cell_space/__init__.py +1 -16
  13. mesa/experimental/cell_space/cell.py +23 -93
  14. mesa/experimental/cell_space/cell_agent.py +21 -117
  15. mesa/experimental/cell_space/cell_collection.py +17 -54
  16. mesa/experimental/cell_space/discrete_space.py +8 -92
  17. mesa/experimental/cell_space/grid.py +8 -32
  18. mesa/experimental/cell_space/network.py +7 -12
  19. mesa/experimental/devs/__init__.py +0 -2
  20. mesa/experimental/devs/eventlist.py +14 -52
  21. mesa/experimental/devs/examples/epstein_civil_violence.py +39 -71
  22. mesa/experimental/devs/examples/wolf_sheep.py +45 -45
  23. mesa/experimental/devs/simulator.py +15 -55
  24. mesa/main.py +63 -0
  25. mesa/model.py +83 -211
  26. mesa/space.py +149 -215
  27. mesa/time.py +77 -62
  28. mesa/{experimental → visualization}/UserParam.py +6 -17
  29. mesa/visualization/__init__.py +2 -25
  30. mesa/{experimental → visualization}/components/altair.py +0 -10
  31. mesa/visualization/components/matplotlib.py +134 -0
  32. mesa/visualization/solara_viz.py +266 -267
  33. {mesa-3.0.0.dist-info → mesa-3.0.0a1.dist-info}/METADATA +13 -65
  34. mesa-3.0.0a1.dist-info/RECORD +38 -0
  35. mesa-3.0.0.dist-info/licenses/NOTICE → mesa-3.0.0a1.dist-info/licenses/LICENSE +2 -2
  36. mesa/examples/README.md +0 -37
  37. mesa/examples/__init__.py +0 -21
  38. mesa/examples/advanced/epstein_civil_violence/Epstein Civil Violence.ipynb +0 -116
  39. mesa/examples/advanced/epstein_civil_violence/Readme.md +0 -34
  40. mesa/examples/advanced/epstein_civil_violence/__init__.py +0 -0
  41. mesa/examples/advanced/epstein_civil_violence/agents.py +0 -164
  42. mesa/examples/advanced/epstein_civil_violence/app.py +0 -73
  43. mesa/examples/advanced/epstein_civil_violence/model.py +0 -114
  44. mesa/examples/advanced/pd_grid/Readme.md +0 -43
  45. mesa/examples/advanced/pd_grid/__init__.py +0 -0
  46. mesa/examples/advanced/pd_grid/agents.py +0 -50
  47. mesa/examples/advanced/pd_grid/analysis.ipynb +0 -228
  48. mesa/examples/advanced/pd_grid/app.py +0 -54
  49. mesa/examples/advanced/pd_grid/model.py +0 -71
  50. mesa/examples/advanced/sugarscape_g1mt/Readme.md +0 -64
  51. mesa/examples/advanced/sugarscape_g1mt/__init__.py +0 -0
  52. mesa/examples/advanced/sugarscape_g1mt/agents.py +0 -344
  53. mesa/examples/advanced/sugarscape_g1mt/app.py +0 -62
  54. mesa/examples/advanced/sugarscape_g1mt/model.py +0 -180
  55. mesa/examples/advanced/sugarscape_g1mt/sugar-map.txt +0 -50
  56. mesa/examples/advanced/sugarscape_g1mt/tests.py +0 -69
  57. mesa/examples/advanced/wolf_sheep/Readme.md +0 -57
  58. mesa/examples/advanced/wolf_sheep/__init__.py +0 -0
  59. mesa/examples/advanced/wolf_sheep/agents.py +0 -102
  60. mesa/examples/advanced/wolf_sheep/app.py +0 -84
  61. mesa/examples/advanced/wolf_sheep/model.py +0 -137
  62. mesa/examples/basic/__init__.py +0 -0
  63. mesa/examples/basic/boid_flockers/Readme.md +0 -22
  64. mesa/examples/basic/boid_flockers/__init__.py +0 -0
  65. mesa/examples/basic/boid_flockers/agents.py +0 -71
  66. mesa/examples/basic/boid_flockers/app.py +0 -58
  67. mesa/examples/basic/boid_flockers/model.py +0 -69
  68. mesa/examples/basic/boltzmann_wealth_model/Readme.md +0 -56
  69. mesa/examples/basic/boltzmann_wealth_model/__init__.py +0 -0
  70. mesa/examples/basic/boltzmann_wealth_model/agents.py +0 -31
  71. mesa/examples/basic/boltzmann_wealth_model/app.py +0 -74
  72. mesa/examples/basic/boltzmann_wealth_model/model.py +0 -43
  73. mesa/examples/basic/boltzmann_wealth_model/st_app.py +0 -115
  74. mesa/examples/basic/conways_game_of_life/Readme.md +0 -39
  75. mesa/examples/basic/conways_game_of_life/__init__.py +0 -0
  76. mesa/examples/basic/conways_game_of_life/agents.py +0 -47
  77. mesa/examples/basic/conways_game_of_life/app.py +0 -51
  78. mesa/examples/basic/conways_game_of_life/model.py +0 -31
  79. mesa/examples/basic/conways_game_of_life/st_app.py +0 -72
  80. mesa/examples/basic/schelling/Readme.md +0 -40
  81. mesa/examples/basic/schelling/__init__.py +0 -0
  82. mesa/examples/basic/schelling/agents.py +0 -26
  83. mesa/examples/basic/schelling/analysis.ipynb +0 -205
  84. mesa/examples/basic/schelling/app.py +0 -42
  85. mesa/examples/basic/schelling/model.py +0 -59
  86. mesa/examples/basic/virus_on_network/Readme.md +0 -61
  87. mesa/examples/basic/virus_on_network/__init__.py +0 -0
  88. mesa/examples/basic/virus_on_network/agents.py +0 -69
  89. mesa/examples/basic/virus_on_network/app.py +0 -114
  90. mesa/examples/basic/virus_on_network/model.py +0 -96
  91. mesa/experimental/cell_space/voronoi.py +0 -257
  92. mesa/experimental/components/matplotlib.py +0 -242
  93. mesa/experimental/solara_viz.py +0 -453
  94. mesa/visualization/components/__init__.py +0 -83
  95. mesa/visualization/components/altair_components.py +0 -188
  96. mesa/visualization/components/matplotlib_components.py +0 -175
  97. mesa/visualization/mpl_space_drawing.py +0 -593
  98. mesa/visualization/user_param.py +0 -69
  99. mesa/visualization/utils.py +0 -9
  100. mesa-3.0.0.dist-info/RECORD +0 -95
  101. mesa-3.0.0.dist-info/licenses/LICENSE +0 -202
  102. /mesa/{examples/advanced → cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}}/__init__.py +0 -0
  103. {mesa-3.0.0.dist-info → mesa-3.0.0a1.dist-info}/WHEEL +0 -0
  104. {mesa-3.0.0.dist-info → mesa-3.0.0a1.dist-info}/entry_points.txt +0 -0
mesa/space.py CHANGED
@@ -1,24 +1,16 @@
1
- """Mesa Space Module.
1
+ """
2
+ Mesa Space Module
3
+ =================
2
4
 
3
5
  Objects used to add a spatial component to a model.
4
6
 
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.
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.
22
14
  """
23
15
 
24
16
  # Mypy; for the `|` operator purpose
@@ -43,7 +35,7 @@ import numpy as np
43
35
  import numpy.typing as npt
44
36
 
45
37
  # For Mypy
46
- from .agent import Agent, AgentSet
38
+ from .agent import Agent
47
39
 
48
40
  # for better performance, we calculate the tuple to use in the is_integer function
49
41
  _types_integer = (int, np.integer)
@@ -62,10 +54,9 @@ F = TypeVar("F", bound=Callable[..., Any])
62
54
 
63
55
 
64
56
  def accept_tuple_argument(wrapped_function: F) -> F:
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
- """
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."""
69
60
 
70
61
  def wrapper(grid_instance, positions) -> Any:
71
62
  if len(positions) == 2 and not isinstance(positions[0], tuple):
@@ -76,13 +67,11 @@ def accept_tuple_argument(wrapped_function: F) -> F:
76
67
 
77
68
 
78
69
  def is_integer(x: Real) -> bool:
79
- """Check if x is either a CPython integer or Numpy integer."""
70
+ # Check if x is either a CPython integer or Numpy integer.
80
71
  return isinstance(x, _types_integer)
81
72
 
82
73
 
83
74
  def warn_if_agent_has_position_already(placement_func):
84
- """Decorator to give warning if agent has position already set."""
85
-
86
75
  def wrapper(self, agent, *args, **kwargs):
87
76
  if agent.pos is not None:
88
77
  warnings.warn(
@@ -113,8 +102,7 @@ class _Grid:
113
102
  """Create a new grid.
114
103
 
115
104
  Args:
116
- width: The grid's width.
117
- height: The grid's height.
105
+ width, height: The width and height of the grid
118
106
  torus: Boolean whether the grid wraps or not.
119
107
  """
120
108
  self.height = height
@@ -164,26 +152,6 @@ class _Grid:
164
152
  @overload
165
153
  def __getitem__(self, index: int | Sequence[Coordinate]) -> list[GridContent]: ...
166
154
 
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
-
187
155
  @overload
188
156
  def __getitem__(
189
157
  self, index: tuple[int | slice, int | slice]
@@ -191,6 +159,7 @@ class _Grid:
191
159
 
192
160
  def __getitem__(self, index):
193
161
  """Access contents from the grid."""
162
+
194
163
  if isinstance(index, int):
195
164
  # grid[x]
196
165
  return self._grid[index]
@@ -223,7 +192,8 @@ class _Grid:
223
192
  return [cell for rows in self._grid[x] for cell in rows[y]]
224
193
 
225
194
  def __iter__(self) -> Iterator[GridContent]:
226
- """Create an iterator that chains the rows of the grid together as if it is one list."""
195
+ """Create an iterator that chains the rows of the grid together
196
+ as if it is one list:"""
227
197
  return itertools.chain(*self._grid)
228
198
 
229
199
  def coord_iter(self) -> Iterator[tuple[GridContent, Coordinate]]:
@@ -239,7 +209,8 @@ class _Grid:
239
209
  include_center: bool = False,
240
210
  radius: int = 1,
241
211
  ) -> Iterator[Coordinate]:
242
- """Return an iterator over cell coordinates that are in the neighborhood of a certain point.
212
+ """Return an iterator over cell coordinates that are in the
213
+ neighborhood of a certain point.
243
214
 
244
215
  Args:
245
216
  pos: Coordinate tuple for the neighborhood to get.
@@ -266,7 +237,8 @@ class _Grid:
266
237
  include_center: bool = False,
267
238
  radius: int = 1,
268
239
  ) -> Sequence[Coordinate]:
269
- """Return a list of cells that are in the neighborhood of a certain point.
240
+ """Return a list of cells that are in the neighborhood of a
241
+ certain point.
270
242
 
271
243
  Args:
272
244
  pos: Coordinate tuple for the neighborhood to get.
@@ -409,7 +381,8 @@ class _Grid:
409
381
  return pos[0] % self.width, pos[1] % self.height
410
382
 
411
383
  def out_of_bounds(self, pos: Coordinate) -> bool:
412
- """Determines whether position is off the grid, returns the out of bounds coordinate."""
384
+ """Determines whether position is off the grid, returns the out of
385
+ bounds coordinate."""
413
386
  x, y = pos
414
387
  return x < 0 or x >= self.width or y < 0 or y >= self.height
415
388
 
@@ -417,7 +390,8 @@ class _Grid:
417
390
  def iter_cell_list_contents(
418
391
  self, cell_list: Iterable[Coordinate]
419
392
  ) -> Iterator[Agent]:
420
- """Returns an iterator of the agents contained in the cells identified in `cell_list`; cells with empty content are excluded.
393
+ """Returns an iterator of the agents contained in the cells identified
394
+ in `cell_list`; cells with empty content are excluded.
421
395
 
422
396
  Args:
423
397
  cell_list: Array-like of (x, y) tuples, or single tuple.
@@ -433,7 +407,8 @@ class _Grid:
433
407
 
434
408
  @accept_tuple_argument
435
409
  def get_cell_list_contents(self, cell_list: Iterable[Coordinate]) -> list[Agent]:
436
- """Returns an iterator of the agents contained in the cells identified in `cell_list`; cells with empty content are excluded.
410
+ """Returns an iterator of the agents contained in the cells identified
411
+ in `cell_list`; cells with empty content are excluded.
437
412
 
438
413
  Args:
439
414
  cell_list: Array-like of (x, y) tuples, or single tuple.
@@ -466,7 +441,8 @@ class _Grid:
466
441
  selection: str = "random",
467
442
  handle_empty: str | None = None,
468
443
  ) -> None:
469
- """Move an agent to one of the given positions.
444
+ """
445
+ Move an agent to one of the given positions.
470
446
 
471
447
  Args:
472
448
  agent: Agent object to move. Assumed to have its current location stored in a 'pos' tuple.
@@ -483,21 +459,15 @@ class _Grid:
483
459
  elif selection == "closest":
484
460
  current_pos = agent.pos
485
461
  # Find the closest position without sorting all positions
486
- # TODO: See if this method can be optimized further
487
- closest_pos = []
462
+ closest_pos = None
488
463
  min_distance = float("inf")
489
464
  agent.random.shuffle(pos)
490
465
  for p in pos:
491
466
  distance = self._distance_squared(p, current_pos)
492
467
  if distance < min_distance:
493
468
  min_distance = distance
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
-
469
+ closest_pos = p
470
+ chosen_pos = closest_pos
501
471
  else:
502
472
  raise ValueError(
503
473
  f"Invalid selection method {selection}. Choose 'random' or 'closest'."
@@ -518,7 +488,9 @@ class _Grid:
518
488
  )
519
489
 
520
490
  def _distance_squared(self, pos1: Coordinate, pos2: Coordinate) -> float:
521
- """Calculate the squared Euclidean distance between two points for performance."""
491
+ """
492
+ Calculate the squared Euclidean distance between two points for performance.
493
+ """
522
494
  # Use squared Euclidean distance to avoid sqrt operation
523
495
  dx, dy = abs(pos1[0] - pos2[0]), abs(pos1[1] - pos2[1])
524
496
  if self.torus:
@@ -527,7 +499,7 @@ class _Grid:
527
499
  return dx**2 + dy**2
528
500
 
529
501
  def swap_pos(self, agent_a: Agent, agent_b: Agent) -> None:
530
- """Swap agents positions."""
502
+ """Swap agents positions"""
531
503
  agents_no_pos = []
532
504
  if (pos_a := agent_a.pos) is None:
533
505
  agents_no_pos.append(agent_a)
@@ -588,16 +560,16 @@ def is_single_argument_function(function):
588
560
  )
589
561
 
590
562
 
591
- def ufunc_requires_additional_input(ufunc): # noqa: D103
563
+ def ufunc_requires_additional_input(ufunc):
592
564
  # NumPy ufuncs have a 'nargs' attribute indicating the number of input arguments
593
565
  # For binary ufuncs (like np.add), nargs is 2
594
566
  return ufunc.nargs > 1
595
567
 
596
568
 
597
569
  class PropertyLayer:
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.
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.
601
573
 
602
574
  Attributes:
603
575
  name (str): The name of the property layer.
@@ -605,14 +577,22 @@ class PropertyLayer:
605
577
  height (int): The height of the grid (number of rows).
606
578
  data (numpy.ndarray): A NumPy array representing the grid data.
607
579
 
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.
608
587
  """
609
588
 
610
- propertylayer_experimental_warning_given = False
589
+ agentset_experimental_warning_given = False
611
590
 
612
591
  def __init__(
613
592
  self, name: str, width: int, height: int, default_value, dtype=np.float64
614
593
  ):
615
- """Initializes a new PropertyLayer instance.
594
+ """
595
+ Initializes a new PropertyLayer instance.
616
596
 
617
597
  Args:
618
598
  name (str): The name of the property layer.
@@ -653,21 +633,24 @@ class PropertyLayer:
653
633
 
654
634
  self.data = np.full((width, height), default_value, dtype=dtype)
655
635
 
656
- if not self.__class__.propertylayer_experimental_warning_given:
636
+ if not self.__class__.agentset_experimental_warning_given:
657
637
  warnings.warn(
658
638
  "The new PropertyLayer and _PropertyGrid classes experimental. It may be changed or removed in any and all future releases, including patch releases.\n"
659
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",
660
640
  FutureWarning,
661
641
  stacklevel=2,
662
642
  )
663
- self.__class__.propertylayer_experimental_warning_given = True
643
+ self.__class__.agentset_experimental_warning_given = True
664
644
 
665
645
  def set_cell(self, position: Coordinate, value):
666
- """Update a single cell's value in-place."""
646
+ """
647
+ Update a single cell's value in-place.
648
+ """
667
649
  self.data[position] = value
668
650
 
669
651
  def set_cells(self, value, condition=None):
670
- """Perform a batch update either on the entire grid or conditionally, in-place.
652
+ """
653
+ Perform a batch update either on the entire grid or conditionally, in-place.
671
654
 
672
655
  Args:
673
656
  value: The value to be used for the update.
@@ -696,8 +679,8 @@ class PropertyLayer:
696
679
  np.copyto(self.data, value, where=condition_result)
697
680
 
698
681
  def modify_cell(self, position: Coordinate, operation, value=None):
699
- """Modify a single cell using an operation, which can be a lambda function or a NumPy ufunc.
700
-
682
+ """
683
+ Modify a single cell using an operation, which can be a lambda function or a NumPy ufunc.
701
684
  If a NumPy ufunc is used, an additional value should be provided.
702
685
 
703
686
  Args:
@@ -718,8 +701,8 @@ class PropertyLayer:
718
701
  raise ValueError("Invalid operation or missing value for NumPy ufunc.")
719
702
 
720
703
  def modify_cells(self, operation, value=None, condition_function=None):
721
- """Modify cells using an operation, which can be a lambda function or a NumPy ufunc.
722
-
704
+ """
705
+ Modify cells using an operation, which can be a lambda function or a NumPy ufunc.
723
706
  If a NumPy ufunc is used, an additional value should be provided.
724
707
 
725
708
  Args:
@@ -753,7 +736,8 @@ class PropertyLayer:
753
736
  self.data = np.where(condition_array, modified_data, self.data)
754
737
 
755
738
  def select_cells(self, condition, return_list=True):
756
- """Find cells that meet a specified condition using NumPy's boolean indexing, in-place.
739
+ """
740
+ Find cells that meet a specified condition using NumPy's boolean indexing, in-place.
757
741
 
758
742
  Args:
759
743
  condition: A callable that returns a boolean array when applied to the data.
@@ -778,9 +762,9 @@ class PropertyLayer:
778
762
 
779
763
 
780
764
  class _PropertyGrid(_Grid):
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
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
784
768
  intended for internal use within the Mesa framework and is currently utilized by SingleGrid
785
769
  and MultiGrid classes to provide enhanced grid functionality.
786
770
 
@@ -793,15 +777,22 @@ class _PropertyGrid(_Grid):
793
777
  properties (dict): A dictionary mapping property layer names to PropertyLayer instances.
794
778
  empty_mask (np.ndarray): A boolean array indicating empty cells on the grid.
795
779
 
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:
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:
805
796
  This class is not intended for direct use in user models but is currently used by the SingleGrid and MultiGrid.
806
797
  """
807
798
 
@@ -812,7 +803,8 @@ class _PropertyGrid(_Grid):
812
803
  torus: bool,
813
804
  property_layers: None | PropertyLayer | list[PropertyLayer] = None,
814
805
  ):
815
- """Initializes a new _PropertyGrid instance with specified dimensions and optional property layers.
806
+ """
807
+ Initializes a new _PropertyGrid instance with specified dimensions and optional property layers.
816
808
 
817
809
  Args:
818
810
  width (int): The width of the grid (number of columns).
@@ -841,12 +833,15 @@ class _PropertyGrid(_Grid):
841
833
 
842
834
  @property
843
835
  def empty_mask(self) -> np.ndarray:
844
- """Returns a boolean mask indicating empty cells on the grid."""
836
+ """
837
+ Returns a boolean mask indicating empty cells on the grid.
838
+ """
845
839
  return self._empty_mask
846
840
 
847
841
  # Add and remove properties to the grid
848
842
  def add_property_layer(self, property_layer: PropertyLayer):
849
- """Adds a new property layer to the grid.
843
+ """
844
+ Adds a new property layer to the grid.
850
845
 
851
846
  Args:
852
847
  property_layer (PropertyLayer): The PropertyLayer instance to be added to the grid.
@@ -864,7 +859,8 @@ class _PropertyGrid(_Grid):
864
859
  self.properties[property_layer.name] = property_layer
865
860
 
866
861
  def remove_property_layer(self, property_name: str):
867
- """Removes a property layer from the grid by its name.
862
+ """
863
+ Removes a property layer from the grid by its name.
868
864
 
869
865
  Args:
870
866
  property_name (str): The name of the property layer to be removed.
@@ -879,8 +875,8 @@ class _PropertyGrid(_Grid):
879
875
  def get_neighborhood_mask(
880
876
  self, pos: Coordinate, moore: bool, include_center: bool, radius: int
881
877
  ) -> np.ndarray:
882
- """Generate a boolean mask representing the neighborhood.
883
-
878
+ """
879
+ Generate a boolean mask representing the neighborhood.
884
880
  Helper method for select_cells_multi_properties() and move_agent_to_random_cell()
885
881
 
886
882
  Args:
@@ -908,7 +904,8 @@ class _PropertyGrid(_Grid):
908
904
  only_empty: bool = False,
909
905
  return_list: bool = True,
910
906
  ) -> list[Coordinate] | np.ndarray:
911
- """Select cells based on property conditions, extreme values, and/or masks, with an option to only select empty cells.
907
+ """
908
+ Select cells based on property conditions, extreme values, and/or masks, with an option to only select empty cells.
912
909
 
913
910
  Args:
914
911
  conditions (dict): A dictionary where keys are property names and values are callables that return a boolean when applied.
@@ -1061,7 +1058,7 @@ class MultiGrid(_PropertyGrid):
1061
1058
  self._empty_mask[agent.pos] = False
1062
1059
  agent.pos = None
1063
1060
 
1064
- def iter_neighbors( # noqa: D102
1061
+ def iter_neighbors(
1065
1062
  self,
1066
1063
  pos: Coordinate,
1067
1064
  moore: bool,
@@ -1076,9 +1073,8 @@ class MultiGrid(_PropertyGrid):
1076
1073
  def iter_cell_list_contents(
1077
1074
  self, cell_list: Iterable[Coordinate]
1078
1075
  ) -> Iterator[Agent]:
1079
- """Returns an iterator of the agents contained in the cells identified in `cell_list`.
1080
-
1081
- Cells with empty content are excluded.
1076
+ """Returns an iterator of the agents contained in the cells identified
1077
+ in `cell_list`; cells with empty content are excluded.
1082
1078
 
1083
1079
  Args:
1084
1080
  cell_list: Array-like of (x, y) tuples, or single tuple.
@@ -1116,9 +1112,9 @@ class _HexGrid:
1116
1112
  def get_neighborhood(
1117
1113
  self, pos: Coordinate, include_center: bool = False, radius: int = 1
1118
1114
  ) -> list[Coordinate]:
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
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
1122
1118
  important, the neighborhood can be sketched as:
1123
1119
 
1124
1120
  Always: (0,-), (0,+)
@@ -1204,7 +1200,8 @@ class _HexGrid:
1204
1200
  def iter_neighborhood(
1205
1201
  self, pos: Coordinate, include_center: bool = False, radius: int = 1
1206
1202
  ) -> Iterator[Coordinate]:
1207
- """Return an iterator over cell coordinates that are in the neighborhood of a certain point.
1203
+ """Return an iterator over cell coordinates that are in the
1204
+ neighborhood of a certain point.
1208
1205
 
1209
1206
  Args:
1210
1207
  pos: Coordinate tuple for the neighborhood to get.
@@ -1254,7 +1251,8 @@ class _HexGrid:
1254
1251
 
1255
1252
 
1256
1253
  class HexSingleGrid(_HexGrid, SingleGrid):
1257
- """Hexagonal SingleGrid: a SingleGrid where neighbors are computed according to a hexagonal tiling of the grid.
1254
+ """Hexagonal SingleGrid: a SingleGrid where neighbors are computed
1255
+ according to a hexagonal tiling of the grid.
1258
1256
 
1259
1257
  Functions according to odd-q rules.
1260
1258
  See http://www.redblobgames.com/grids/hexagons/#coordinates for more.
@@ -1270,7 +1268,8 @@ class HexSingleGrid(_HexGrid, SingleGrid):
1270
1268
 
1271
1269
 
1272
1270
  class HexMultiGrid(_HexGrid, MultiGrid):
1273
- """Hexagonal MultiGrid: a MultiGrid where neighbors are computed according to a hexagonal tiling of the grid.
1271
+ """Hexagonal MultiGrid: a MultiGrid where neighbors are computed
1272
+ according to a hexagonal tiling of the grid.
1274
1273
 
1275
1274
  Functions according to odd-q rules.
1276
1275
  See http://www.redblobgames.com/grids/hexagons/#coordinates for more.
@@ -1287,6 +1286,30 @@ class HexMultiGrid(_HexGrid, MultiGrid):
1287
1286
  """
1288
1287
 
1289
1288
 
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
+
1290
1313
  class ContinuousSpace:
1291
1314
  """Continuous space where each agent can have an arbitrary position.
1292
1315
 
@@ -1312,13 +1335,11 @@ class ContinuousSpace:
1312
1335
  """Create a new continuous space.
1313
1336
 
1314
1337
  Args:
1315
- x_max: the maximum x-coordinate
1316
- y_max: the maximum y-coordinate.
1338
+ x_max, y_max: Maximum x and y coordinates for the space.
1317
1339
  torus: Boolean for whether the edges loop around.
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.
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.
1322
1343
  """
1323
1344
  self.x_min = x_min
1324
1345
  self.x_max = x_max
@@ -1334,19 +1355,6 @@ class ContinuousSpace:
1334
1355
  self._index_to_agent: dict[int, Agent] = {}
1335
1356
  self._agent_to_index: dict[Agent, int | None] = {}
1336
1357
 
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
-
1350
1358
  def _build_agent_cache(self):
1351
1359
  """Cache agents positions to speed up neighbors calculations."""
1352
1360
  self._index_to_agent = {}
@@ -1434,13 +1442,11 @@ class ContinuousSpace:
1434
1442
  self, pos_1: FloatCoordinate, pos_2: FloatCoordinate
1435
1443
  ) -> FloatCoordinate:
1436
1444
  """Get the heading vector between two points, accounting for toroidal space.
1437
-
1438
1445
  It is possible to calculate the heading angle by applying the atan2 function to the
1439
1446
  result.
1440
1447
 
1441
1448
  Args:
1442
- pos_1: Coordinate tuples for both points.
1443
- pos_2: Coordinate tuples for both points.
1449
+ pos_1, pos_2: Coordinate tuples for both points.
1444
1450
  """
1445
1451
  one = np.array(pos_1)
1446
1452
  two = np.array(pos_2)
@@ -1466,8 +1472,7 @@ class ContinuousSpace:
1466
1472
  """Get the distance between two point, accounting for toroidal space.
1467
1473
 
1468
1474
  Args:
1469
- pos_1: Coordinate tuples for point1.
1470
- pos_2: Coordinate tuples for point2.
1475
+ pos_1, pos_2: Coordinate tuples for both points.
1471
1476
  """
1472
1477
  x1, y1 = pos_1
1473
1478
  x2, y2 = pos_2
@@ -1514,33 +1519,12 @@ class NetworkGrid:
1514
1519
  """Create a new network.
1515
1520
 
1516
1521
  Args:
1517
- g: a NetworkX graph instance.
1522
+ G: a NetworkX graph instance.
1518
1523
  """
1519
1524
  self.G = g
1520
1525
  for node_id in self.G.nodes:
1521
1526
  g.nodes[node_id]["agent"] = self.default_val()
1522
1527
 
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
-
1544
1528
  @staticmethod
1545
1529
  def default_val() -> list:
1546
1530
  """Default value for a new node."""
@@ -1555,16 +1539,7 @@ class NetworkGrid:
1555
1539
  def get_neighborhood(
1556
1540
  self, node_id: int, include_center: bool = False, radius: int = 1
1557
1541
  ) -> list[int]:
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
- """
1542
+ """Get all adjacent nodes within a certain radius"""
1568
1543
  if radius == 1:
1569
1544
  neighborhood = list(self.G.neighbors(node_id))
1570
1545
  if include_center:
@@ -1581,61 +1556,28 @@ class NetworkGrid:
1581
1556
  def get_neighbors(
1582
1557
  self, node_id: int, include_center: bool = False, radius: int = 1
1583
1558
  ) -> list[Agent]:
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
- """
1559
+ """Get all agents in adjacent nodes (within a certain radius)."""
1594
1560
  neighborhood = self.get_neighborhood(node_id, include_center, radius)
1595
1561
  return self.get_cell_list_contents(neighborhood)
1596
1562
 
1597
1563
  def move_agent(self, agent: Agent, node_id: int) -> None:
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
- """
1564
+ """Move an agent from its current node to a new node."""
1605
1565
  self.remove_agent(agent)
1606
1566
  self.place_agent(agent, node_id)
1607
1567
 
1608
1568
  def remove_agent(self, agent: Agent) -> None:
1609
- """Remove the agent from the network and set its pos attribute to None.
1610
-
1611
- Args:
1612
- agent: agent instance
1613
-
1614
- """
1569
+ """Remove the agent from the network and set its pos attribute to None."""
1615
1570
  node_id = agent.pos
1616
1571
  self.G.nodes[node_id]["agent"].remove(agent)
1617
1572
  agent.pos = None
1618
1573
 
1619
1574
  def is_cell_empty(self, node_id: int) -> bool:
1620
- """Returns a bool of the contents of a cell.
1621
-
1622
- Args:
1623
- node_id: id of node
1624
-
1625
- """
1575
+ """Returns a bool of the contents of a cell."""
1626
1576
  return self.G.nodes[node_id]["agent"] == self.default_val()
1627
1577
 
1628
1578
  def get_cell_list_contents(self, cell_list: list[int]) -> list[Agent]:
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
-
1579
+ """Returns a list of the agents contained in the nodes identified
1580
+ in `cell_list`; nodes with empty content are excluded.
1639
1581
  """
1640
1582
  return list(self.iter_cell_list_contents(cell_list))
1641
1583
 
@@ -1644,16 +1586,8 @@ class NetworkGrid:
1644
1586
  return self.get_cell_list_contents(self.G)
1645
1587
 
1646
1588
  def iter_cell_list_contents(self, cell_list: list[int]) -> Iterator[Agent]:
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
-
1589
+ """Returns an iterator of the agents contained in the nodes identified
1590
+ in `cell_list`; nodes with empty content are excluded.
1657
1591
  """
1658
1592
  return itertools.chain.from_iterable(
1659
1593
  self.G.nodes[node_id]["agent"]