Mesa 3.1.0.dev0__py3-none-any.whl → 3.1.1__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 (57) hide show
  1. mesa/__init__.py +3 -3
  2. mesa/agent.py +48 -0
  3. mesa/batchrunner.py +14 -1
  4. mesa/datacollection.py +1 -6
  5. mesa/examples/__init__.py +2 -2
  6. mesa/examples/advanced/epstein_civil_violence/app.py +5 -0
  7. mesa/examples/advanced/pd_grid/agents.py +2 -1
  8. mesa/examples/advanced/pd_grid/app.py +5 -0
  9. mesa/examples/advanced/pd_grid/model.py +3 -5
  10. mesa/examples/advanced/sugarscape_g1mt/agents.py +12 -65
  11. mesa/examples/advanced/sugarscape_g1mt/app.py +24 -19
  12. mesa/examples/advanced/sugarscape_g1mt/model.py +45 -52
  13. mesa/examples/advanced/wolf_sheep/agents.py +3 -1
  14. mesa/examples/advanced/wolf_sheep/model.py +17 -16
  15. mesa/examples/basic/boid_flockers/app.py +5 -0
  16. mesa/examples/basic/boltzmann_wealth_model/app.py +8 -5
  17. mesa/examples/basic/boltzmann_wealth_model/st_app.py +1 -1
  18. mesa/examples/basic/conways_game_of_life/app.py +5 -0
  19. mesa/examples/basic/conways_game_of_life/st_app.py +2 -2
  20. mesa/examples/basic/schelling/agents.py +11 -5
  21. mesa/examples/basic/schelling/app.py +6 -1
  22. mesa/examples/basic/virus_on_network/app.py +5 -0
  23. mesa/experimental/__init__.py +17 -10
  24. mesa/experimental/cell_space/__init__.py +19 -7
  25. mesa/experimental/cell_space/cell.py +22 -37
  26. mesa/experimental/cell_space/cell_agent.py +12 -1
  27. mesa/experimental/cell_space/cell_collection.py +18 -3
  28. mesa/experimental/cell_space/discrete_space.py +15 -64
  29. mesa/experimental/cell_space/grid.py +74 -4
  30. mesa/experimental/cell_space/network.py +13 -1
  31. mesa/experimental/cell_space/property_layer.py +444 -0
  32. mesa/experimental/cell_space/voronoi.py +13 -1
  33. mesa/experimental/devs/__init__.py +20 -2
  34. mesa/experimental/devs/eventlist.py +19 -1
  35. mesa/experimental/devs/simulator.py +24 -8
  36. mesa/experimental/mesa_signals/__init__.py +23 -0
  37. mesa/experimental/mesa_signals/mesa_signal.py +485 -0
  38. mesa/experimental/mesa_signals/observable_collections.py +133 -0
  39. mesa/experimental/mesa_signals/signals_util.py +52 -0
  40. mesa/mesa_logging.py +190 -0
  41. mesa/model.py +17 -23
  42. mesa/visualization/__init__.py +2 -2
  43. mesa/visualization/mpl_space_drawing.py +8 -5
  44. mesa/visualization/solara_viz.py +49 -11
  45. {mesa-3.1.0.dev0.dist-info → mesa-3.1.1.dist-info}/METADATA +1 -1
  46. mesa-3.1.1.dist-info/RECORD +94 -0
  47. {mesa-3.1.0.dev0.dist-info → mesa-3.1.1.dist-info}/WHEEL +1 -1
  48. mesa/experimental/UserParam.py +0 -67
  49. mesa/experimental/components/altair.py +0 -81
  50. mesa/experimental/components/matplotlib.py +0 -242
  51. mesa/experimental/devs/examples/epstein_civil_violence.py +0 -305
  52. mesa/experimental/devs/examples/wolf_sheep.py +0 -250
  53. mesa/experimental/solara_viz.py +0 -453
  54. mesa-3.1.0.dev0.dist-info/RECORD +0 -94
  55. {mesa-3.1.0.dev0.dist-info → mesa-3.1.1.dist-info}/entry_points.txt +0 -0
  56. {mesa-3.1.0.dev0.dist-info → mesa-3.1.1.dist-info}/licenses/LICENSE +0 -0
  57. {mesa-3.1.0.dev0.dist-info → mesa-3.1.1.dist-info}/licenses/NOTICE +0 -0
@@ -20,6 +20,11 @@ def post_process(ax):
20
20
 
21
21
 
22
22
  model_params = {
23
+ "seed": {
24
+ "type": "InputText",
25
+ "value": 42,
26
+ "label": "Random Seed",
27
+ },
23
28
  "width": {
24
29
  "type": "SliderInt",
25
30
  "value": 50,
@@ -49,9 +49,9 @@ if run:
49
49
  for i in range(num_ticks):
50
50
  model.step()
51
51
  my_bar.progress((i / num_ticks), text="Simulation progress")
52
- placeholder.text("Step = %d" % i)
52
+ placeholder.text(f"Step = {i}")
53
53
  for contents, (x, y) in model.grid.coord_iter():
54
- # print('x:',x,'y:',y, 'state:',contents)
54
+ # print(f"x: {x}, y: {y}, state: {contents}")
55
55
  selected_row = df_grid[(df_grid["x"] == x) & (df_grid["y"] == y)]
56
56
  df_grid.loc[selected_row.index, "state"] = (
57
57
  contents.state
@@ -6,7 +6,6 @@ class SchellingAgent(Agent):
6
6
 
7
7
  def __init__(self, model, agent_type: int) -> None:
8
8
  """Create a new Schelling agent.
9
-
10
9
  Args:
11
10
  model: The model instance the agent belongs to
12
11
  agent_type: Indicator for the agent's type (minority=1, majority=0)
@@ -16,15 +15,22 @@ class SchellingAgent(Agent):
16
15
 
17
16
  def step(self) -> None:
18
17
  """Determine if agent is happy and move if necessary."""
19
- neighbors = self.model.grid.iter_neighbors(
18
+ neighbors = self.model.grid.get_neighbors(
20
19
  self.pos, moore=True, radius=self.model.radius
21
20
  )
22
21
 
23
22
  # Count similar neighbors
24
- similar = sum(neighbor.type == self.type for neighbor in neighbors)
23
+ similar_neighbors = len([n for n in neighbors if n.type == self.type])
24
+
25
+ # Calculate the fraction of similar neighbors
26
+ if (valid_neighbors := len(neighbors)) > 0:
27
+ similarity_fraction = similar_neighbors / valid_neighbors
28
+ else:
29
+ # If there are no neighbors, the similarity fraction is 0
30
+ similarity_fraction = 0.0
25
31
 
26
- # If unhappy, move to a random empty cell:
27
- if similar < self.model.homophily:
32
+ # Move if unhappy
33
+ if similarity_fraction < self.model.homophily:
28
34
  self.model.grid.move_to_empty(self)
29
35
  else:
30
36
  self.model.happy += 1
@@ -19,9 +19,14 @@ def agent_portrayal(agent):
19
19
 
20
20
 
21
21
  model_params = {
22
+ "seed": {
23
+ "type": "InputText",
24
+ "value": 42,
25
+ "label": "Random Seed",
26
+ },
22
27
  "density": Slider("Agent density", 0.8, 0.1, 1.0, 0.1),
23
28
  "minority_pc": Slider("Fraction minority", 0.2, 0.0, 1.0, 0.05),
24
- "homophily": Slider("Homophily", 3, 0, 8, 1),
29
+ "homophily": Slider("Homophily", 0.3, 0.0, 0.8, 0.1),
25
30
  "width": 20,
26
31
  "height": 20,
27
32
  }
@@ -35,6 +35,11 @@ def get_resistant_susceptible_ratio(model):
35
35
 
36
36
 
37
37
  model_params = {
38
+ "seed": {
39
+ "type": "InputText",
40
+ "value": 42,
41
+ "label": "Random Seed",
42
+ },
38
43
  "num_nodes": Slider(
39
44
  label="Number of agents",
40
45
  value=10,
@@ -1,13 +1,20 @@
1
- """Experimental init."""
1
+ """Experimental features package for Mesa.
2
2
 
3
- from mesa.experimental import cell_space
3
+ This package contains modules that are under active development and testing. These
4
+ features are provided to allow early access and feedback from the Mesa community, but
5
+ their APIs may change between releases without following semantic versioning.
4
6
 
5
- try:
6
- from .solara_viz import JupyterViz, Slider, SolaraViz, make_text
7
+ Current experimental modules:
8
+ cell_space: Alternative API for discrete spaces with cell-centric functionality
9
+ devs: Discrete event simulation system for scheduling events at arbitrary times
10
+ mesa_signals: Reactive programming capabilities for tracking state changes
7
11
 
8
- __all__ = ["cell_space", "JupyterViz", "Slider", "SolaraViz", "make_text"]
9
- except ImportError:
10
- print(
11
- "Could not import SolaraViz. If you need it, install with 'pip install --pre mesa[viz]'"
12
- )
13
- __all__ = ["cell_space"]
12
+ Notes:
13
+ - Features in this package may be changed or removed without notice
14
+ - APIs are not guaranteed to be stable between releases
15
+ - Features graduate from experimental status once their APIs are stabilized
16
+ """
17
+
18
+ from mesa.experimental import cell_space, devs, mesa_signals
19
+
20
+ __all__ = ["cell_space", "devs", "mesa_signals"]
@@ -1,8 +1,18 @@
1
- """Cell spaces.
1
+ """Cell spaces for active, property-rich spatial modeling in Mesa.
2
2
 
3
- Cell spaces offer an alternative API for discrete spaces. It is experimental and under development. The API is more
4
- expressive that the default grids available in `mesa.space`.
3
+ Cell spaces extend Mesa's spatial modeling capabilities by making the space itself active -
4
+ each position (cell) can have properties and behaviors rather than just containing agents.
5
+ This enables more sophisticated environmental modeling and agent-environment interactions.
5
6
 
7
+ Key components:
8
+ - Cells: Active positions that can have properties and contain agents
9
+ - CellAgents: Agents that understand how to interact with cells
10
+ - Spaces: Different cell organization patterns (grids, networks, etc.)
11
+ - PropertyLayers: Efficient property storage and manipulation
12
+
13
+ This is particularly useful for models where the environment plays an active role,
14
+ like resource growth, pollution diffusion, or infrastructure networks. The cell
15
+ space system is experimental and under active development.
6
16
  """
7
17
 
8
18
  from mesa.experimental.cell_space.cell import Cell
@@ -20,19 +30,21 @@ from mesa.experimental.cell_space.grid import (
20
30
  OrthogonalVonNeumannGrid,
21
31
  )
22
32
  from mesa.experimental.cell_space.network import Network
33
+ from mesa.experimental.cell_space.property_layer import PropertyLayer
23
34
  from mesa.experimental.cell_space.voronoi import VoronoiGrid
24
35
 
25
36
  __all__ = [
26
- "CellCollection",
27
37
  "Cell",
28
38
  "CellAgent",
29
- "Grid2DMovingAgent",
30
- "FixedAgent",
39
+ "CellCollection",
31
40
  "DiscreteSpace",
41
+ "FixedAgent",
32
42
  "Grid",
43
+ "Grid2DMovingAgent",
33
44
  "HexGrid",
45
+ "Network",
34
46
  "OrthogonalMooreGrid",
35
47
  "OrthogonalVonNeumannGrid",
36
- "Network",
48
+ "PropertyLayer",
37
49
  "VoronoiGrid",
38
50
  ]
@@ -1,15 +1,25 @@
1
- """The Cell in a cell space."""
1
+ """Cells are positions in space that can have properties and contain agents.
2
+
3
+ A cell represents a location that can:
4
+ - Have properties (like temperature or resources)
5
+ - Track and limit the agents it contains
6
+ - Connect to neighboring cells
7
+ - Provide neighborhood information
8
+
9
+ Cells form the foundation of the cell space system, enabling rich spatial
10
+ environments where both location properties and agent behaviors matter. They're
11
+ useful for modeling things like varying terrain, infrastructure capacity, or
12
+ environmental conditions.
13
+ """
2
14
 
3
15
  from __future__ import annotations
4
16
 
5
- from collections.abc import Callable
6
17
  from functools import cache, cached_property
7
18
  from random import Random
8
- from typing import TYPE_CHECKING, Any
19
+ from typing import TYPE_CHECKING
9
20
 
10
21
  from mesa.experimental.cell_space.cell_agent import CellAgent
11
22
  from mesa.experimental.cell_space.cell_collection import CellCollection
12
- from mesa.space import PropertyLayer
13
23
 
14
24
  if TYPE_CHECKING:
15
25
  from mesa.agent import Agent
@@ -24,31 +34,20 @@ class Cell:
24
34
  coordinate (Tuple[int, int]) : the position of the cell in the discrete space
25
35
  agents (List[Agent]): the agents occupying the cell
26
36
  capacity (int): the maximum number of agents that can simultaneously occupy the cell
27
- properties (dict[str, Any]): the properties of the cell
28
37
  random (Random): the random number generator
29
38
 
30
39
  """
31
40
 
32
41
  __slots__ = [
33
- "coordinate",
34
- "connections",
42
+ "__dict__",
35
43
  "agents",
36
44
  "capacity",
45
+ "connections",
46
+ "coordinate",
37
47
  "properties",
38
48
  "random",
39
- "_mesa_property_layers",
40
- "__dict__",
41
49
  ]
42
50
 
43
- # def __new__(cls,
44
- # coordinate: tuple[int, ...],
45
- # capacity: float | None = None,
46
- # random: Random | None = None,):
47
- # if capacity != 1:
48
- # return object.__new__(cls)
49
- # else:
50
- # return object.__new__(SingleAgentCell)
51
-
52
51
  def __init__(
53
52
  self,
54
53
  coordinate: Coordinate,
@@ -70,9 +69,10 @@ class Cell:
70
69
  Agent
71
70
  ] = [] # TODO:: change to AgentSet or weakrefs? (neither is very performant, )
72
71
  self.capacity: int | None = capacity
73
- self.properties: dict[Coordinate, object] = {}
72
+ self.properties: dict[
73
+ Coordinate, object
74
+ ] = {} # fixme still used by voronoi mesh
74
75
  self.random = random
75
- self._mesa_property_layers: dict[str, PropertyLayer] = {}
76
76
 
77
77
  def connect(self, other: Cell, key: Coordinate | None = None) -> None:
78
78
  """Connects this cell to another cell.
@@ -105,6 +105,7 @@ class Cell:
105
105
 
106
106
  """
107
107
  n = len(self.agents)
108
+ self.empty = False
108
109
 
109
110
  if self.capacity and n >= self.capacity:
110
111
  raise Exception(
@@ -121,6 +122,7 @@ class Cell:
121
122
 
122
123
  """
123
124
  self.agents.remove(agent)
125
+ self.empty = self.is_empty
124
126
 
125
127
  @property
126
128
  def is_empty(self) -> bool:
@@ -195,23 +197,6 @@ class Cell:
195
197
  neighborhood.pop(self, None)
196
198
  return neighborhood
197
199
 
198
- # PropertyLayer methods
199
- def get_property(self, property_name: str) -> Any:
200
- """Get the value of a property."""
201
- return self._mesa_property_layers[property_name].data[self.coordinate]
202
-
203
- def set_property(self, property_name: str, value: Any):
204
- """Set the value of a property."""
205
- self._mesa_property_layers[property_name].set_cell(self.coordinate, value)
206
-
207
- def modify_property(
208
- self, property_name: str, operation: Callable, value: Any = None
209
- ):
210
- """Modify the value of a property."""
211
- self._mesa_property_layers[property_name].modify_cell(
212
- self.coordinate, operation, value
213
- )
214
-
215
200
  def __getstate__(self):
216
201
  """Return state of the Cell with connections set to empty."""
217
202
  # fixme, once we shift to 3.11, replace this with super. __getstate__
@@ -1,4 +1,15 @@
1
- """An agent with movement methods for cell spaces."""
1
+ """Agents that understand how to exist in and move through cell spaces.
2
+
3
+ Provides specialized agent classes that handle cell occupation, movement, and
4
+ proper registration:
5
+ - CellAgent: Mobile agents that can move between cells
6
+ - FixedAgent: Immobile agents permanently fixed to cells
7
+ - Grid2DMovingAgent: Agents with grid-specific movement capabilities
8
+
9
+ These classes ensure consistent agent-cell relationships and proper state management
10
+ as agents move through the space. They can be used directly or as examples for
11
+ creating custom cell-aware agents.
12
+ """
2
13
 
3
14
  from __future__ import annotations
4
15
 
@@ -1,4 +1,17 @@
1
- """CellCollection class."""
1
+ """Collection class for managing and querying groups of cells.
2
+
3
+ The CellCollection class provides a consistent interface for operating on multiple
4
+ cells, supporting:
5
+ - Filtering and selecting cells based on conditions
6
+ - Random cell and agent selection
7
+ - Access to contained agents
8
+ - Group operations
9
+
10
+ This is useful for implementing area effects, zones, or any operation that needs
11
+ to work with multiple cells as a unit. The collection handles efficient iteration
12
+ and agent access across cells. The class is used throughout the cell space
13
+ implementation to represent neighborhoods, selections, and other cell groupings.
14
+ """
2
15
 
3
16
  from __future__ import annotations
4
17
 
@@ -48,8 +61,10 @@ class CellCollection(Generic[T]):
48
61
  else:
49
62
  self._cells = {cell: cell.agents for cell in cells}
50
63
 
51
- #
52
- self._capacity: int = next(iter(self._cells.keys())).capacity
64
+ # Get capacity from first cell if collection is not empty
65
+ self._capacity: int | None = (
66
+ next(iter(self._cells.keys())).capacity if self._cells else None
67
+ )
53
68
 
54
69
  if random is None:
55
70
  warnings.warn(
@@ -1,17 +1,28 @@
1
- """DiscreteSpace base class."""
1
+ """Base class for building cell-based spatial environments.
2
+
3
+ DiscreteSpace provides the core functionality needed by all cell-based spaces:
4
+ - Cell creation and tracking
5
+ - Agent-cell relationship management
6
+ - Property layer support
7
+ - Random selection capabilities
8
+ - Capacity management
9
+
10
+ This serves as the foundation for specific space implementations like grids
11
+ and networks, ensuring consistent behavior and shared functionality across
12
+ different space types. All concrete cell space implementations (grids, networks, etc.)
13
+ inherit from this class.
14
+ """
2
15
 
3
16
  from __future__ import annotations
4
17
 
5
18
  import warnings
6
- from collections.abc import Callable
7
19
  from functools import cached_property
8
20
  from random import Random
9
- from typing import Any, Generic, TypeVar
21
+ from typing import Generic, TypeVar
10
22
 
11
23
  from mesa.agent import AgentSet
12
24
  from mesa.experimental.cell_space.cell import Cell
13
25
  from mesa.experimental.cell_space.cell_collection import CellCollection
14
- from mesa.space import PropertyLayer
15
26
 
16
27
  T = TypeVar("T", bound=Cell)
17
28
 
@@ -61,8 +72,6 @@ class DiscreteSpace(Generic[T]):
61
72
  self.cell_klass = cell_klass
62
73
 
63
74
  self._empties: dict[tuple[int, ...], None] = {}
64
- self._empties_initialized = False
65
- self.property_layers: dict[str, PropertyLayer] = {}
66
75
 
67
76
  @property
68
77
  def cutoff_empties(self): # noqa
@@ -98,64 +107,6 @@ class DiscreteSpace(Generic[T]):
98
107
  """Select random empty cell."""
99
108
  return self.random.choice(list(self.empties))
100
109
 
101
- # PropertyLayer methods
102
- def add_property_layer(
103
- self, property_layer: PropertyLayer, add_to_cells: bool = True
104
- ):
105
- """Add a property layer to the grid.
106
-
107
- Args:
108
- property_layer: the property layer to add
109
- add_to_cells: whether to add the property layer to all cells (default: True)
110
- """
111
- if property_layer.name in self.property_layers:
112
- raise ValueError(f"Property layer {property_layer.name} already exists.")
113
- self.property_layers[property_layer.name] = property_layer
114
- if add_to_cells:
115
- for cell in self._cells.values():
116
- cell._mesa_property_layers[property_layer.name] = property_layer
117
-
118
- def remove_property_layer(self, property_name: str, remove_from_cells: bool = True):
119
- """Remove a property layer from the grid.
120
-
121
- Args:
122
- property_name: the name of the property layer to remove
123
- remove_from_cells: whether to remove the property layer from all cells (default: True)
124
- """
125
- del self.property_layers[property_name]
126
- if remove_from_cells:
127
- for cell in self._cells.values():
128
- del cell._mesa_property_layers[property_name]
129
-
130
- def set_property(
131
- self, property_name: str, value, condition: Callable[[T], bool] | None = None
132
- ):
133
- """Set the value of a property for all cells in the grid.
134
-
135
- Args:
136
- property_name: the name of the property to set
137
- value: the value to set
138
- condition: a function that takes a cell and returns a boolean
139
- """
140
- self.property_layers[property_name].set_cells(value, condition)
141
-
142
- def modify_properties(
143
- self,
144
- property_name: str,
145
- operation: Callable,
146
- value: Any = None,
147
- condition: Callable[[T], bool] | None = None,
148
- ):
149
- """Modify the values of a specific property for all cells in the grid.
150
-
151
- Args:
152
- property_name: the name of the property to modify
153
- operation: the operation to perform
154
- value: the value to use in the operation
155
- condition: a function that takes a cell and returns a boolean (used to filter cells)
156
- """
157
- self.property_layers[property_name].modify_cells(operation, value, condition)
158
-
159
110
  def __setstate__(self, state):
160
111
  """Set the state of the discrete space and rebuild the connections."""
161
112
  self.__dict__ = state
@@ -1,18 +1,62 @@
1
- """Various Grid Spaces."""
1
+ """Grid-based cell space implementations with different connection patterns.
2
+
3
+ Provides several grid types for organizing cells:
4
+ - OrthogonalMooreGrid: 8 neighbors in 2D, (3^n)-1 in nD
5
+ - OrthogonalVonNeumannGrid: 4 neighbors in 2D, 2n in nD
6
+ - HexGrid: 6 neighbors in hexagonal pattern (2D only)
7
+
8
+ Each grid type supports optional wrapping (torus) and cell capacity limits.
9
+ Choose based on how movement and connectivity should work in your model -
10
+ Moore for unrestricted movement, Von Neumann for orthogonal-only movement,
11
+ or Hex for more uniform distances.
12
+ """
2
13
 
3
14
  from __future__ import annotations
4
15
 
16
+ import copyreg
5
17
  from collections.abc import Sequence
6
18
  from itertools import product
7
19
  from random import Random
8
- from typing import Generic, TypeVar
20
+ from typing import Any, Generic, TypeVar
9
21
 
10
22
  from mesa.experimental.cell_space import Cell, DiscreteSpace
23
+ from mesa.experimental.cell_space.property_layer import (
24
+ HasPropertyLayers,
25
+ PropertyDescriptor,
26
+ )
11
27
 
12
28
  T = TypeVar("T", bound=Cell)
13
29
 
14
30
 
15
- class Grid(DiscreteSpace[T], Generic[T]):
31
+ def pickle_gridcell(obj):
32
+ """Helper function for pickling GridCell instances."""
33
+ # we have the base class and the state via __getstate__
34
+ args = obj.__class__.__bases__[0], obj.__getstate__()
35
+ return unpickle_gridcell, args
36
+
37
+
38
+ def unpickle_gridcell(parent, fields):
39
+ """Helper function for unpickling GridCell instances."""
40
+ # since the class is dynamically created, we recreate it here
41
+ cell_klass = type(
42
+ "GridCell",
43
+ (parent,),
44
+ {"_mesa_properties": set()},
45
+ )
46
+ instance = cell_klass(
47
+ (0, 0)
48
+ ) # we use a default coordinate and overwrite it with the correct value next
49
+
50
+ # __gestate__ returns a tuple with dict and slots, but slots contains the dict so we can just use the
51
+ # second item only
52
+ for k, v in fields[1].items():
53
+ if k != "__dict__":
54
+ setattr(instance, k, v)
55
+
56
+ return instance
57
+
58
+
59
+ class Grid(DiscreteSpace[T], Generic[T], HasPropertyLayers):
16
60
  """Base class for all grid classes.
17
61
 
18
62
  Attributes:
@@ -60,14 +104,23 @@ class Grid(DiscreteSpace[T], Generic[T]):
60
104
  self._try_random = True
61
105
  self._ndims = len(dimensions)
62
106
  self._validate_parameters()
107
+ self.cell_klass = type(
108
+ "GridCell",
109
+ (self.cell_klass,),
110
+ {"_mesa_properties": set()},
111
+ )
112
+
113
+ # we register the pickle_gridcell helper function
114
+ copyreg.pickle(self.cell_klass, pickle_gridcell)
63
115
 
64
116
  coordinates = product(*(range(dim) for dim in self.dimensions))
65
117
 
66
118
  self._cells = {
67
- coord: cell_klass(coord, capacity, random=self.random)
119
+ coord: self.cell_klass(coord, capacity, random=self.random)
68
120
  for coord in coordinates
69
121
  }
70
122
  self._connect_cells()
123
+ self.create_property_layer("empty", default_value=True, dtype=bool)
71
124
 
72
125
  def _connect_cells(self) -> None:
73
126
  if self._ndims == 2:
@@ -126,6 +179,23 @@ class Grid(DiscreteSpace[T], Generic[T]):
126
179
  if 0 <= ni < height and 0 <= nj < width:
127
180
  cell.connect(self._cells[ni, nj], (di, dj))
128
181
 
182
+ def __getstate__(self) -> dict[str, Any]:
183
+ """Custom __getstate__ for handling dynamic GridCell class and PropertyDescriptors."""
184
+ state = super().__getstate__()
185
+ state = {k: v for k, v in state.items() if k != "cell_klass"}
186
+ return state
187
+
188
+ def __setstate__(self, state: dict[str, Any]) -> None:
189
+ """Custom __setstate__ for handling dynamic GridCell class and PropertyDescriptors."""
190
+ self.__dict__ = state
191
+ self._connect_cells() # using super fails for this for some reason, so we repeat ourselves
192
+
193
+ self.cell_klass = type(
194
+ self._cells[(0, 0)]
195
+ ) # the __reduce__ function handles this for us nicely
196
+ for layer in self._mesa_property_layers.values():
197
+ setattr(self.cell_klass, layer.name, PropertyDescriptor(layer))
198
+
129
199
 
130
200
  class OrthogonalMooreGrid(Grid[T]):
131
201
  """Grid where cells are connected to their 8 neighbors.
@@ -1,4 +1,16 @@
1
- """A Network grid."""
1
+ """Network-based cell space using arbitrary connection patterns.
2
+
3
+ Creates spaces where cells connect based on network relationships rather than
4
+ spatial proximity. Built on NetworkX graphs, this enables:
5
+ - Arbitrary connectivity patterns between cells
6
+ - Graph-based neighborhood definitions
7
+ - Logical rather than physical distances
8
+ - Dynamic connectivity changes
9
+ - Integration with NetworkX's graph algorithms
10
+
11
+ Useful for modeling systems like social networks, transportation systems,
12
+ or any environment where connectivity matters more than physical location.
13
+ """
2
14
 
3
15
  from random import Random
4
16
  from typing import Any