Mesa 3.1.2__py3-none-any.whl → 3.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of Mesa might be problematic. Click here for more details.

mesa/__init__.py CHANGED
@@ -22,7 +22,7 @@ __all__ = [
22
22
  ]
23
23
 
24
24
  __title__ = "mesa"
25
- __version__ = "3.1.2"
25
+ __version__ = "3.1.3"
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"
@@ -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"]
@@ -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"]
@@ -0,0 +1,273 @@
1
+ """A Continuous Space class."""
2
+
3
+ import warnings
4
+ from collections.abc import Iterable
5
+ from itertools import compress
6
+ from random import Random
7
+
8
+ import numpy as np
9
+ from numpy.typing import ArrayLike
10
+ from scipy.spatial.distance import cdist
11
+
12
+ from mesa.agent import Agent, AgentSet
13
+
14
+
15
+ class ContinuousSpace:
16
+ """Continuous space where each agent can have an arbitrary position."""
17
+
18
+ @property
19
+ def x_min(self): # noqa: D102
20
+ # compatibility with solara_viz
21
+ return self.dimensions[0, 0]
22
+
23
+ @property
24
+ def x_max(self): # noqa: D102
25
+ # compatibility with solara_viz
26
+ return self.dimensions[0, 1]
27
+
28
+ @property
29
+ def y_min(self): # noqa: D102
30
+ # compatibility with solara_viz
31
+ return self.dimensions[1, 0]
32
+
33
+ @property
34
+ def y_max(self): # noqa: D102
35
+ # compatibility with solara_viz
36
+ return self.dimensions[1, 1]
37
+
38
+ @property
39
+ def width(self): # noqa: D102
40
+ # compatibility with solara_viz
41
+ return self.size[0]
42
+
43
+ @property
44
+ def height(self): # noqa: D102
45
+ # compatibility with solara_viz
46
+ return self.size[1]
47
+
48
+ def __init__(
49
+ self,
50
+ dimensions: ArrayLike,
51
+ torus: bool = False,
52
+ random: Random | None = None,
53
+ n_agents: int = 100,
54
+ ) -> None:
55
+ """Create a new continuous space.
56
+
57
+ Args:
58
+ dimensions: a numpy array like object where each row specifies the minimum and maximum value of that dimension.
59
+ torus: boolean for whether the space wraps around or not
60
+ random: a seeded stdlib random.Random instance
61
+ n_agents: the expected number of agents in the space
62
+
63
+ Internally, a numpy array is used to store the positions of all agents. This is resized if needed,
64
+ but you can control the initial size explicitly by passing n_agents.
65
+
66
+
67
+ """
68
+ if random is None:
69
+ warnings.warn(
70
+ "Random number generator not specified, this can make models non-reproducible. Please pass a random number generator explicitly",
71
+ UserWarning,
72
+ stacklevel=2,
73
+ )
74
+ random = Random()
75
+ self.random = random
76
+
77
+ self.dimensions: np.array = np.asanyarray(dimensions)
78
+ self.ndims: int = self.dimensions.shape[0]
79
+ self.size: np.array = self.dimensions[:, 1] - self.dimensions[:, 0]
80
+ self.center: np.array = np.sum(self.dimensions, axis=1) / 2
81
+
82
+ self.torus: bool = torus
83
+
84
+ # self._agent_positions is the array containing all agent positions
85
+ # plus potential extra empty rows
86
+ # agent_positions is a view into _agent_positions containing only the filled rows
87
+ self._agent_positions: np.array = np.empty(
88
+ (n_agents, self.dimensions.shape[0]), dtype=float
89
+ )
90
+ self.agent_positions: (
91
+ np.array
92
+ ) # a view on _agent_positions containing all active positions
93
+
94
+ # the list of agents in the space
95
+ self.active_agents = []
96
+ self._n_agents = 0 # the number of active agents in the space
97
+
98
+ # a mapping from agents to index and vice versa
99
+ self._index_to_agent: dict[int, Agent] = {}
100
+ self._agent_to_index: dict[Agent, int | None] = {}
101
+
102
+ @property
103
+ def agents(self) -> AgentSet:
104
+ """Return an AgentSet with the agents in the space."""
105
+ return AgentSet(self.active_agents, random=self.random)
106
+
107
+ def _add_agent(self, agent: Agent) -> int:
108
+ """Helper method for adding an agent to the space.
109
+
110
+ This method manages the numpy array with the agent positions and ensuring it is
111
+ enlarged if and when needed. It is called automatically by ContinousSpaceAgent when created.
112
+
113
+ """
114
+ index = self._n_agents
115
+ self._n_agents += 1
116
+
117
+ if self._agent_positions.shape[0] <= index:
118
+ # we are out of space
119
+ fraction = 0.2 # we add 20% Fixme
120
+ n = int(round(fraction * self._n_agents))
121
+ self._agent_positions = np.vstack(
122
+ [
123
+ self._agent_positions,
124
+ np.empty(
125
+ (n, self.dimensions.shape[0]),
126
+ ),
127
+ ]
128
+ )
129
+
130
+ self._agent_to_index[agent] = index
131
+ self._index_to_agent[index] = agent
132
+
133
+ # we want to maintain a view rather than a copy on the active agents and positions
134
+ # this is essential for the performance of the rest of this code
135
+ self.active_agents.append(agent)
136
+ self.agent_positions = self._agent_positions[0 : self._n_agents]
137
+
138
+ return index
139
+
140
+ def _remove_agent(self, agent: Agent) -> None:
141
+ """Remove an agent from the space.
142
+
143
+ This method is automatically called by ContinuousSpaceAgent.remove.
144
+
145
+ """
146
+ index = self._agent_to_index[agent]
147
+ self._agent_to_index.pop(agent, None)
148
+ self._index_to_agent.pop(index, None)
149
+ del self.active_agents[index]
150
+
151
+ # we update all indices
152
+ for agent in self.active_agents[index::]:
153
+ old_index = self._agent_to_index[agent]
154
+ self._agent_to_index[agent] = old_index - 1
155
+ self._index_to_agent[old_index - 1] = agent
156
+
157
+ # we move all data below the removed agent one row up
158
+ self._agent_positions[index : self._n_agents - 1] = self._agent_positions[
159
+ index + 1 : self._n_agents
160
+ ]
161
+ self._n_agents -= 1
162
+ self.agent_positions = self._agent_positions[0 : self._n_agents]
163
+
164
+ def calculate_difference_vector(self, point: np.ndarray, agents=None) -> np.ndarray:
165
+ """Calculate the difference vector between the point and all agenents.
166
+
167
+ Args:
168
+ point: the point to calculate the difference vector for
169
+ agents: the agents to calculate the difference vector of point with. By default,
170
+ all agents are considered.
171
+
172
+
173
+ """
174
+ point = np.asanyarray(point)
175
+ positions = (
176
+ self.agent_positions
177
+ if agents is None
178
+ else self._agent_positions[[self._agent_to_index[a] for a in agents]]
179
+ )
180
+
181
+ delta = positions - point
182
+
183
+ if self.torus:
184
+ inverse_delta = delta - np.sign(delta) * self.size
185
+
186
+ # we need to use the lowest absolute value from delta and inverse delta
187
+ logical = np.abs(delta) < np.abs(inverse_delta)
188
+
189
+ out = np.zeros(delta.shape)
190
+ out[logical] = delta[logical]
191
+ out[~logical] = inverse_delta[~logical]
192
+
193
+ delta = out
194
+
195
+ return delta
196
+
197
+ def calculate_distances(
198
+ self, point: ArrayLike, agents: Iterable[Agent] | None = None, **kwargs
199
+ ) -> tuple[np.ndarray, list]:
200
+ """Calculate the distance between the point and all agents.
201
+
202
+ Args:
203
+ point: the point to calculate the difference vector for
204
+ agents: the agents to calculate the difference vector of point with. By default,
205
+ all agents are considered.
206
+ kwargs: any additional keyword arguments are passed to scipy's cdist, which is used
207
+ only if torus is False. This allows for non-Euclidian distance measures.
208
+
209
+ """
210
+ point = np.asanyarray(point)
211
+
212
+ if agents is None:
213
+ positions = self.agent_positions
214
+ agents = self.active_agents
215
+ else:
216
+ positions = self._agent_positions[[self._agent_to_index[a] for a in agents]]
217
+ agents = np.asarray(agents)
218
+
219
+ if self.torus:
220
+ delta = np.abs(point - positions)
221
+ delta = np.minimum(delta, self.size - delta, out=delta)
222
+
223
+ # + is much faster than np.sum or array.sum
224
+ dists = delta[:, 0] ** 2
225
+ for i in range(1, self.ndims):
226
+ dists += delta[:, i] ** 2
227
+ dists = np.sqrt(dists)
228
+ else:
229
+ dists = cdist(point[np.newaxis, :], positions, **kwargs)[0, :]
230
+ return dists, agents
231
+
232
+ def get_agents_in_radius(
233
+ self, point: ArrayLike, radius: float | int = 1
234
+ ) -> tuple[list, np.ndarray]:
235
+ """Return the agents and their distances within a radius for the point."""
236
+ distances, agents = self.calculate_distances(point)
237
+ logical = distances <= radius
238
+ agents = list(compress(agents, logical))
239
+ return (
240
+ agents,
241
+ distances[logical],
242
+ )
243
+
244
+ def get_k_nearest_agents(
245
+ self, point: ArrayLike, k: int = 1
246
+ ) -> tuple[list, np.ndarray]:
247
+ """Return the k nearest agents and their distances to the point.
248
+
249
+ Notes:
250
+ This method returns exactly k agents, ignoring ties. In case of ties, the
251
+ earlier an agent is inserted the higher it will rank.
252
+
253
+ """
254
+ dists, agents = self.calculate_distances(point)
255
+
256
+ indices = np.argpartition(dists, k)[:k]
257
+ agents = [agents[i] for i in indices]
258
+ return agents, dists[indices]
259
+
260
+ def in_bounds(self, point: ArrayLike) -> bool:
261
+ """Check if point is inside the bounds of the space."""
262
+ return bool(
263
+ (
264
+ (np.asanyarray(point) >= self.dimensions[:, 0])
265
+ & (point <= self.dimensions[:, 1])
266
+ ).all()
267
+ )
268
+
269
+ def torus_correct(self, point: ArrayLike) -> np.ndarray:
270
+ """Apply a torus correction to the point."""
271
+ return self.dimensions[:, 0] + np.mod(
272
+ np.asanyarray(point) - self.dimensions[:, 0], self.size
273
+ )
@@ -0,0 +1,101 @@
1
+ """Continuous space agents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from itertools import compress
6
+ from typing import Protocol
7
+
8
+ import numpy as np
9
+
10
+ from mesa.agent import Agent
11
+ from mesa.experimental.continuous_space import ContinuousSpace
12
+
13
+
14
+ class HasPositionProtocol(Protocol):
15
+ """Protocol for continuous space position holders."""
16
+
17
+ position: np.ndarray
18
+
19
+
20
+ class ContinuousSpaceAgent(Agent):
21
+ """A continuous space agent.
22
+
23
+ Attributes:
24
+ space (ContinuousSpace): the continuous space in which the agent is located
25
+ position (np.ndarray): the position of the agent
26
+
27
+ """
28
+
29
+ __slots__ = ["_mesa_index", "space"]
30
+
31
+ @property
32
+ def position(self) -> np.ndarray:
33
+ """Position of the agent."""
34
+ return self.space.agent_positions[self.space._agent_to_index[self]]
35
+
36
+ @position.setter
37
+ def position(self, value: np.ndarray) -> None:
38
+ if not self.space.in_bounds(value):
39
+ if self.space.torus:
40
+ value = self.space.torus_correct(value)
41
+ else:
42
+ raise ValueError(f"point {value} is outside the bounds of the space")
43
+
44
+ self.space.agent_positions[self.space._agent_to_index[self]] = value
45
+
46
+ @property
47
+ def pos(self): # noqa: D102
48
+ # just here for compatibility with solara_viz.
49
+ return self.position
50
+
51
+ @pos.setter
52
+ def pos(self, value):
53
+ # just here for compatibility solara_viz.
54
+ pass
55
+
56
+ def __init__(self, space: ContinuousSpace, model):
57
+ """Initialize a continuous space agent.
58
+
59
+ Args:
60
+ space: the continuous space in which the agent is located
61
+ model: the model to which the agent belongs
62
+
63
+ """
64
+ super().__init__(model)
65
+ self.space: ContinuousSpace = space
66
+ self.space._add_agent(self)
67
+ # self.position[:] = np.nan
68
+
69
+ def remove(self) -> None:
70
+ """Remove and delete the agent from the model and continuous space."""
71
+ super().remove()
72
+ self.space._remove_agent(self)
73
+ self._mesa_index = None
74
+ self.space = None
75
+
76
+ def get_neighbors_in_radius(
77
+ self, radius: float | int = 1
78
+ ) -> tuple[list, np.ndarray]:
79
+ """Get neighbors within radius.
80
+
81
+ Args:
82
+ radius: radius within which to look for neighbors
83
+
84
+ """
85
+ agents, dists = self.space.get_agents_in_radius(self.position, radius=radius)
86
+ logical = np.asarray([agent is not self for agent in agents])
87
+ agents = list(compress(agents, logical))
88
+ return agents, dists[logical]
89
+
90
+ def get_nearest_neighbors(self, k: int = 1) -> tuple[list, np.ndarray]:
91
+ """Get neighbors within radius.
92
+
93
+ Args:
94
+ k: the number of nearest neighbors to return
95
+
96
+ """
97
+ # return includes self, so we need to get k+1
98
+ agents, dists = self.space.get_k_nearest_agents(self.position, k=k + 1)
99
+ logical = np.asarray([agent is not self for agent in agents])
100
+ agents = list(compress(agents, logical))
101
+ return agents, dists[logical]
mesa/model.py CHANGED
@@ -94,7 +94,7 @@ class Model:
94
94
  self._seed = seed # this allows for reproducing stdlib.random
95
95
 
96
96
  try:
97
- self.rng: np.random.Generator = np.random.default_rng(rng)
97
+ self.rng: np.random.Generator = np.random.default_rng(seed)
98
98
  except TypeError:
99
99
  rng = self.random.randint(0, sys.maxsize)
100
100
  self.rng: np.random.Generator = np.random.default_rng(rng)
mesa/space.py CHANGED
@@ -1415,6 +1415,13 @@ class ContinuousSpace:
1415
1415
  coordinates. i.e. if you are searching for the
1416
1416
  neighbors of a given agent, True will include that
1417
1417
  agent in the results.
1418
+
1419
+ Notes:
1420
+ If 1 or more agents are located on pos, include_center=False will remove all these agents
1421
+ from the results. So, if you really want to get the neighbors of a given agent,
1422
+ you should set include_center=True, and then filter the list of agents to remove
1423
+ the given agent (i.e., self when calling it from an agent).
1424
+
1418
1425
  """
1419
1426
  if self._agent_points is None:
1420
1427
  self._build_agent_cache()
@@ -20,7 +20,7 @@ from matplotlib.axes import Axes
20
20
  from matplotlib.cm import ScalarMappable
21
21
  from matplotlib.collections import PatchCollection
22
22
  from matplotlib.colors import LinearSegmentedColormap, Normalize, to_rgba
23
- from matplotlib.patches import RegularPolygon
23
+ from matplotlib.patches import Polygon, RegularPolygon
24
24
 
25
25
  import mesa
26
26
  from mesa.experimental.cell_space import (
@@ -143,7 +143,10 @@ def draw_space(
143
143
  draw_orthogonal_grid(space, agent_portrayal, ax=ax, **space_drawing_kwargs)
144
144
  case mesa.space.NetworkGrid() | mesa.experimental.cell_space.Network():
145
145
  draw_network(space, agent_portrayal, ax=ax, **space_drawing_kwargs)
146
- case mesa.space.ContinuousSpace():
146
+ case (
147
+ mesa.space.ContinuousSpace()
148
+ | mesa.experimental.continuous_space.ContinuousSpace()
149
+ ):
147
150
  draw_continuous_space(space, agent_portrayal, ax=ax)
148
151
  case VoronoiGrid():
149
152
  draw_voronoi_grid(space, agent_portrayal, ax=ax)
@@ -498,7 +501,11 @@ def draw_continuous_space(
498
501
 
499
502
 
500
503
  def draw_voronoi_grid(
501
- space: VoronoiGrid, agent_portrayal: Callable, ax: Axes | None = None, **kwargs
504
+ space: VoronoiGrid,
505
+ agent_portrayal: Callable,
506
+ ax: Axes | None = None,
507
+ draw_grid: bool = True,
508
+ **kwargs,
502
509
  ):
503
510
  """Visualize a voronoi grid.
504
511
 
@@ -506,6 +513,7 @@ def draw_voronoi_grid(
506
513
  space: the space to visualize
507
514
  agent_portrayal: a callable that is called with the agent and returns a dict
508
515
  ax: a Matplotlib Axes instance. If none is provided a new figure and ax will be created using plt.subplots
516
+ draw_grid: whether to draw the grid or not
509
517
  kwargs: additional keyword arguments passed to ax.scatter
510
518
 
511
519
  Returns:
@@ -538,16 +546,18 @@ def draw_voronoi_grid(
538
546
 
539
547
  _scatter(ax, arguments, **kwargs)
540
548
 
541
- for cell in space.all_cells:
542
- polygon = cell.properties["polygon"]
543
- ax.fill(
544
- *zip(*polygon),
545
- alpha=min(1, cell.properties[space.cell_coloring_property]),
546
- c="red",
547
- zorder=0,
548
- ) # Plot filled polygon
549
- ax.plot(*zip(*polygon), color="black") # Plot polygon edges in black
549
+ def setup_voroinoimesh(cells):
550
+ patches = []
551
+ for cell in cells:
552
+ patch = Polygon(cell.properties["polygon"])
553
+ patches.append(patch)
554
+ mesh = PatchCollection(
555
+ patches, edgecolor="k", facecolor=(1, 1, 1, 0), linestyle="dotted", lw=1
556
+ )
557
+ return mesh
550
558
 
559
+ if draw_grid:
560
+ ax.add_collection(setup_voroinoimesh(space.all_cells.cells))
551
561
  return ax
552
562
 
553
563
 
@@ -52,6 +52,7 @@ def SolaraViz(
52
52
  | Literal["default"] = "default",
53
53
  *,
54
54
  play_interval: int = 100,
55
+ render_interval: int = 1,
55
56
  simulator: Simulator | None = None,
56
57
  model_params=None,
57
58
  name: str | None = None,
@@ -72,6 +73,8 @@ def SolaraViz(
72
73
  Defaults to "default", which uses the default Altair space visualization.
73
74
  play_interval (int, optional): Interval for playing the model steps in milliseconds.
74
75
  This controls the speed of the model's automatic stepping. Defaults to 100 ms.
76
+ render_interval (int, optional): Controls how often plots are updated during a simulation,
77
+ allowing users to skip intermediate steps and update graphs less frequently.
75
78
  simulator: A simulator that controls the model (optional)
76
79
  model_params (dict, optional): Parameters for (re-)instantiating a model.
77
80
  Can include user-adjustable parameters and fixed parameters. Defaults to None.
@@ -90,6 +93,8 @@ def SolaraViz(
90
93
  model instance is provided, it will be converted to a reactive model using `solara.use_reactive`.
91
94
  - The `play_interval` argument controls the speed of the model's automatic stepping. A lower
92
95
  value results in faster stepping, while a higher value results in slower stepping.
96
+ - The `render_interval` argument determines how often plots are updated during simulation. Higher values
97
+ reduce update frequency,resulting in faster execution.
93
98
  """
94
99
  if components == "default":
95
100
  components = [components_altair.make_altair_space()]
@@ -103,7 +108,7 @@ def SolaraViz(
103
108
  # set up reactive model_parameters shared by ModelCreator and ModelController
104
109
  reactive_model_parameters = solara.use_reactive({})
105
110
  reactive_play_interval = solara.use_reactive(play_interval)
106
-
111
+ reactive_render_interval = solara.use_reactive(render_interval)
107
112
  with solara.AppBar():
108
113
  solara.AppBarTitle(name if name else model.value.__class__.__name__)
109
114
 
@@ -117,11 +122,20 @@ def SolaraViz(
117
122
  max=500,
118
123
  step=10,
119
124
  )
125
+ solara.SliderInt(
126
+ label="Render Interval (steps)",
127
+ value=reactive_render_interval,
128
+ on_value=lambda v: reactive_render_interval.set(v),
129
+ min=1,
130
+ max=100,
131
+ step=2,
132
+ )
120
133
  if not isinstance(simulator, Simulator):
121
134
  ModelController(
122
135
  model,
123
136
  model_parameters=reactive_model_parameters,
124
137
  play_interval=reactive_play_interval,
138
+ render_interval=reactive_render_interval,
125
139
  )
126
140
  else:
127
141
  SimulatorController(
@@ -129,6 +143,7 @@ def SolaraViz(
129
143
  simulator,
130
144
  model_parameters=reactive_model_parameters,
131
145
  play_interval=reactive_play_interval,
146
+ render_interval=reactive_render_interval,
132
147
  )
133
148
  with solara.Card("Model Parameters"):
134
149
  ModelCreator(
@@ -189,6 +204,7 @@ def ModelController(
189
204
  *,
190
205
  model_parameters: dict | solara.Reactive[dict] = None,
191
206
  play_interval: int | solara.Reactive[int] = 100,
207
+ render_interval: int | solara.Reactive[int] = 1,
192
208
  ):
193
209
  """Create controls for model execution (step, play, pause, reset).
194
210
 
@@ -196,7 +212,7 @@ def ModelController(
196
212
  model: Reactive model instance
197
213
  model_parameters: Reactive parameters for (re-)instantiating a model.
198
214
  play_interval: Interval for playing the model steps in milliseconds.
199
-
215
+ render_interval: Controls how often the plots are updated during simulation steps.Higher value reduce update frequency.
200
216
  """
201
217
  playing = solara.use_reactive(False)
202
218
  running = solara.use_reactive(True)
@@ -215,9 +231,12 @@ def ModelController(
215
231
 
216
232
  @function_logger(__name__)
217
233
  def do_step():
218
- """Advance the model by one step."""
219
- model.value.step()
234
+ """Advance the model by the number of steps specified by the render_interval slider."""
235
+ for _ in range(render_interval.value):
236
+ model.value.step()
237
+
220
238
  running.value = model.value.running
239
+
221
240
  force_update()
222
241
 
223
242
  @function_logger(__name__)
@@ -259,6 +278,7 @@ def SimulatorController(
259
278
  *,
260
279
  model_parameters: dict | solara.Reactive[dict] = None,
261
280
  play_interval: int | solara.Reactive[int] = 100,
281
+ render_interval: int | solara.Reactive[int] = 1,
262
282
  ):
263
283
  """Create controls for model execution (step, play, pause, reset).
264
284
 
@@ -267,7 +287,11 @@ def SimulatorController(
267
287
  simulator: Simulator instance
268
288
  model_parameters: Reactive parameters for (re-)instantiating a model.
269
289
  play_interval: Interval for playing the model steps in milliseconds.
290
+ render_interval: Controls how often the plots are updated during simulation steps.Higher values reduce update frequency.
270
291
 
292
+ Notes:
293
+ The `step button` increments the step by the value specified in the `render_interval` slider.
294
+ This behavior ensures synchronization between simulation steps and plot updates.
271
295
  """
272
296
  playing = solara.use_reactive(False)
273
297
  running = solara.use_reactive(True)
@@ -285,8 +309,8 @@ def SimulatorController(
285
309
  )
286
310
 
287
311
  def do_step():
288
- """Advance the model by one step."""
289
- simulator.run_for(1)
312
+ """Advance the model by the number of steps specified by the render_interval slider."""
313
+ simulator.run_for(render_interval.value)
290
314
  running.value = model.value.running
291
315
  force_update()
292
316
 
@@ -390,7 +414,6 @@ def ModelCreator(
390
414
  or are dictionaries containing parameter details such as type, value, min, and max.
391
415
  - The `seed` argument ensures reproducibility by setting the initial seed for the model's random number generator.
392
416
  - The component provides an interface for adjusting user-defined parameters and reseeding the model.
393
-
394
417
  """
395
418
  if model_parameters is None:
396
419
  model_parameters = {}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Mesa
3
- Version: 3.1.2
3
+ Version: 3.1.3
4
4
  Summary: Agent-based modeling (ABM) in Python
5
5
  Project-URL: homepage, https://github.com/projectmesa/mesa
6
6
  Project-URL: repository, https://github.com/projectmesa/mesa
@@ -24,6 +24,7 @@ Classifier: Topic :: Scientific/Engineering :: Artificial Life
24
24
  Requires-Python: >=3.11
25
25
  Requires-Dist: numpy
26
26
  Requires-Dist: pandas
27
+ Requires-Dist: scipy
27
28
  Requires-Dist: tqdm
28
29
  Provides-Extra: all
29
30
  Requires-Dist: ipython; extra == 'all'
@@ -1,10 +1,10 @@
1
- mesa/__init__.py,sha256=ZzoDPNujAbk6_r2EXHXdaqxwv8kfwY92pdL7jALFKOY,611
1
+ mesa/__init__.py,sha256=cQpR402xBpOx0vJjUbQB8d6D9d2JK_ZpWkmWSlm4cWk,611
2
2
  mesa/agent.py,sha256=4CXMOFA9KhvTypaV_OHZGqxOR4GVwyX4x8DOtQENUQA,26130
3
3
  mesa/batchrunner.py,sha256=w8StV82F_7DAAVQc5V7_Ggp0EL1NYn__UcBE-Nwrgv4,7771
4
4
  mesa/datacollection.py,sha256=8loT4pQsXcHArxHSsbRc7HTc2GP5gsEIeKFKr3xya4I,15991
5
5
  mesa/mesa_logging.py,sha256=PEDqUaQ2Y4bkYBkrHVkGT0sF86gUdbSH1T3vCg3qQeE,4949
6
- mesa/model.py,sha256=oVcV-_OZ3GVv1sb3KqyGA9TAWCDQu9qEDWwRi_AXR40,8353
7
- mesa/space.py,sha256=cfzlRfy9chegp8d89k2aqI29jo9cb18USlz2G2iOZU4,64082
6
+ mesa/model.py,sha256=VkdBea_mkWcBMxMq-pwuU23UlI1gbG4TOIyqkBfeiFo,8354
7
+ mesa/space.py,sha256=MNCblKf862pdkoIAa-VpjaurmI8GJtb02W3q3QWFjTE,64458
8
8
  mesa/examples/README.md,sha256=dNn8kv0BNQem3NNhO5mbOANQoK8UUYOo7rnkCFV9tnE,2882
9
9
  mesa/examples/__init__.py,sha256=pyPWFRUxyYtQilJECbH7LY1eYBk8VB0Yg-_SbFEEvFA,825
10
10
  mesa/examples/advanced/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -35,9 +35,9 @@ mesa/examples/advanced/wolf_sheep/model.py,sha256=IUN1STm6jCGuzXo2sCF86r1U-dI63y
35
35
  mesa/examples/basic/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
36
  mesa/examples/basic/boid_flockers/Readme.md,sha256=4KJinsLPtUciQSMzvaX3tU5r1HTUg3AFOFDKy73W5RE,894
37
37
  mesa/examples/basic/boid_flockers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
- mesa/examples/basic/boid_flockers/agents.py,sha256=72oL4jWEiJfSgshSEcL44irptIY6WAQDRAQ0cO2MxS8,3827
39
- mesa/examples/basic/boid_flockers/app.py,sha256=oJbv_CbeiXu97q1W_2xlsXIVAqqLvfxshn2Y70_7PyA,1244
40
- mesa/examples/basic/boid_flockers/model.py,sha256=scy0OaYzQVY5vVarsI7dUydynwoH3-uYoYcsBTtwtyA,3399
38
+ mesa/examples/basic/boid_flockers/agents.py,sha256=f9IpVMpuo6WPtMffLOqgKsxoG4syJt8-k9Njvh4fMN8,3232
39
+ mesa/examples/basic/boid_flockers/app.py,sha256=5y52V_PRENzNGkmV_-KB5ZQeUN589i1ntJW-AIgZD0s,1323
40
+ mesa/examples/basic/boid_flockers/model.py,sha256=hgYScacUAv6Yq240al41r9Iv3Iqbn4wfFVQEHyorJ3c,2981
41
41
  mesa/examples/basic/boltzmann_wealth_model/Readme.md,sha256=wl1ylO9KWoTiuIJKOnk2FGdcmyVUqJ5wiSbVUa3WWAc,2725
42
42
  mesa/examples/basic/boltzmann_wealth_model/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
43
43
  mesa/examples/basic/boltzmann_wealth_model/agents.py,sha256=Jol2aspw--UtQn0EiReXVmlWtzpdC2o_YUTAyu1sDk4,1546
@@ -61,7 +61,7 @@ mesa/examples/basic/virus_on_network/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeu
61
61
  mesa/examples/basic/virus_on_network/agents.py,sha256=a_WhqYblJlW6od67eXfU-nb7IMRyYpgxtf0le--VYoA,1975
62
62
  mesa/examples/basic/virus_on_network/app.py,sha256=8I8VWQ7pBcOaNGyLDEO4IbNSTRy161-eWg-iEUVQ3-I,2553
63
63
  mesa/examples/basic/virus_on_network/model.py,sha256=jQoCmvygwCvhUrlL0l7V8GcDLv94CgwtuK7DDGU8q8g,2813
64
- mesa/experimental/__init__.py,sha256=32D_blj5droexwyh83MjPska0_xYzL1ncJFgyuyBaWs,905
64
+ mesa/experimental/__init__.py,sha256=hSGXGvsVANvqhgA994HcLBLIRC2WhSF3e_w2gvoCMUo,943
65
65
  mesa/experimental/cell_space/__init__.py,sha256=0A3YTen0Lk-e3Q73VEXK7N2UEHb9f50gW11H_jx7DXc,1744
66
66
  mesa/experimental/cell_space/cell.py,sha256=Dl0ek7W_vgAZOp6zXBvkwYTG7E36OQ-4zZ5bW43mKtw,6600
67
67
  mesa/experimental/cell_space/cell_agent.py,sha256=LOTLKR2To9hU-igKsauA5sikS7k8wgwlt-Pi0C7lGU0,4262
@@ -70,7 +70,10 @@ mesa/experimental/cell_space/discrete_space.py,sha256=qNGez9SV4E83Outs_12suuS0Zt
70
70
  mesa/experimental/cell_space/grid.py,sha256=d-1S2iXijGkoJ9yc271pB8iXlzsX13usJjcjevCs_rU,10432
71
71
  mesa/experimental/cell_space/network.py,sha256=ujN2dV1i9hcXh6H0s7gwTuPT6gh7BCaziOUYPCybQKk,1862
72
72
  mesa/experimental/cell_space/property_layer.py,sha256=HFpBWOjI7PFU_K8VDb_pl9h62MftCBWL7PUKQNT3Ke8,17379
73
- mesa/experimental/cell_space/voronoi.py,sha256=5D9j45FW2IVPcZrp-hskw_z84QwHIdduwXLdTKeqWMQ,10472
73
+ mesa/experimental/cell_space/voronoi.py,sha256=FXJD8ci81Jil3FaL7ZFNfMPGvXvg3uym5Ooo1ZqKSKs,10199
74
+ mesa/experimental/continuous_space/__init__.py,sha256=JkCkL4zZpe8c0GHqw4huM2-uoGOYqrCyt7J1M454kFA,269
75
+ mesa/experimental/continuous_space/continuous_space.py,sha256=UcD-nsi5oEAJzf8ZI7BU26FYn6DdJzsW-dgXqaZIrQk,9530
76
+ mesa/experimental/continuous_space/continuous_space_agents.py,sha256=859ypIiWMgjknTsER0i8Y6aCWjMpw6MC5cfoKa6l_iA,3044
74
77
  mesa/experimental/devs/__init__.py,sha256=wkDrpqQ3qHGqrsOSTD-UOj-qOw0oFgnCw_ZXr9xFs90,1200
75
78
  mesa/experimental/devs/eventlist.py,sha256=6igPkHJt-syLcdpFV14_n6HGej2F1cM9giDQo85fHPw,7217
76
79
  mesa/experimental/devs/simulator.py,sha256=UiVRIlNodSIveD2mS_8-vj0T_FulU8vhXxSxCfsK1Vc,12991
@@ -79,16 +82,16 @@ mesa/experimental/mesa_signals/mesa_signal.py,sha256=Vxo4gIV6a959MANL3RMANsGh0R9
79
82
  mesa/experimental/mesa_signals/observable_collections.py,sha256=rHEj6BYxLHFFGzSdoDKAdtzJ6y-IHHfcP3qEDJJsY6Y,3917
80
83
  mesa/experimental/mesa_signals/signals_util.py,sha256=fmq_FsIxsIvGjtmc4A9TGdBUtdliMHhEOpjRXivRDjA,1618
81
84
  mesa/visualization/__init__.py,sha256=YW-oHEOTjbtDKD_TylAMtVnt8mrsz1Fw7ifdc4WeHxA,743
82
- mesa/visualization/mpl_space_drawing.py,sha256=GVpNwdAN1Q_VzkSsQBC2_OBrGo1hwRXSi4n-lgGZlT8,20077
83
- mesa/visualization/solara_viz.py,sha256=BjhmH2FLlVc8rxfAze9Ex1wj_0jkVOH-_bXz2MYzd2A,19325
85
+ mesa/visualization/mpl_space_drawing.py,sha256=iqm1PYUUsmhUIraK8L9OTcTaDPDYQtlQCKtepREBA5c,20326
86
+ mesa/visualization/solara_viz.py,sha256=ItExWMLjg7rHb5RGlZx99YsuPhmC4i0ZCaY1MYzgqZ4,20931
84
87
  mesa/visualization/user_param.py,sha256=Dl2WOwLYLf0pfLpabCZtIdFRyKZrK6Qtc3utZx5GPYg,2139
85
88
  mesa/visualization/utils.py,sha256=lJHgRKF5BHLf72Tw3YpwyiWuRoIimaTKQ7xBCw_Rx3A,146
86
89
  mesa/visualization/components/__init__.py,sha256=Bq3nrPikcaIo9BSs0O3zptWVLlUmAkLo3s0mEmpH1RE,3022
87
90
  mesa/visualization/components/altair_components.py,sha256=wotpFFQgMY-ZR3lNVm_fRos-iDg0Wjnj6Tk67_7f1SQ,5847
88
91
  mesa/visualization/components/matplotlib_components.py,sha256=xQETaFyHIfmL_9JwrLIgubuIQ7-pp7TMoXT1WMmozus,5441
89
- mesa-3.1.2.dist-info/METADATA,sha256=GCOm4kE42KgetomHYGWGoHjI_jWBELOtU-ejvyC_SMs,9949
90
- mesa-3.1.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
91
- mesa-3.1.2.dist-info/entry_points.txt,sha256=IOcQtetGF8l4wHpOs_hGb19Rz-FS__BMXOJR10IBPsA,39
92
- mesa-3.1.2.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
93
- mesa-3.1.2.dist-info/licenses/NOTICE,sha256=GbsWoK0QWv1JyZ_xer2s-jNilv0RtWl-0UrtlJANHPg,578
94
- mesa-3.1.2.dist-info/RECORD,,
92
+ mesa-3.1.3.dist-info/METADATA,sha256=4-LYctEQ5WYoTPl1jXxSOrIn6vfKV4lLTR_l7byd7E8,9970
93
+ mesa-3.1.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
94
+ mesa-3.1.3.dist-info/entry_points.txt,sha256=IOcQtetGF8l4wHpOs_hGb19Rz-FS__BMXOJR10IBPsA,39
95
+ mesa-3.1.3.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
96
+ mesa-3.1.3.dist-info/licenses/NOTICE,sha256=GbsWoK0QWv1JyZ_xer2s-jNilv0RtWl-0UrtlJANHPg,578
97
+ mesa-3.1.3.dist-info/RECORD,,
File without changes