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
@@ -1,11 +1,16 @@
1
+ """DiscreteSpace base class."""
2
+
1
3
  from __future__ import annotations
2
4
 
5
+ from collections.abc import Callable
3
6
  from functools import cached_property
4
7
  from random import Random
5
- from typing import Generic, TypeVar
8
+ from typing import Any, Generic, TypeVar
6
9
 
10
+ from mesa.agent import AgentSet
7
11
  from mesa.experimental.cell_space.cell import Cell
8
12
  from mesa.experimental.cell_space.cell_collection import CellCollection
13
+ from mesa.space import PropertyLayer
9
14
 
10
15
  T = TypeVar("T", bound=Cell)
11
16
 
@@ -18,8 +23,8 @@ class DiscreteSpace(Generic[T]):
18
23
  all_cells (CellCollection): The cells composing the discrete space
19
24
  random (Random): The random number generator
20
25
  cell_klass (Type) : the type of cell class
21
- empties (CellCollection) : collecction of all cells that are empty
22
-
26
+ empties (CellCollection) : collection of all cells that are empty
27
+ property_layers (dict[str, PropertyLayer]): the property layers of the discrete space
23
28
  """
24
29
 
25
30
  def __init__(
@@ -28,6 +33,13 @@ class DiscreteSpace(Generic[T]):
28
33
  cell_klass: type[T] = Cell,
29
34
  random: Random | None = None,
30
35
  ):
36
+ """Instantiate a DiscreteSpace.
37
+
38
+ Args:
39
+ capacity: capacity of cells
40
+ cell_klass: base class for all cells
41
+ random: random number generator
42
+ """
31
43
  super().__init__()
32
44
  self.capacity = capacity
33
45
  self._cells: dict[tuple[int, ...], T] = {}
@@ -38,27 +50,99 @@ class DiscreteSpace(Generic[T]):
38
50
 
39
51
  self._empties: dict[tuple[int, ...], None] = {}
40
52
  self._empties_initialized = False
53
+ self.property_layers: dict[str, PropertyLayer] = {}
41
54
 
42
55
  @property
43
- def cutoff_empties(self):
56
+ def cutoff_empties(self): # noqa
44
57
  return 7.953 * len(self._cells) ** 0.384
45
58
 
59
+ @property
60
+ def agents(self) -> AgentSet:
61
+ """Return an AgentSet with the agents in the space."""
62
+ return AgentSet(self.all_cells.agents, random=self.random)
63
+
64
+ def _connect_cells(self): ...
46
65
  def _connect_single_cell(self, cell: T): ...
47
66
 
48
67
  @cached_property
49
68
  def all_cells(self):
69
+ """Return all cells in space."""
50
70
  return CellCollection({cell: cell.agents for cell in self._cells.values()})
51
71
 
52
- def __iter__(self):
72
+ def __iter__(self): # noqa
53
73
  return iter(self._cells.values())
54
74
 
55
- def __getitem__(self, key):
75
+ def __getitem__(self, key: tuple[int, ...]) -> T: # noqa: D105
56
76
  return self._cells[key]
57
77
 
58
78
  @property
59
- def empties(self) -> CellCollection:
79
+ def empties(self) -> CellCollection[T]:
80
+ """Return all empty in spaces."""
60
81
  return self.all_cells.select(lambda cell: cell.is_empty)
61
82
 
62
83
  def select_random_empty_cell(self) -> T:
63
- """select random empty cell"""
84
+ """Select random empty cell."""
64
85
  return self.random.choice(list(self.empties))
86
+
87
+ # PropertyLayer methods
88
+ def add_property_layer(
89
+ self, property_layer: PropertyLayer, add_to_cells: bool = True
90
+ ):
91
+ """Add a property layer to the grid.
92
+
93
+ Args:
94
+ property_layer: the property layer to add
95
+ add_to_cells: whether to add the property layer to all cells (default: True)
96
+ """
97
+ if property_layer.name in self.property_layers:
98
+ raise ValueError(f"Property layer {property_layer.name} already exists.")
99
+ self.property_layers[property_layer.name] = property_layer
100
+ if add_to_cells:
101
+ for cell in self._cells.values():
102
+ cell._mesa_property_layers[property_layer.name] = property_layer
103
+
104
+ def remove_property_layer(self, property_name: str, remove_from_cells: bool = True):
105
+ """Remove a property layer from the grid.
106
+
107
+ Args:
108
+ property_name: the name of the property layer to remove
109
+ remove_from_cells: whether to remove the property layer from all cells (default: True)
110
+ """
111
+ del self.property_layers[property_name]
112
+ if remove_from_cells:
113
+ for cell in self._cells.values():
114
+ del cell._mesa_property_layers[property_name]
115
+
116
+ def set_property(
117
+ self, property_name: str, value, condition: Callable[[T], bool] | None = None
118
+ ):
119
+ """Set the value of a property for all cells in the grid.
120
+
121
+ Args:
122
+ property_name: the name of the property to set
123
+ value: the value to set
124
+ condition: a function that takes a cell and returns a boolean
125
+ """
126
+ self.property_layers[property_name].set_cells(value, condition)
127
+
128
+ def modify_properties(
129
+ self,
130
+ property_name: str,
131
+ operation: Callable,
132
+ value: Any = None,
133
+ condition: Callable[[T], bool] | None = None,
134
+ ):
135
+ """Modify the values of a specific property for all cells in the grid.
136
+
137
+ Args:
138
+ property_name: the name of the property to modify
139
+ operation: the operation to perform
140
+ value: the value to use in the operation
141
+ condition: a function that takes a cell and returns a boolean (used to filter cells)
142
+ """
143
+ self.property_layers[property_name].modify_cells(operation, value, condition)
144
+
145
+ def __setstate__(self, state):
146
+ """Set the state of the discrete space and rebuild the connections."""
147
+ self.__dict__ = state
148
+ self._connect_cells()
@@ -1,3 +1,5 @@
1
+ """Various Grid Spaces."""
2
+
1
3
  from __future__ import annotations
2
4
 
3
5
  from collections.abc import Sequence
@@ -10,8 +12,8 @@ from mesa.experimental.cell_space import Cell, DiscreteSpace
10
12
  T = TypeVar("T", bound=Cell)
11
13
 
12
14
 
13
- class Grid(DiscreteSpace, Generic[T]):
14
- """Base class for all grid classes
15
+ class Grid(DiscreteSpace[T], Generic[T]):
16
+ """Base class for all grid classes.
15
17
 
16
18
  Attributes:
17
19
  dimensions (Sequence[int]): the dimensions of the grid
@@ -20,8 +22,21 @@ class Grid(DiscreteSpace, Generic[T]):
20
22
  random (Random): the random number generator
21
23
  _try_random (bool): whether to get empty cell be repeatedly trying random cell
22
24
 
25
+ Notes:
26
+ width and height are accessible via properties, higher dimensions can be retrieved via dimensions
27
+
23
28
  """
24
29
 
30
+ @property
31
+ def width(self) -> int:
32
+ """Convenience access to the width of the grid."""
33
+ return self.dimensions[0]
34
+
35
+ @property
36
+ def height(self) -> int:
37
+ """Convenience access to the height of the grid."""
38
+ return self.dimensions[1]
39
+
25
40
  def __init__(
26
41
  self,
27
42
  dimensions: Sequence[int],
@@ -30,6 +45,15 @@ class Grid(DiscreteSpace, Generic[T]):
30
45
  random: Random | None = None,
31
46
  cell_klass: type[T] = Cell,
32
47
  ) -> None:
48
+ """Initialise the grid class.
49
+
50
+ Args:
51
+ dimensions: the dimensions of the space
52
+ torus: whether the space wraps
53
+ capacity: capacity of the grid cell
54
+ random: a random number generator
55
+ cell_klass: the base class to use for the cells
56
+ """
33
57
  super().__init__(capacity=capacity, random=random, cell_klass=cell_klass)
34
58
  self.torus = torus
35
59
  self.dimensions = dimensions
@@ -60,10 +84,10 @@ class Grid(DiscreteSpace, Generic[T]):
60
84
  raise ValueError("Dimensions must be a list of positive integers.")
61
85
  if not isinstance(self.torus, bool):
62
86
  raise ValueError("Torus must be a boolean.")
63
- if self.capacity is not None and not isinstance(self.capacity, (float, int)):
87
+ if self.capacity is not None and not isinstance(self.capacity, float | int):
64
88
  raise ValueError("Capacity must be a number or None.")
65
89
 
66
- def select_random_empty_cell(self) -> T:
90
+ def select_random_empty_cell(self) -> T: # noqa
67
91
  # FIXME:: currently just a simple boolean to control behavior
68
92
  # FIXME:: basically if grid is close to 99% full, creating empty list can be faster
69
93
  # FIXME:: note however that the old results don't apply because in this implementation
@@ -89,7 +113,7 @@ class Grid(DiscreteSpace, Generic[T]):
89
113
  if self.torus:
90
114
  n_coord = tuple(nc % d for nc, d in zip(n_coord, self.dimensions))
91
115
  if all(0 <= nc < d for nc, d in zip(n_coord, self.dimensions)):
92
- cell.connect(self._cells[n_coord])
116
+ cell.connect(self._cells[n_coord], d_coord)
93
117
 
94
118
  def _connect_single_cell_2d(self, cell: T, offsets: list[tuple[int, int]]) -> None:
95
119
  i, j = cell.coordinate
@@ -100,7 +124,7 @@ class Grid(DiscreteSpace, Generic[T]):
100
124
  if self.torus:
101
125
  ni, nj = ni % height, nj % width
102
126
  if 0 <= ni < height and 0 <= nj < width:
103
- cell.connect(self._cells[ni, nj])
127
+ cell.connect(self._cells[ni, nj], (di, dj))
104
128
 
105
129
 
106
130
  class OrthogonalMooreGrid(Grid[T]):
@@ -122,7 +146,6 @@ class OrthogonalMooreGrid(Grid[T]):
122
146
  ( 1, -1), ( 1, 0), ( 1, 1),
123
147
  ]
124
148
  # fmt: on
125
- height, width = self.dimensions
126
149
 
127
150
  for cell in self.all_cells:
128
151
  self._connect_single_cell_2d(cell, offsets)
@@ -154,13 +177,12 @@ class OrthogonalVonNeumannGrid(Grid[T]):
154
177
  ( 1, 0),
155
178
  ]
156
179
  # fmt: on
157
- height, width = self.dimensions
158
180
 
159
181
  for cell in self.all_cells:
160
182
  self._connect_single_cell_2d(cell, offsets)
161
183
 
162
184
  def _connect_cells_nd(self) -> None:
163
- offsets = []
185
+ offsets: list[tuple[int, ...]] = []
164
186
  dimensions = len(self.dimensions)
165
187
  for dim in range(dimensions):
166
188
  for delta in [
@@ -176,6 +198,8 @@ class OrthogonalVonNeumannGrid(Grid[T]):
176
198
 
177
199
 
178
200
  class HexGrid(Grid[T]):
201
+ """A Grid with hexagonal tilling of the space."""
202
+
179
203
  def _connect_cells_2d(self) -> None:
180
204
  # fmt: off
181
205
  even_offsets = [
@@ -1,27 +1,29 @@
1
+ """A Network grid."""
2
+
1
3
  from random import Random
2
- from typing import Any, Optional
4
+ from typing import Any
3
5
 
4
6
  from mesa.experimental.cell_space.cell import Cell
5
7
  from mesa.experimental.cell_space.discrete_space import DiscreteSpace
6
8
 
7
9
 
8
- class Network(DiscreteSpace):
9
- """A networked discrete space"""
10
+ class Network(DiscreteSpace[Cell]):
11
+ """A networked discrete space."""
10
12
 
11
13
  def __init__(
12
14
  self,
13
15
  G: Any, # noqa: N803
14
- capacity: Optional[int] = None,
15
- random: Optional[Random] = None,
16
+ capacity: int | None = None,
17
+ random: Random | None = None,
16
18
  cell_klass: type[Cell] = Cell,
17
19
  ) -> None:
18
- """A Networked grid
20
+ """A Networked grid.
19
21
 
20
22
  Args:
21
23
  G: a NetworkX Graph instance.
22
24
  capacity (int) : the capacity of the cell
23
- random (Random):
24
- CellKlass (type[Cell]): The base Cell class to use in the Network
25
+ random (Random): a random number generator
26
+ cell_klass (type[Cell]): The base Cell class to use in the Network
25
27
 
26
28
  """
27
29
  super().__init__(capacity=capacity, random=random, cell_klass=cell_klass)
@@ -32,9 +34,12 @@ class Network(DiscreteSpace):
32
34
  node_id, capacity, random=self.random
33
35
  )
34
36
 
37
+ self._connect_cells()
38
+
39
+ def _connect_cells(self) -> None:
35
40
  for cell in self.all_cells:
36
41
  self._connect_single_cell(cell)
37
42
 
38
- def _connect_single_cell(self, cell):
43
+ def _connect_single_cell(self, cell: Cell):
39
44
  for node_id in self.G.neighbors(cell.coordinate):
40
- cell.connect(self._cells[node_id])
45
+ cell.connect(self._cells[node_id], node_id)
@@ -0,0 +1,257 @@
1
+ """Support for Voronoi meshed grids."""
2
+
3
+ from collections.abc import Sequence
4
+ from itertools import combinations
5
+ from random import Random
6
+
7
+ import numpy as np
8
+
9
+ from mesa.experimental.cell_space.cell import Cell
10
+ from mesa.experimental.cell_space.discrete_space import DiscreteSpace
11
+
12
+
13
+ class Delaunay:
14
+ """Class to compute a Delaunay triangulation in 2D.
15
+
16
+ ref: http://github.com/jmespadero/pyDelaunay2D
17
+ """
18
+
19
+ def __init__(self, center: tuple = (0, 0), radius: int = 9999) -> None:
20
+ """Init and create a new frame to contain the triangulation.
21
+
22
+ Args:
23
+ center: Optional position for the center of the frame. Default (0,0)
24
+ radius: Optional distance from corners to the center.
25
+ """
26
+ center = np.asarray(center)
27
+ # Create coordinates for the corners of the frame
28
+ self.coords = [
29
+ center + radius * np.array((-1, -1)),
30
+ center + radius * np.array((+1, -1)),
31
+ center + radius * np.array((+1, +1)),
32
+ center + radius * np.array((-1, +1)),
33
+ ]
34
+
35
+ # Create two dicts to store triangle neighbours and circumcircles.
36
+ self.triangles = {}
37
+ self.circles = {}
38
+
39
+ # Create two CCW triangles for the frame
40
+ triangle1 = (0, 1, 3)
41
+ triangle2 = (2, 3, 1)
42
+ self.triangles[triangle1] = [triangle2, None, None]
43
+ self.triangles[triangle2] = [triangle1, None, None]
44
+
45
+ # Compute circumcenters and circumradius for each triangle
46
+ for t in self.triangles:
47
+ self.circles[t] = self._circumcenter(t)
48
+
49
+ def _circumcenter(self, triangle: list) -> tuple:
50
+ """Compute circumcenter and circumradius of a triangle in 2D."""
51
+ points = np.asarray([self.coords[v] for v in triangle])
52
+ points2 = np.dot(points, points.T)
53
+ a = np.bmat([[2 * points2, [[1], [1], [1]]], [[[1, 1, 1, 0]]]])
54
+
55
+ b = np.hstack((np.sum(points * points, axis=1), [1]))
56
+ x = np.linalg.solve(a, b)
57
+ bary_coords = x[:-1]
58
+ center = np.dot(bary_coords, points)
59
+
60
+ radius = np.sum(np.square(points[0] - center)) # squared distance
61
+ return (center, radius)
62
+
63
+ def _in_circle(self, triangle: list, point: list) -> bool:
64
+ """Check if point p is inside of precomputed circumcircle of triangle."""
65
+ center, radius = self.circles[triangle]
66
+ return np.sum(np.square(center - point)) <= radius
67
+
68
+ def add_point(self, point: Sequence) -> None:
69
+ """Add a point to the current DT, and refine it using Bowyer-Watson."""
70
+ point_index = len(self.coords)
71
+ self.coords.append(np.asarray(point))
72
+
73
+ bad_triangles = []
74
+ for triangle in self.triangles:
75
+ if self._in_circle(triangle, point):
76
+ bad_triangles.append(triangle)
77
+
78
+ boundary = []
79
+ triangle = bad_triangles[0]
80
+ edge = 0
81
+
82
+ while True:
83
+ opposite_triangle = self.triangles[triangle][edge]
84
+ if opposite_triangle not in bad_triangles:
85
+ boundary.append(
86
+ (
87
+ triangle[(edge + 1) % 3],
88
+ triangle[(edge - 1) % 3],
89
+ opposite_triangle,
90
+ )
91
+ )
92
+ edge = (edge + 1) % 3
93
+ if boundary[0][0] == boundary[-1][1]:
94
+ break
95
+ else:
96
+ edge = (self.triangles[opposite_triangle].index(triangle) + 1) % 3
97
+ triangle = opposite_triangle
98
+
99
+ for triangle in bad_triangles:
100
+ del self.triangles[triangle]
101
+ del self.circles[triangle]
102
+
103
+ new_triangles = []
104
+ for e0, e1, opposite_triangle in boundary:
105
+ triangle = (point_index, e0, e1)
106
+ self.circles[triangle] = self._circumcenter(triangle)
107
+ self.triangles[triangle] = [opposite_triangle, None, None]
108
+ if opposite_triangle:
109
+ for i, neighbor in enumerate(self.triangles[opposite_triangle]):
110
+ if neighbor and e1 in neighbor and e0 in neighbor:
111
+ self.triangles[opposite_triangle][i] = triangle
112
+
113
+ new_triangles.append(triangle)
114
+
115
+ n = len(new_triangles)
116
+ for i, triangle in enumerate(new_triangles):
117
+ self.triangles[triangle][1] = new_triangles[(i + 1) % n] # next
118
+ self.triangles[triangle][2] = new_triangles[(i - 1) % n] # previous
119
+
120
+ def export_triangles(self) -> list:
121
+ """Export the current list of Delaunay triangles."""
122
+ triangles_list = [
123
+ (a - 4, b - 4, c - 4)
124
+ for (a, b, c) in self.triangles
125
+ if a > 3 and b > 3 and c > 3
126
+ ]
127
+ return triangles_list
128
+
129
+ def export_voronoi_regions(self):
130
+ """Export coordinates and regions of Voronoi diagram as indexed data."""
131
+ use_vertex = {i: [] for i in range(len(self.coords))}
132
+ vor_coors = []
133
+ index = {}
134
+ for triangle_index, (a, b, c) in enumerate(sorted(self.triangles)):
135
+ vor_coors.append(self.circles[(a, b, c)][0])
136
+ use_vertex[a] += [(b, c, a)]
137
+ use_vertex[b] += [(c, a, b)]
138
+ use_vertex[c] += [(a, b, c)]
139
+
140
+ index[(a, b, c)] = triangle_index
141
+ index[(c, a, b)] = triangle_index
142
+ index[(b, c, a)] = triangle_index
143
+
144
+ regions = {}
145
+ for i in range(4, len(self.coords)):
146
+ vertex = use_vertex[i][0][0]
147
+ region = []
148
+ for _ in range(len(use_vertex[i])):
149
+ triangle = next(
150
+ triangle for triangle in use_vertex[i] if triangle[0] == vertex
151
+ )
152
+ region.append(index[triangle])
153
+ vertex = triangle[1]
154
+ regions[i - 4] = region
155
+
156
+ return vor_coors, regions
157
+
158
+
159
+ def round_float(x: float) -> int: # noqa
160
+ return int(x * 500)
161
+
162
+
163
+ class VoronoiGrid(DiscreteSpace):
164
+ """Voronoi meshed GridSpace."""
165
+
166
+ triangulation: Delaunay
167
+ voronoi_coordinates: list
168
+ regions: list
169
+
170
+ def __init__(
171
+ self,
172
+ centroids_coordinates: Sequence[Sequence[float]],
173
+ capacity: float | None = None,
174
+ random: Random | None = None,
175
+ cell_klass: type[Cell] = Cell,
176
+ capacity_function: callable = round_float,
177
+ cell_coloring_property: str | None = None,
178
+ ) -> None:
179
+ """A Voronoi Tessellation Grid.
180
+
181
+ Given a set of points, this class creates a grid where a cell is centered in each point,
182
+ its neighbors are given by Voronoi Tessellation cells neighbors
183
+ and the capacity by the polygon area.
184
+
185
+ Args:
186
+ centroids_coordinates: coordinates of centroids to build the tessellation space
187
+ capacity (int) : capacity of the cells in the discrete space
188
+ random (Random): random number generator
189
+ cell_klass (type[Cell]): type of cell class
190
+ capacity_function (Callable): function to compute (int) capacity according to (float) area
191
+ cell_coloring_property (str): voronoi visualization polygon fill property
192
+ """
193
+ super().__init__(capacity=capacity, random=random, cell_klass=cell_klass)
194
+ self.centroids_coordinates = centroids_coordinates
195
+ self._validate_parameters()
196
+
197
+ self._cells = {
198
+ i: cell_klass(self.centroids_coordinates[i], capacity, random=self.random)
199
+ for i in range(len(self.centroids_coordinates))
200
+ }
201
+
202
+ self.regions = None
203
+ self.triangulation = None
204
+ self.voronoi_coordinates = None
205
+ self.capacity_function = capacity_function
206
+ self.cell_coloring_property = cell_coloring_property
207
+
208
+ self._connect_cells()
209
+ self._build_cell_polygons()
210
+
211
+ def _connect_cells(self) -> None:
212
+ """Connect cells to neighbors based on given centroids and using Delaunay Triangulation."""
213
+ self.triangulation = Delaunay()
214
+ for centroid in self.centroids_coordinates:
215
+ self.triangulation.add_point(centroid)
216
+
217
+ for point in self.triangulation.export_triangles():
218
+ for i, j in combinations(point, 2):
219
+ self._cells[i].connect(self._cells[j], (i, j))
220
+ self._cells[j].connect(self._cells[i], (j, i))
221
+
222
+ def _validate_parameters(self) -> None:
223
+ if self.capacity is not None and not isinstance(self.capacity, float | int):
224
+ raise ValueError("Capacity must be a number or None.")
225
+ if not isinstance(self.centroids_coordinates, Sequence) or not isinstance(
226
+ self.centroids_coordinates[0], Sequence
227
+ ):
228
+ raise ValueError("Centroids should be a list of lists")
229
+ dimension_1 = len(self.centroids_coordinates[0])
230
+ for coordinate in self.centroids_coordinates:
231
+ if dimension_1 != len(coordinate):
232
+ raise ValueError("Centroid coordinates should be a homogeneous array")
233
+
234
+ def _get_voronoi_regions(self) -> tuple:
235
+ if self.voronoi_coordinates is None or self.regions is None:
236
+ (
237
+ self.voronoi_coordinates,
238
+ self.regions,
239
+ ) = self.triangulation.export_voronoi_regions()
240
+ return self.voronoi_coordinates, self.regions
241
+
242
+ @staticmethod
243
+ def _compute_polygon_area(polygon: list) -> float:
244
+ polygon = np.array(polygon)
245
+ x = polygon[:, 0]
246
+ y = polygon[:, 1]
247
+ return 0.5 * np.abs(np.dot(x, np.roll(y, 1)) - np.dot(y, np.roll(x, 1)))
248
+
249
+ def _build_cell_polygons(self):
250
+ coordinates, regions = self._get_voronoi_regions()
251
+ for region in regions:
252
+ polygon = [coordinates[i] for i in regions[region]]
253
+ self._cells[region].properties["polygon"] = polygon
254
+ polygon_area = self._compute_polygon_area(polygon)
255
+ self._cells[region].properties["area"] = polygon_area
256
+ self._cells[region].capacity = self.capacity_function(polygon_area)
257
+ self._cells[region].properties[self.cell_coloring_property] = 0
@@ -1,5 +1,6 @@
1
+ """Altair components."""
2
+
1
3
  import contextlib
2
- from typing import Optional
3
4
 
4
5
  import solara
5
6
 
@@ -8,7 +9,15 @@ with contextlib.suppress(ImportError):
8
9
 
9
10
 
10
11
  @solara.component
11
- def SpaceAltair(model, agent_portrayal, dependencies: Optional[list[any]] = None):
12
+ def SpaceAltair(model, agent_portrayal, dependencies: list[any] | None = None):
13
+ """A component that renders a Space using Altair.
14
+
15
+ Args:
16
+ model: a model instance
17
+ agent_portrayal: agent portray specification
18
+ dependencies: optional list of dependencies (currently not used)
19
+
20
+ """
12
21
  space = getattr(model, "grid", None)
13
22
  if space is None:
14
23
  # Sometimes the space is defined as model.space instead of model.grid