Mesa 3.1.2__py3-none-any.whl → 3.1.4__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.
mesa/__init__.py CHANGED
@@ -22,7 +22,7 @@ __all__ = [
22
22
  ]
23
23
 
24
24
  __title__ = "mesa"
25
- __version__ = "3.1.2"
25
+ __version__ = "3.1.4"
26
26
  __license__ = "Apache 2.0"
27
27
  _this_year = datetime.datetime.now(tz=datetime.UTC).date().year
28
28
  __copyright__ = f"Copyright {_this_year} Project Mesa Team"
mesa/datacollection.py CHANGED
@@ -117,6 +117,9 @@ class DataCollector:
117
117
  self._agenttype_records = {}
118
118
  self.tables = {}
119
119
 
120
+ # add the signal of the validation of model reporter
121
+ self._validated = False
122
+
120
123
  if model_reporters is not None:
121
124
  for name, reporter in model_reporters.items():
122
125
  self._new_model_reporter(name, reporter)
@@ -134,13 +137,66 @@ class DataCollector:
134
137
  for name, columns in tables.items():
135
138
  self._new_table(name, columns)
136
139
 
140
+ def _validate_model_reporter(self, name, reporter, model):
141
+ """Validate model reporter and handle validation results appropriately.
142
+
143
+ Args:
144
+ name: Name of the reporter
145
+ reporter: Reporter definition (lambda/method/attribute/function list)
146
+ model: Model instance
147
+
148
+ Raises:
149
+ ValueError: If reporter is None or has invalid format
150
+ AttributeError: If model attribute doesn't exist
151
+ TypeError: If reporter type is not supported
152
+ RuntimeError: If reporter execution fails
153
+ """
154
+ self._validated = True # put the change of signal firstly avoid losing efficacy
155
+
156
+ # Type 1: Lambda function
157
+ if isinstance(reporter, types.LambdaType):
158
+ try:
159
+ reporter(model)
160
+ except Exception as e:
161
+ raise RuntimeError(
162
+ f"Lambda reporter '{name}' failed validation: {e!s}\n"
163
+ f"Example: lambda m: len(m.agents)"
164
+ ) from e
165
+
166
+ # Type 2: Method of class/instance
167
+ if not callable(reporter) and not isinstance(reporter, types.LambdaType):
168
+ pass
169
+
170
+ # Type 3: Model attribute (string)
171
+ if isinstance(reporter, str):
172
+ try:
173
+ if not hasattr(model, reporter):
174
+ raise AttributeError(
175
+ f"Model reporter '{name}' references non-existent attribute '{reporter}'\n"
176
+ )
177
+ getattr(model, reporter) # 验证属性是否可访问
178
+ except AttributeError as e:
179
+ raise AttributeError(
180
+ f"Model reporter '{name}' attribute validation failed: {e!s}\n"
181
+ ) from e
182
+
183
+ # Type 4: Function with parameters in list
184
+ if isinstance(reporter, list) and (not reporter or not callable(reporter[0])):
185
+ raise ValueError(
186
+ f"Invalid function list format for reporter '{name}'\n"
187
+ f"Expected: [function, [param1, param2]], got: {reporter}"
188
+ )
189
+
137
190
  def _new_model_reporter(self, name, reporter):
138
191
  """Add a new model-level reporter to collect.
139
192
 
140
193
  Args:
141
194
  name: Name of the model-level variable to collect.
142
- reporter: Attribute string, or function object that returns the
143
- variable when given a model instance.
195
+ reporter: Can be one of four types:
196
+ 1. Attribute name (str): "attribute_name"
197
+ 2. Lambda function: lambda m: len(m.agents)
198
+ 3. Method: model.get_count or Model.get_count
199
+ 4. List of [function, [parameters]]
144
200
  """
145
201
  self.model_reporters[name] = reporter
146
202
  self.model_vars[name] = []
@@ -262,6 +318,10 @@ class DataCollector:
262
318
  def collect(self, model):
263
319
  """Collect all the data for the given model object."""
264
320
  if self.model_reporters:
321
+ if not self._validated:
322
+ for name, reporter in self.model_reporters.items():
323
+ self._validate_model_reporter(name, reporter, model)
324
+
265
325
  for var, reporter in self.model_reporters.items():
266
326
  # Check if lambda or partial function
267
327
  if isinstance(reporter, types.LambdaType | partial):
@@ -1,46 +1,24 @@
1
- import numpy as np
2
- import solara
3
- from matplotlib.figure import Figure
4
-
5
1
  from mesa.examples.advanced.sugarscape_g1mt.model import SugarscapeG1mt
6
2
  from mesa.visualization import Slider, SolaraViz, make_plot_component
3
+ from mesa.visualization.components.matplotlib_components import make_mpl_space_component
4
+
7
5
 
6
+ def agent_portrayal(agent):
7
+ return {"marker": "o", "color": "red", "size": 10}
8
8
 
9
- def SpaceDrawer(model):
10
- def portray(g):
11
- layers = {
12
- "trader": {"x": [], "y": [], "c": "tab:red", "marker": "o", "s": 10},
13
- }
14
9
 
15
- for agent in g.all_cells.agents:
16
- i, j = agent.cell.coordinate
17
- layers["trader"]["x"].append(i)
18
- layers["trader"]["y"].append(j)
19
- return layers
10
+ propertylayer_portrayal = {
11
+ "sugar": {"color": "blue", "alpha": 0.8, "colorbar": True, "vmin": 0, "vmax": 10},
12
+ "spice": {"color": "red", "alpha": 0.8, "colorbar": True, "vmin": 0, "vmax": 10},
13
+ }
20
14
 
21
- fig = Figure()
22
- ax = fig.subplots()
23
- out = portray(model.grid)
24
- # Sugar
25
- # Important note: imshow by default draws from upper left. You have to
26
- # always explicitly specify origin="lower".
27
- im = ax.imshow(
28
- np.ma.masked_where(model.grid.sugar.data <= 1, model.grid.sugar.data),
29
- cmap="spring",
30
- origin="lower",
31
- )
32
- fig.colorbar(im, orientation="vertical")
33
- # Spice
34
- ax.imshow(
35
- np.ma.masked_where(model.grid.spice.data <= 1, model.grid.spice.data),
36
- cmap="winter",
37
- origin="lower",
38
- )
39
- # Trader
40
- ax.scatter(**out["trader"])
41
- ax.set_axis_off()
42
- return solara.FigureMatplotlib(fig)
43
15
 
16
+ sugarscape_space = make_mpl_space_component(
17
+ agent_portrayal=agent_portrayal,
18
+ propertylayer_portrayal=propertylayer_portrayal,
19
+ post_process=None,
20
+ draw_grid=False,
21
+ )
44
22
 
45
23
  model_params = {
46
24
  "seed": {
@@ -72,7 +50,7 @@ model = SugarscapeG1mt()
72
50
  page = SolaraViz(
73
51
  model,
74
52
  components=[
75
- SpaceDrawer,
53
+ sugarscape_space,
76
54
  make_plot_component("#Traders"),
77
55
  make_plot_component("Price"),
78
56
  ],
@@ -6,10 +6,10 @@ of flocking behavior.
6
6
 
7
7
  import numpy as np
8
8
 
9
- from mesa import Agent
9
+ from mesa.experimental.continuous_space import ContinuousSpaceAgent
10
10
 
11
11
 
12
- class Boid(Agent):
12
+ class Boid(ContinuousSpaceAgent):
13
13
  """A Boid-style flocker agent.
14
14
 
15
15
  The agent follows three behaviors to flock:
@@ -26,10 +26,12 @@ class Boid(Agent):
26
26
  def __init__(
27
27
  self,
28
28
  model,
29
- speed,
30
- direction,
31
- vision,
32
- separation,
29
+ space,
30
+ position=(0, 0),
31
+ speed=1,
32
+ direction=(1, 1),
33
+ vision=1,
34
+ separation=1,
33
35
  cohere=0.03,
34
36
  separate=0.015,
35
37
  match=0.05,
@@ -46,7 +48,8 @@ class Boid(Agent):
46
48
  separate: Relative importance of avoiding close neighbors (default: 0.015)
47
49
  match: Relative importance of matching neighbors' directions (default: 0.05)
48
50
  """
49
- super().__init__(model)
51
+ super().__init__(space, model)
52
+ self.position = position
50
53
  self.speed = speed
51
54
  self.direction = direction
52
55
  self.vision = vision
@@ -58,46 +61,31 @@ class Boid(Agent):
58
61
 
59
62
  def step(self):
60
63
  """Get the Boid's neighbors, compute the new vector, and move accordingly."""
61
- self.neighbors = self.model.space.get_neighbors(self.pos, self.vision, False)
64
+ neighbors, distances = self.get_neighbors_in_radius(radius=self.vision)
65
+ self.neighbors = [n for n in neighbors if n is not self]
62
66
 
63
67
  # If no neighbors, maintain current direction
64
- if not self.neighbors:
65
- new_pos = self.pos + self.direction * self.speed
66
- self.model.space.move_agent(self, new_pos)
68
+ if not neighbors:
69
+ self.position += self.direction * self.speed
67
70
  return
68
71
 
69
- # Initialize vectors for the three flocking behaviors
70
- cohere = np.zeros(2) # Cohesion vector
71
- match_vector = np.zeros(2) # Alignment vector
72
- separation_vector = np.zeros(2) # Separation vector
72
+ delta = self.space.calculate_difference_vector(self.position, agents=neighbors)
73
73
 
74
- # Calculate the contribution of each neighbor to the three behaviors
75
- for neighbor in self.neighbors:
76
- heading = self.model.space.get_heading(self.pos, neighbor.pos)
77
- distance = self.model.space.get_distance(self.pos, neighbor.pos)
78
-
79
- # Cohesion - steer towards the average position of neighbors
80
- cohere += heading
81
-
82
- # Separation - avoid getting too close
83
- if distance < self.separation:
84
- separation_vector -= heading
85
-
86
- # Alignment - match neighbors' flying direction
87
- match_vector += neighbor.direction
88
-
89
- # Weight each behavior by its factor and normalize by number of neighbors
90
- n = len(self.neighbors)
91
- cohere = cohere * self.cohere_factor
92
- separation_vector = separation_vector * self.separate_factor
93
- match_vector = match_vector * self.match_factor
74
+ cohere_vector = delta.sum(axis=0) * self.cohere_factor
75
+ separation_vector = (
76
+ -1 * delta[distances < self.separation].sum(axis=0) * self.separate_factor
77
+ )
78
+ match_vector = (
79
+ np.asarray([n.direction for n in neighbors]).sum(axis=0) * self.match_factor
80
+ )
94
81
 
95
82
  # Update direction based on the three behaviors
96
- self.direction += (cohere + separation_vector + match_vector) / n
83
+ self.direction += (cohere_vector + separation_vector + match_vector) / len(
84
+ neighbors
85
+ )
97
86
 
98
87
  # Normalize direction vector
99
88
  self.direction /= np.linalg.norm(self.direction)
100
89
 
101
90
  # Move boid
102
- new_pos = self.pos + self.direction * self.speed
103
- self.model.space.move_agent(self, new_pos)
91
+ self.position += self.direction * self.speed
@@ -1,3 +1,8 @@
1
+ import os
2
+ import sys
3
+
4
+ sys.path.insert(0, os.path.abspath("../../../.."))
5
+
1
6
  from mesa.examples.basic.boid_flockers.model import BoidFlockers
2
7
  from mesa.visualization import Slider, SolaraViz, make_space_component
3
8
 
@@ -17,7 +22,7 @@ model_params = {
17
22
  "value": 42,
18
23
  "label": "Random Seed",
19
24
  },
20
- "population": Slider(
25
+ "population_size": Slider(
21
26
  label="Number of boids",
22
27
  value=100,
23
28
  min=10,
@@ -5,11 +5,17 @@ A Mesa implementation of Craig Reynolds's Boids flocker model.
5
5
  Uses numpy arrays to represent vectors.
6
6
  """
7
7
 
8
+ import os
9
+ import sys
10
+
11
+ sys.path.insert(0, os.path.abspath("../../../.."))
12
+
13
+
8
14
  import numpy as np
9
15
 
10
16
  from mesa import Model
11
17
  from mesa.examples.basic.boid_flockers.agents import Boid
12
- from mesa.space import ContinuousSpace
18
+ from mesa.experimental.continuous_space import ContinuousSpace
13
19
 
14
20
 
15
21
  class BoidFlockers(Model):
@@ -17,7 +23,7 @@ class BoidFlockers(Model):
17
23
 
18
24
  def __init__(
19
25
  self,
20
- population=100,
26
+ population_size=100,
21
27
  width=100,
22
28
  height=100,
23
29
  speed=1,
@@ -31,7 +37,7 @@ class BoidFlockers(Model):
31
37
  """Create a new Boids Flocking model.
32
38
 
33
39
  Args:
34
- population: Number of Boids in the simulation (default: 100)
40
+ population_size: Number of Boids in the simulation (default: 100)
35
41
  width: Width of the space (default: 100)
36
42
  height: Height of the space (default: 100)
37
43
  speed: How fast the Boids move (default: 1)
@@ -44,48 +50,35 @@ class BoidFlockers(Model):
44
50
  """
45
51
  super().__init__(seed=seed)
46
52
 
47
- # Model Parameters
48
- self.population = population
49
- self.vision = vision
50
- self.speed = speed
51
- self.separation = separation
52
-
53
53
  # Set up the space
54
- self.space = ContinuousSpace(width, height, torus=True)
55
-
56
- # Store flocking weights
57
- self.factors = {"cohere": cohere, "separate": separate, "match": match}
54
+ self.space = ContinuousSpace(
55
+ [[0, width], [0, height]],
56
+ torus=True,
57
+ random=self.random,
58
+ n_agents=population_size,
59
+ )
58
60
 
59
61
  # Create and place the Boid agents
60
- self.make_agents()
62
+ positions = self.rng.random(size=(population_size, 2)) * self.space.size
63
+ directions = self.rng.uniform(-1, 1, size=(population_size, 2))
64
+ Boid.create_agents(
65
+ self,
66
+ population_size,
67
+ self.space,
68
+ position=positions,
69
+ direction=directions,
70
+ cohere=cohere,
71
+ separate=separate,
72
+ match=match,
73
+ speed=speed,
74
+ vision=vision,
75
+ separation=separation,
76
+ )
61
77
 
62
78
  # For tracking statistics
63
79
  self.average_heading = None
64
80
  self.update_average_heading()
65
81
 
66
- def make_agents(self):
67
- """Create and place all Boid agents randomly in the space."""
68
- for _ in range(self.population):
69
- # Random position
70
- x = self.random.random() * self.space.x_max
71
- y = self.random.random() * self.space.y_max
72
- pos = np.array((x, y))
73
-
74
- # Random initial direction
75
- direction = np.random.random(2) * 2 - 1 # Random vector between -1 and 1
76
- direction /= np.linalg.norm(direction) # Normalize
77
-
78
- # Create and place the Boid
79
- boid = Boid(
80
- model=self,
81
- speed=self.speed,
82
- direction=direction,
83
- vision=self.vision,
84
- separation=self.separation,
85
- **self.factors,
86
- )
87
- self.space.place_agent(boid, pos)
88
-
89
82
  def update_average_heading(self):
90
83
  """Calculate the average heading (direction) of all Boids."""
91
84
  if not self.agents:
@@ -15,6 +15,6 @@ Notes:
15
15
  - Features graduate from experimental status once their APIs are stabilized
16
16
  """
17
17
 
18
- from mesa.experimental import cell_space, devs, mesa_signals
18
+ from mesa.experimental import cell_space, continuous_space, devs, mesa_signals
19
19
 
20
- __all__ = ["cell_space", "devs", "mesa_signals"]
20
+ __all__ = ["cell_space", "continuous_space", "devs", "mesa_signals"]
@@ -273,20 +273,20 @@ class HexGrid(Grid[T]):
273
273
  def _connect_cells_2d(self) -> None:
274
274
  # fmt: off
275
275
  even_offsets = [
276
- (-1, -1), (-1, 0),
277
- ( 0, -1), ( 0, 1),
278
- ( 1, -1), ( 1, 0),
276
+ (-1, -1), (0, -1),
277
+ ( -1, 0), ( 1, 0),
278
+ ( -1, 1), (0, 1),
279
279
  ]
280
280
  odd_offsets = [
281
- (-1, 0), (-1, 1),
282
- ( 0, -1), ( 0, 1),
283
- ( 1, 0), ( 1, 1),
281
+ (0, -1), (1, -1),
282
+ ( -1, 0), ( 1, 0),
283
+ ( 0, 1), ( 1, 1),
284
284
  ]
285
285
  # fmt: on
286
286
 
287
287
  for cell in self.all_cells:
288
- i = cell.coordinate[0]
289
- offsets = even_offsets if i % 2 == 0 else odd_offsets
288
+ i = cell.coordinate[1]
289
+ offsets = even_offsets if i % 2 else odd_offsets
290
290
  self._connect_single_cell_2d(cell, offsets=offsets)
291
291
 
292
292
  def _connect_cells_nd(self) -> None:
@@ -186,7 +186,6 @@ class VoronoiGrid(DiscreteSpace):
186
186
  random: Random | None = None,
187
187
  cell_klass: type[Cell] = Cell,
188
188
  capacity_function: callable = round_float,
189
- cell_coloring_property: str | None = None,
190
189
  ) -> None:
191
190
  """A Voronoi Tessellation Grid.
192
191
 
@@ -200,7 +199,7 @@ class VoronoiGrid(DiscreteSpace):
200
199
  random (Random): random number generator
201
200
  cell_klass (type[Cell]): type of cell class
202
201
  capacity_function (Callable): function to compute (int) capacity according to (float) area
203
- cell_coloring_property (str): voronoi visualization polygon fill property
202
+
204
203
  """
205
204
  super().__init__(capacity=capacity, random=random, cell_klass=cell_klass)
206
205
  self.centroids_coordinates = centroids_coordinates
@@ -215,7 +214,6 @@ class VoronoiGrid(DiscreteSpace):
215
214
  self.triangulation = None
216
215
  self.voronoi_coordinates = None
217
216
  self.capacity_function = capacity_function
218
- self.cell_coloring_property = cell_coloring_property
219
217
 
220
218
  self._connect_cells()
221
219
  self._build_cell_polygons()
@@ -266,4 +264,3 @@ class VoronoiGrid(DiscreteSpace):
266
264
  polygon_area = self._compute_polygon_area(polygon)
267
265
  self._cells[region].properties["area"] = polygon_area
268
266
  self._cells[region].capacity = self.capacity_function(polygon_area)
269
- self._cells[region].properties[self.cell_coloring_property] = 0
@@ -0,0 +1,8 @@
1
+ """Continuous space support."""
2
+
3
+ from mesa.experimental.continuous_space.continuous_space import ContinuousSpace
4
+ from mesa.experimental.continuous_space.continuous_space_agents import (
5
+ ContinuousSpaceAgent,
6
+ )
7
+
8
+ __all__ = ["ContinuousSpace", "ContinuousSpaceAgent"]