Mesa 3.0.0a4__py3-none-any.whl → 3.0.0b0__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 (41) hide show
  1. mesa/__init__.py +2 -3
  2. mesa/agent.py +116 -85
  3. mesa/batchrunner.py +22 -23
  4. mesa/cookiecutter-mesa/hooks/post_gen_project.py +2 -0
  5. mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/__init__.py +1 -0
  6. mesa/datacollection.py +138 -27
  7. mesa/experimental/UserParam.py +17 -6
  8. mesa/experimental/__init__.py +2 -0
  9. mesa/experimental/cell_space/__init__.py +14 -1
  10. mesa/experimental/cell_space/cell.py +84 -23
  11. mesa/experimental/cell_space/cell_agent.py +117 -21
  12. mesa/experimental/cell_space/cell_collection.py +54 -17
  13. mesa/experimental/cell_space/discrete_space.py +79 -7
  14. mesa/experimental/cell_space/grid.py +19 -8
  15. mesa/experimental/cell_space/network.py +9 -7
  16. mesa/experimental/cell_space/voronoi.py +26 -33
  17. mesa/experimental/components/altair.py +10 -0
  18. mesa/experimental/components/matplotlib.py +18 -0
  19. mesa/experimental/devs/__init__.py +2 -0
  20. mesa/experimental/devs/eventlist.py +36 -15
  21. mesa/experimental/devs/examples/epstein_civil_violence.py +65 -29
  22. mesa/experimental/devs/examples/wolf_sheep.py +40 -35
  23. mesa/experimental/devs/simulator.py +55 -15
  24. mesa/experimental/solara_viz.py +10 -19
  25. mesa/main.py +6 -4
  26. mesa/model.py +51 -54
  27. mesa/space.py +145 -120
  28. mesa/time.py +57 -67
  29. mesa/visualization/UserParam.py +19 -6
  30. mesa/visualization/__init__.py +3 -2
  31. mesa/visualization/components/altair.py +4 -2
  32. mesa/visualization/components/matplotlib.py +176 -85
  33. mesa/visualization/solara_viz.py +167 -84
  34. mesa/visualization/utils.py +3 -1
  35. {mesa-3.0.0a4.dist-info → mesa-3.0.0b0.dist-info}/METADATA +55 -13
  36. mesa-3.0.0b0.dist-info/RECORD +45 -0
  37. mesa-3.0.0b0.dist-info/licenses/LICENSE +202 -0
  38. mesa-3.0.0a4.dist-info/licenses/LICENSE → mesa-3.0.0b0.dist-info/licenses/NOTICE +2 -2
  39. mesa-3.0.0a4.dist-info/RECORD +0 -44
  40. {mesa-3.0.0a4.dist-info → mesa-3.0.0b0.dist-info}/WHEEL +0 -0
  41. {mesa-3.0.0a4.dist-info → mesa-3.0.0b0.dist-info}/entry_points.txt +0 -0
@@ -1,37 +1,133 @@
1
+ """An agent with movement methods for cell spaces."""
2
+
1
3
  from __future__ import annotations
2
4
 
3
- from typing import TYPE_CHECKING
5
+ from typing import TYPE_CHECKING, Protocol
4
6
 
5
- from mesa import Agent, Model
7
+ from mesa.agent import Agent
6
8
 
7
9
  if TYPE_CHECKING:
8
- from mesa.experimental.cell_space.cell import Cell
10
+ from mesa.experimental.cell_space import Cell
9
11
 
10
12
 
11
- class CellAgent(Agent):
12
- """Cell Agent is an extension of the Agent class and adds behavior for moving in discrete spaces
13
+ class HasCellProtocol(Protocol):
14
+ """Protocol for discrete space cell holders."""
13
15
 
16
+ cell: Cell
14
17
 
15
- Attributes:
16
- unique_id (int): A unique identifier for this agent.
17
- model (Model): The model instance to which the agent belongs
18
- pos: (Position | None): The position of the agent in the space
19
- cell: (Cell | None): the cell which the agent occupies
20
- """
21
18
 
22
- def __init__(self, model: Model) -> None:
23
- """
24
- Create a new agent.
19
+ class HasCell:
20
+ """Descriptor for cell movement behavior."""
25
21
 
26
- Args:
27
- unique_id (int): A unique identifier for this agent.
28
- model (Model): The model instance in which the agent exists.
29
- """
30
- super().__init__(model)
31
- self.cell: Cell | None = None
22
+ _mesa_cell: Cell | None = None
32
23
 
33
- def move_to(self, cell) -> None:
24
+ @property
25
+ def cell(self) -> Cell | None: # noqa: D102
26
+ return self._mesa_cell
27
+
28
+ @cell.setter
29
+ def cell(self, cell: Cell | None) -> None:
30
+ # remove from current cell
34
31
  if self.cell is not None:
35
32
  self.cell.remove_agent(self)
33
+
34
+ # update private attribute
35
+ self._mesa_cell = cell
36
+
37
+ # add to new cell
38
+ if cell is not None:
39
+ cell.add_agent(self)
40
+
41
+
42
+ class BasicMovement:
43
+ """Mixin for moving agents in discrete space."""
44
+
45
+ def move_to(self: HasCellProtocol, cell: Cell) -> None:
46
+ """Move to a new cell."""
36
47
  self.cell = cell
48
+
49
+ def move_relative(self: HasCellProtocol, direction: tuple[int, ...]):
50
+ """Move to a cell relative to the current cell.
51
+
52
+ Args:
53
+ direction: The direction to move in.
54
+ """
55
+ new_cell = self.cell.connections.get(direction)
56
+ if new_cell is not None:
57
+ self.cell = new_cell
58
+ else:
59
+ raise ValueError(f"No cell in direction {direction}")
60
+
61
+
62
+ class FixedCell(HasCell):
63
+ """Mixin for agents that are fixed to a cell."""
64
+
65
+ @property
66
+ def cell(self) -> Cell | None: # noqa: D102
67
+ return self._mesa_cell
68
+
69
+ @cell.setter
70
+ def cell(self, cell: Cell) -> None:
71
+ if self.cell is not None:
72
+ raise ValueError("Cannot move agent in FixedCell")
73
+ self._mesa_cell = cell
74
+
37
75
  cell.add_agent(self)
76
+
77
+
78
+ class CellAgent(Agent, HasCell, BasicMovement):
79
+ """Cell Agent is an extension of the Agent class and adds behavior for moving in discrete spaces.
80
+
81
+ Attributes:
82
+ cell (Cell): The cell the agent is currently in.
83
+ """
84
+
85
+ def remove(self):
86
+ """Remove the agent from the model."""
87
+ super().remove()
88
+ self.cell = None # ensures that we are also removed from cell
89
+
90
+
91
+ class FixedAgent(Agent, FixedCell):
92
+ """A patch in a 2D grid."""
93
+
94
+ def remove(self):
95
+ """Remove the agent from the model."""
96
+ super().remove()
97
+
98
+ # fixme we leave self._mesa_cell on the original value
99
+ # so you cannot hijack remove() to move patches
100
+ self.cell.remove_agent(self)
101
+
102
+
103
+ class Grid2DMovingAgent(CellAgent):
104
+ """Mixin for moving agents in 2D grids."""
105
+
106
+ # fmt: off
107
+ DIRECTION_MAP = {
108
+ "n": (-1, 0), "north": (-1, 0), "up": (-1, 0),
109
+ "s": (1, 0), "south": (1, 0), "down": (1, 0),
110
+ "e": (0, 1), "east": (0, 1), "right": (0, 1),
111
+ "w": (0, -1), "west": (0, -1), "left": (0, -1),
112
+ "ne": (-1, 1), "northeast": (-1, 1), "upright": (-1, 1),
113
+ "nw": (-1, -1), "northwest": (-1, -1), "upleft": (-1, -1),
114
+ "se": (1, 1), "southeast": (1, 1), "downright": (1, 1),
115
+ "sw": (1, -1), "southwest": (1, -1), "downleft": (1, -1)
116
+ }
117
+ # fmt: on
118
+
119
+ def move(self, direction: str, distance: int = 1):
120
+ """Move the agent in a cardinal direction.
121
+
122
+ Args:
123
+ direction: The cardinal direction to move in.
124
+ distance: The distance to move.
125
+ """
126
+ direction = direction.lower() # Convert direction to lowercase
127
+
128
+ if direction not in self.DIRECTION_MAP:
129
+ raise ValueError(f"Invalid direction: {direction}")
130
+
131
+ move_vector = self.DIRECTION_MAP[direction]
132
+ for _ in range(distance):
133
+ self.move_relative(move_vector)
@@ -1,3 +1,5 @@
1
+ """CellCollection class."""
2
+
1
3
  from __future__ import annotations
2
4
 
3
5
  import itertools
@@ -14,7 +16,7 @@ T = TypeVar("T", bound="Cell")
14
16
 
15
17
 
16
18
  class CellCollection(Generic[T]):
17
- """An immutable collection of cells
19
+ """An immutable collection of cells.
18
20
 
19
21
  Attributes:
20
22
  cells (List[Cell]): The list of cells this collection represents
@@ -28,6 +30,12 @@ class CellCollection(Generic[T]):
28
30
  cells: Mapping[T, list[CellAgent]] | Iterable[T],
29
31
  random: Random | None = None,
30
32
  ) -> None:
33
+ """Initialize a CellCollection.
34
+
35
+ Args:
36
+ cells: cells to add to the collection
37
+ random: a seeded random number generator.
38
+ """
31
39
  if isinstance(cells, dict):
32
40
  self._cells = cells
33
41
  else:
@@ -40,42 +48,71 @@ class CellCollection(Generic[T]):
40
48
  random = Random() # FIXME
41
49
  self.random = random
42
50
 
43
- def __iter__(self):
51
+ def __iter__(self): # noqa
44
52
  return iter(self._cells)
45
53
 
46
- def __getitem__(self, key: T) -> Iterable[CellAgent]:
54
+ def __getitem__(self, key: T) -> Iterable[CellAgent]: # noqa
47
55
  return self._cells[key]
48
56
 
49
57
  # @cached_property
50
- def __len__(self) -> int:
58
+ def __len__(self) -> int: # noqa
51
59
  return len(self._cells)
52
60
 
53
- def __repr__(self):
61
+ def __repr__(self): # noqa
54
62
  return f"CellCollection({self._cells})"
55
63
 
56
64
  @cached_property
57
- def cells(self) -> list[T]:
65
+ def cells(self) -> list[T]: # noqa
58
66
  return list(self._cells.keys())
59
67
 
60
68
  @property
61
- def agents(self) -> Iterable[CellAgent]:
69
+ def agents(self) -> Iterable[CellAgent]: # noqa
62
70
  return itertools.chain.from_iterable(self._cells.values())
63
71
 
64
72
  def select_random_cell(self) -> T:
73
+ """Select a random cell."""
65
74
  return self.random.choice(self.cells)
66
75
 
67
76
  def select_random_agent(self) -> CellAgent:
77
+ """Select a random agent.
78
+
79
+ Returns:
80
+ CellAgent instance
81
+
82
+
83
+ """
68
84
  return self.random.choice(list(self.agents))
69
85
 
70
- def select(self, filter_func: Callable[[T], bool] | None = None, n=0):
71
- # FIXME: n is not considered
72
- if filter_func is None and n == 0:
86
+ def select(
87
+ self,
88
+ filter_func: Callable[[T], bool] | None = None,
89
+ at_most: int | float = float("inf"),
90
+ ):
91
+ """Select cells based on filter function.
92
+
93
+ Args:
94
+ filter_func: filter function
95
+ at_most: The maximum amount of cells to select. Defaults to infinity.
96
+ - If an integer, at most the first number of matching cells is selected.
97
+ - If a float between 0 and 1, at most that fraction of original number of cells
98
+
99
+ Returns:
100
+ CellCollection
101
+
102
+ """
103
+ if filter_func is None and at_most == float("inf"):
73
104
  return self
74
105
 
75
- return CellCollection(
76
- {
77
- cell: agents
78
- for cell, agents in self._cells.items()
79
- if filter_func is None or filter_func(cell)
80
- }
81
- )
106
+ if at_most <= 1.0 and isinstance(at_most, float):
107
+ at_most = int(len(self) * at_most) # Note that it rounds down (floor)
108
+
109
+ def cell_generator(filter_func, at_most):
110
+ count = 0
111
+ for cell in self:
112
+ if count >= at_most:
113
+ break
114
+ if not filter_func or filter_func(cell):
115
+ yield cell
116
+ count += 1
117
+
118
+ return CellCollection(cell_generator(filter_func, at_most))
@@ -1,11 +1,15 @@
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
 
7
10
  from mesa.experimental.cell_space.cell import Cell
8
11
  from mesa.experimental.cell_space.cell_collection import CellCollection
12
+ from mesa.space import PropertyLayer
9
13
 
10
14
  T = TypeVar("T", bound=Cell)
11
15
 
@@ -19,7 +23,7 @@ class DiscreteSpace(Generic[T]):
19
23
  random (Random): The random number generator
20
24
  cell_klass (Type) : the type of cell class
21
25
  empties (CellCollection) : collecction of all cells that are empty
22
-
26
+ property_layers (dict[str, PropertyLayer]): the property layers of the discrete space
23
27
  """
24
28
 
25
29
  def __init__(
@@ -28,6 +32,13 @@ class DiscreteSpace(Generic[T]):
28
32
  cell_klass: type[T] = Cell,
29
33
  random: Random | None = None,
30
34
  ):
35
+ """Instantiate a DiscreteSpace.
36
+
37
+ Args:
38
+ capacity: capacity of cells
39
+ cell_klass: base class for all cells
40
+ random: random number generator
41
+ """
31
42
  super().__init__()
32
43
  self.capacity = capacity
33
44
  self._cells: dict[tuple[int, ...], T] = {}
@@ -38,27 +49,88 @@ class DiscreteSpace(Generic[T]):
38
49
 
39
50
  self._empties: dict[tuple[int, ...], None] = {}
40
51
  self._empties_initialized = False
52
+ self.property_layers: dict[str, PropertyLayer] = {}
41
53
 
42
54
  @property
43
- def cutoff_empties(self):
55
+ def cutoff_empties(self): # noqa
44
56
  return 7.953 * len(self._cells) ** 0.384
45
57
 
46
58
  def _connect_single_cell(self, cell: T): ...
47
59
 
48
60
  @cached_property
49
61
  def all_cells(self):
62
+ """Return all cells in space."""
50
63
  return CellCollection({cell: cell.agents for cell in self._cells.values()})
51
64
 
52
- def __iter__(self):
65
+ def __iter__(self): # noqa
53
66
  return iter(self._cells.values())
54
67
 
55
- def __getitem__(self, key):
68
+ def __getitem__(self, key: tuple[int, ...]) -> T: # noqa: D105
56
69
  return self._cells[key]
57
70
 
58
71
  @property
59
- def empties(self) -> CellCollection:
72
+ def empties(self) -> CellCollection[T]:
73
+ """Return all empty in spaces."""
60
74
  return self.all_cells.select(lambda cell: cell.is_empty)
61
75
 
62
76
  def select_random_empty_cell(self) -> T:
63
- """select random empty cell"""
77
+ """Select random empty cell."""
64
78
  return self.random.choice(list(self.empties))
79
+
80
+ # PropertyLayer methods
81
+ def add_property_layer(
82
+ self, property_layer: PropertyLayer, add_to_cells: bool = True
83
+ ):
84
+ """Add a property layer to the grid.
85
+
86
+ Args:
87
+ property_layer: the property layer to add
88
+ add_to_cells: whether to add the property layer to all cells (default: True)
89
+ """
90
+ if property_layer.name in self.property_layers:
91
+ raise ValueError(f"Property layer {property_layer.name} already exists.")
92
+ self.property_layers[property_layer.name] = property_layer
93
+ if add_to_cells:
94
+ for cell in self._cells.values():
95
+ cell._mesa_property_layers[property_layer.name] = property_layer
96
+
97
+ def remove_property_layer(self, property_name: str, remove_from_cells: bool = True):
98
+ """Remove a property layer from the grid.
99
+
100
+ Args:
101
+ property_name: the name of the property layer to remove
102
+ remove_from_cells: whether to remove the property layer from all cells (default: True)
103
+ """
104
+ del self.property_layers[property_name]
105
+ if remove_from_cells:
106
+ for cell in self._cells.values():
107
+ del cell._mesa_property_layers[property_name]
108
+
109
+ def set_property(
110
+ self, property_name: str, value, condition: Callable[[T], bool] | None = None
111
+ ):
112
+ """Set the value of a property for all cells in the grid.
113
+
114
+ Args:
115
+ property_name: the name of the property to set
116
+ value: the value to set
117
+ condition: a function that takes a cell and returns a boolean
118
+ """
119
+ self.property_layers[property_name].set_cells(value, condition)
120
+
121
+ def modify_properties(
122
+ self,
123
+ property_name: str,
124
+ operation: Callable,
125
+ value: Any = None,
126
+ condition: Callable[[T], bool] | None = None,
127
+ ):
128
+ """Modify the values of a specific property for all cells in the grid.
129
+
130
+ Args:
131
+ property_name: the name of the property to modify
132
+ operation: the operation to perform
133
+ value: the value to use in the operation
134
+ condition: a function that takes a cell and returns a boolean (used to filter cells)
135
+ """
136
+ self.property_layers[property_name].modify_cells(operation, value, condition)
@@ -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
@@ -30,6 +32,15 @@ class Grid(DiscreteSpace, Generic[T]):
30
32
  random: Random | None = None,
31
33
  cell_klass: type[T] = Cell,
32
34
  ) -> None:
35
+ """Initialise the grid class.
36
+
37
+ Args:
38
+ dimensions: the dimensions of the space
39
+ torus: whether the space wraps
40
+ capacity: capacity of the grid cell
41
+ random: a random number generator
42
+ cell_klass: the base class to use for the cells
43
+ """
33
44
  super().__init__(capacity=capacity, random=random, cell_klass=cell_klass)
34
45
  self.torus = torus
35
46
  self.dimensions = dimensions
@@ -63,7 +74,7 @@ class Grid(DiscreteSpace, Generic[T]):
63
74
  if self.capacity is not None and not isinstance(self.capacity, float | int):
64
75
  raise ValueError("Capacity must be a number or None.")
65
76
 
66
- def select_random_empty_cell(self) -> T:
77
+ def select_random_empty_cell(self) -> T: # noqa
67
78
  # FIXME:: currently just a simple boolean to control behavior
68
79
  # FIXME:: basically if grid is close to 99% full, creating empty list can be faster
69
80
  # FIXME:: note however that the old results don't apply because in this implementation
@@ -89,7 +100,7 @@ class Grid(DiscreteSpace, Generic[T]):
89
100
  if self.torus:
90
101
  n_coord = tuple(nc % d for nc, d in zip(n_coord, self.dimensions))
91
102
  if all(0 <= nc < d for nc, d in zip(n_coord, self.dimensions)):
92
- cell.connect(self._cells[n_coord])
103
+ cell.connect(self._cells[n_coord], d_coord)
93
104
 
94
105
  def _connect_single_cell_2d(self, cell: T, offsets: list[tuple[int, int]]) -> None:
95
106
  i, j = cell.coordinate
@@ -100,7 +111,7 @@ class Grid(DiscreteSpace, Generic[T]):
100
111
  if self.torus:
101
112
  ni, nj = ni % height, nj % width
102
113
  if 0 <= ni < height and 0 <= nj < width:
103
- cell.connect(self._cells[ni, nj])
114
+ cell.connect(self._cells[ni, nj], (di, dj))
104
115
 
105
116
 
106
117
  class OrthogonalMooreGrid(Grid[T]):
@@ -122,7 +133,6 @@ class OrthogonalMooreGrid(Grid[T]):
122
133
  ( 1, -1), ( 1, 0), ( 1, 1),
123
134
  ]
124
135
  # fmt: on
125
- height, width = self.dimensions
126
136
 
127
137
  for cell in self.all_cells:
128
138
  self._connect_single_cell_2d(cell, offsets)
@@ -154,13 +164,12 @@ class OrthogonalVonNeumannGrid(Grid[T]):
154
164
  ( 1, 0),
155
165
  ]
156
166
  # fmt: on
157
- height, width = self.dimensions
158
167
 
159
168
  for cell in self.all_cells:
160
169
  self._connect_single_cell_2d(cell, offsets)
161
170
 
162
171
  def _connect_cells_nd(self) -> None:
163
- offsets = []
172
+ offsets: list[tuple[int, ...]] = []
164
173
  dimensions = len(self.dimensions)
165
174
  for dim in range(dimensions):
166
175
  for delta in [
@@ -176,6 +185,8 @@ class OrthogonalVonNeumannGrid(Grid[T]):
176
185
 
177
186
 
178
187
  class HexGrid(Grid[T]):
188
+ """A Grid with hexagonal tilling of the space."""
189
+
179
190
  def _connect_cells_2d(self) -> None:
180
191
  # fmt: off
181
192
  even_offsets = [
@@ -1,3 +1,5 @@
1
+ """A Network grid."""
2
+
1
3
  from random import Random
2
4
  from typing import Any
3
5
 
@@ -5,8 +7,8 @@ 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,
@@ -15,13 +17,13 @@ class Network(DiscreteSpace):
15
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)
@@ -35,6 +37,6 @@ class Network(DiscreteSpace):
35
37
  for cell in self.all_cells:
36
38
  self._connect_single_cell(cell)
37
39
 
38
- def _connect_single_cell(self, cell):
40
+ def _connect_single_cell(self, cell: Cell):
39
41
  for node_id in self.G.neighbors(cell.coordinate):
40
- cell.connect(self._cells[node_id])
42
+ cell.connect(self._cells[node_id], node_id)
@@ -1,3 +1,5 @@
1
+ """Support for Voronoi meshed grids."""
2
+
1
3
  from collections.abc import Sequence
2
4
  from itertools import combinations
3
5
  from random import Random
@@ -9,16 +11,17 @@ from mesa.experimental.cell_space.discrete_space import DiscreteSpace
9
11
 
10
12
 
11
13
  class Delaunay:
12
- """
13
- Class to compute a Delaunay triangulation in 2D
14
+ """Class to compute a Delaunay triangulation in 2D.
15
+
14
16
  ref: http://github.com/jmespadero/pyDelaunay2D
15
17
  """
16
18
 
17
19
  def __init__(self, center: tuple = (0, 0), radius: int = 9999) -> None:
18
- """
19
- Init and create a new frame to contain the triangulation
20
- center: Optional position for the center of the frame. Default (0,0)
21
- radius: Optional distance from corners to the center.
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.
22
25
  """
23
26
  center = np.asarray(center)
24
27
  # Create coordinates for the corners of the frame
@@ -44,9 +47,7 @@ class Delaunay:
44
47
  self.circles[t] = self._circumcenter(t)
45
48
 
46
49
  def _circumcenter(self, triangle: list) -> tuple:
47
- """
48
- Compute circumcenter and circumradius of a triangle in 2D.
49
- """
50
+ """Compute circumcenter and circumradius of a triangle in 2D."""
50
51
  points = np.asarray([self.coords[v] for v in triangle])
51
52
  points2 = np.dot(points, points.T)
52
53
  a = np.bmat([[2 * points2, [[1], [1], [1]]], [[[1, 1, 1, 0]]]])
@@ -60,16 +61,12 @@ class Delaunay:
60
61
  return (center, radius)
61
62
 
62
63
  def _in_circle(self, triangle: list, point: list) -> bool:
63
- """
64
- Check if point p is inside of precomputed circumcircle of triangle.
65
- """
64
+ """Check if point p is inside of precomputed circumcircle of triangle."""
66
65
  center, radius = self.circles[triangle]
67
66
  return np.sum(np.square(center - point)) <= radius
68
67
 
69
68
  def add_point(self, point: Sequence) -> None:
70
- """
71
- Add a point to the current DT, and refine it using Bowyer-Watson.
72
- """
69
+ """Add a point to the current DT, and refine it using Bowyer-Watson."""
73
70
  point_index = len(self.coords)
74
71
  self.coords.append(np.asarray(point))
75
72
 
@@ -121,9 +118,7 @@ class Delaunay:
121
118
  self.triangles[triangle][2] = new_triangles[(i - 1) % n] # previous
122
119
 
123
120
  def export_triangles(self) -> list:
124
- """
125
- Export the current list of Delaunay triangles
126
- """
121
+ """Export the current list of Delaunay triangles."""
127
122
  triangles_list = [
128
123
  (a - 4, b - 4, c - 4)
129
124
  for (a, b, c) in self.triangles
@@ -132,9 +127,7 @@ class Delaunay:
132
127
  return triangles_list
133
128
 
134
129
  def export_voronoi_regions(self):
135
- """
136
- Export coordinates and regions of Voronoi diagram as indexed data.
137
- """
130
+ """Export coordinates and regions of Voronoi diagram as indexed data."""
138
131
  use_vertex = {i: [] for i in range(len(self.coords))}
139
132
  vor_coors = []
140
133
  index = {}
@@ -163,11 +156,13 @@ class Delaunay:
163
156
  return vor_coors, regions
164
157
 
165
158
 
166
- def round_float(x: float) -> int:
159
+ def round_float(x: float) -> int: # noqa
167
160
  return int(x * 500)
168
161
 
169
162
 
170
163
  class VoronoiGrid(DiscreteSpace):
164
+ """Voronoi meshed GridSpace."""
165
+
171
166
  triangulation: Delaunay
172
167
  voronoi_coordinates: list
173
168
  regions: list
@@ -181,8 +176,7 @@ class VoronoiGrid(DiscreteSpace):
181
176
  capacity_function: callable = round_float,
182
177
  cell_coloring_property: str | None = None,
183
178
  ) -> None:
184
- """
185
- A Voronoi Tessellation Grid.
179
+ """A Voronoi Tessellation Grid.
186
180
 
187
181
  Given a set of points, this class creates a grid where a cell is centered in each point,
188
182
  its neighbors are given by Voronoi Tessellation cells neighbors
@@ -192,7 +186,7 @@ class VoronoiGrid(DiscreteSpace):
192
186
  centroids_coordinates: coordinates of centroids to build the tessellation space
193
187
  capacity (int) : capacity of the cells in the discrete space
194
188
  random (Random): random number generator
195
- CellKlass (type[Cell]): type of cell class
189
+ cell_klass (type[Cell]): type of cell class
196
190
  capacity_function (Callable): function to compute (int) capacity according to (float) area
197
191
  cell_coloring_property (str): voronoi visualization polygon fill property
198
192
  """
@@ -215,17 +209,15 @@ class VoronoiGrid(DiscreteSpace):
215
209
  self._build_cell_polygons()
216
210
 
217
211
  def _connect_cells(self) -> None:
218
- """
219
- Connect cells to neighbors based on given centroids and using Delaunay Triangulation
220
- """
212
+ """Connect cells to neighbors based on given centroids and using Delaunay Triangulation."""
221
213
  self.triangulation = Delaunay()
222
214
  for centroid in self.centroids_coordinates:
223
215
  self.triangulation.add_point(centroid)
224
216
 
225
217
  for point in self.triangulation.export_triangles():
226
218
  for i, j in combinations(point, 2):
227
- self._cells[i].connect(self._cells[j])
228
- self._cells[j].connect(self._cells[i])
219
+ self._cells[i].connect(self._cells[j], (i, j))
220
+ self._cells[j].connect(self._cells[i], (j, i))
229
221
 
230
222
  def _validate_parameters(self) -> None:
231
223
  if self.capacity is not None and not isinstance(self.capacity, float | int):
@@ -241,9 +233,10 @@ class VoronoiGrid(DiscreteSpace):
241
233
 
242
234
  def _get_voronoi_regions(self) -> tuple:
243
235
  if self.voronoi_coordinates is None or self.regions is None:
244
- self.voronoi_coordinates, self.regions = (
245
- self.triangulation.export_voronoi_regions()
246
- )
236
+ (
237
+ self.voronoi_coordinates,
238
+ self.regions,
239
+ ) = self.triangulation.export_voronoi_regions()
247
240
  return self.voronoi_coordinates, self.regions
248
241
 
249
242
  @staticmethod