Mesa 3.0.0__py3-none-any.whl → 3.0.0a0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of Mesa might be problematic. Click here for more details.
- mesa/__init__.py +3 -3
- mesa/agent.py +114 -406
- mesa/batchrunner.py +27 -54
- mesa/cookiecutter-mesa/cookiecutter.json +8 -0
- mesa/cookiecutter-mesa/hooks/post_gen_project.py +11 -0
- mesa/cookiecutter-mesa/{{cookiecutter.snake}}/README.md +4 -0
- mesa/cookiecutter-mesa/{{cookiecutter.snake}}/app.pytemplate +27 -0
- mesa/cookiecutter-mesa/{{cookiecutter.snake}}/setup.pytemplate +11 -0
- mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/model.pytemplate +60 -0
- mesa/datacollection.py +29 -140
- mesa/experimental/__init__.py +1 -11
- mesa/experimental/cell_space/__init__.py +1 -16
- mesa/experimental/cell_space/cell.py +23 -93
- mesa/experimental/cell_space/cell_agent.py +21 -117
- mesa/experimental/cell_space/cell_collection.py +17 -54
- mesa/experimental/cell_space/discrete_space.py +8 -92
- mesa/experimental/cell_space/grid.py +8 -32
- mesa/experimental/cell_space/network.py +7 -12
- mesa/experimental/devs/__init__.py +0 -2
- mesa/experimental/devs/eventlist.py +14 -52
- mesa/experimental/devs/examples/epstein_civil_violence.py +39 -71
- mesa/experimental/devs/examples/wolf_sheep.py +45 -45
- mesa/experimental/devs/simulator.py +15 -55
- mesa/main.py +63 -0
- mesa/model.py +83 -211
- mesa/space.py +149 -215
- mesa/time.py +77 -62
- mesa/{experimental → visualization}/UserParam.py +6 -17
- mesa/visualization/__init__.py +2 -25
- mesa/{experimental → visualization}/components/altair.py +0 -10
- mesa/visualization/components/matplotlib.py +134 -0
- mesa/{experimental/solara_viz.py → visualization/jupyter_viz.py} +110 -65
- {mesa-3.0.0.dist-info → mesa-3.0.0a0.dist-info}/METADATA +13 -65
- mesa-3.0.0a0.dist-info/RECORD +38 -0
- mesa-3.0.0.dist-info/licenses/NOTICE → mesa-3.0.0a0.dist-info/licenses/LICENSE +2 -2
- mesa/examples/README.md +0 -37
- mesa/examples/__init__.py +0 -21
- mesa/examples/advanced/epstein_civil_violence/Epstein Civil Violence.ipynb +0 -116
- mesa/examples/advanced/epstein_civil_violence/Readme.md +0 -34
- mesa/examples/advanced/epstein_civil_violence/__init__.py +0 -0
- mesa/examples/advanced/epstein_civil_violence/agents.py +0 -164
- mesa/examples/advanced/epstein_civil_violence/app.py +0 -73
- mesa/examples/advanced/epstein_civil_violence/model.py +0 -114
- mesa/examples/advanced/pd_grid/Readme.md +0 -43
- mesa/examples/advanced/pd_grid/__init__.py +0 -0
- mesa/examples/advanced/pd_grid/agents.py +0 -50
- mesa/examples/advanced/pd_grid/analysis.ipynb +0 -228
- mesa/examples/advanced/pd_grid/app.py +0 -54
- mesa/examples/advanced/pd_grid/model.py +0 -71
- mesa/examples/advanced/sugarscape_g1mt/Readme.md +0 -64
- mesa/examples/advanced/sugarscape_g1mt/__init__.py +0 -0
- mesa/examples/advanced/sugarscape_g1mt/agents.py +0 -344
- mesa/examples/advanced/sugarscape_g1mt/app.py +0 -62
- mesa/examples/advanced/sugarscape_g1mt/model.py +0 -180
- mesa/examples/advanced/sugarscape_g1mt/sugar-map.txt +0 -50
- mesa/examples/advanced/sugarscape_g1mt/tests.py +0 -69
- mesa/examples/advanced/wolf_sheep/Readme.md +0 -57
- mesa/examples/advanced/wolf_sheep/__init__.py +0 -0
- mesa/examples/advanced/wolf_sheep/agents.py +0 -102
- mesa/examples/advanced/wolf_sheep/app.py +0 -84
- mesa/examples/advanced/wolf_sheep/model.py +0 -137
- mesa/examples/basic/__init__.py +0 -0
- mesa/examples/basic/boid_flockers/Readme.md +0 -22
- mesa/examples/basic/boid_flockers/__init__.py +0 -0
- mesa/examples/basic/boid_flockers/agents.py +0 -71
- mesa/examples/basic/boid_flockers/app.py +0 -58
- mesa/examples/basic/boid_flockers/model.py +0 -69
- mesa/examples/basic/boltzmann_wealth_model/Readme.md +0 -56
- mesa/examples/basic/boltzmann_wealth_model/__init__.py +0 -0
- mesa/examples/basic/boltzmann_wealth_model/agents.py +0 -31
- mesa/examples/basic/boltzmann_wealth_model/app.py +0 -74
- mesa/examples/basic/boltzmann_wealth_model/model.py +0 -43
- mesa/examples/basic/boltzmann_wealth_model/st_app.py +0 -115
- mesa/examples/basic/conways_game_of_life/Readme.md +0 -39
- mesa/examples/basic/conways_game_of_life/__init__.py +0 -0
- mesa/examples/basic/conways_game_of_life/agents.py +0 -47
- mesa/examples/basic/conways_game_of_life/app.py +0 -51
- mesa/examples/basic/conways_game_of_life/model.py +0 -31
- mesa/examples/basic/conways_game_of_life/st_app.py +0 -72
- mesa/examples/basic/schelling/Readme.md +0 -40
- mesa/examples/basic/schelling/__init__.py +0 -0
- mesa/examples/basic/schelling/agents.py +0 -26
- mesa/examples/basic/schelling/analysis.ipynb +0 -205
- mesa/examples/basic/schelling/app.py +0 -42
- mesa/examples/basic/schelling/model.py +0 -59
- mesa/examples/basic/virus_on_network/Readme.md +0 -61
- mesa/examples/basic/virus_on_network/__init__.py +0 -0
- mesa/examples/basic/virus_on_network/agents.py +0 -69
- mesa/examples/basic/virus_on_network/app.py +0 -114
- mesa/examples/basic/virus_on_network/model.py +0 -96
- mesa/experimental/cell_space/voronoi.py +0 -257
- mesa/experimental/components/matplotlib.py +0 -242
- mesa/visualization/components/__init__.py +0 -83
- mesa/visualization/components/altair_components.py +0 -188
- mesa/visualization/components/matplotlib_components.py +0 -175
- mesa/visualization/mpl_space_drawing.py +0 -593
- mesa/visualization/solara_viz.py +0 -458
- mesa/visualization/user_param.py +0 -69
- mesa/visualization/utils.py +0 -9
- mesa-3.0.0.dist-info/RECORD +0 -95
- mesa-3.0.0.dist-info/licenses/LICENSE +0 -202
- /mesa/{examples/advanced → cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}}/__init__.py +0 -0
- {mesa-3.0.0.dist-info → mesa-3.0.0a0.dist-info}/WHEEL +0 -0
- {mesa-3.0.0.dist-info → mesa-3.0.0a0.dist-info}/entry_points.txt +0 -0
mesa/space.py
CHANGED
|
@@ -1,24 +1,16 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""
|
|
2
|
+
Mesa Space Module
|
|
3
|
+
=================
|
|
2
4
|
|
|
3
5
|
Objects used to add a spatial component to a model.
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
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
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
"""
|
|
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
|
-
|
|
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
|
|
495
|
-
|
|
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
|
-
"""
|
|
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):
|
|
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
|
-
"""
|
|
599
|
-
|
|
600
|
-
|
|
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
|
-
|
|
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
|
-
"""
|
|
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__.
|
|
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__.
|
|
643
|
+
self.__class__.agentset_experimental_warning_given = True
|
|
664
644
|
|
|
665
645
|
def set_cell(self, position: Coordinate, value):
|
|
666
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
782
|
-
|
|
783
|
-
|
|
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
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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(
|
|
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
|
|
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
|
|
1120
|
-
|
|
1121
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1319
|
-
|
|
1320
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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"]
|