Mesa 3.1.1__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.

@@ -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:
@@ -17,7 +17,9 @@
17
17
  },
18
18
  {
19
19
  "cell_type": "code",
20
+ "execution_count": null,
20
21
  "metadata": {},
22
+ "outputs": [],
21
23
  "source": [
22
24
  "import matplotlib.pyplot as plt\n",
23
25
  "import pandas as pd\n",
@@ -25,9 +27,7 @@
25
27
  "%matplotlib inline\n",
26
28
  "\n",
27
29
  "from model import Schelling"
28
- ],
29
- "outputs": [],
30
- "execution_count": null
30
+ ]
31
31
  },
32
32
  {
33
33
  "cell_type": "markdown",
@@ -38,10 +38,14 @@
38
38
  },
39
39
  {
40
40
  "cell_type": "code",
41
+ "execution_count": null,
41
42
  "metadata": {},
42
- "source": "model = Schelling(height=10, width=10, homophily=3, density=0.8, minority_pc=0.2)",
43
43
  "outputs": [],
44
- "execution_count": null
44
+ "source": [
45
+ "schelling_model = Schelling(\n",
46
+ " height=10, width=10, homophily=3, density=0.8, minority_pc=0.2\n",
47
+ ")"
48
+ ]
45
49
  },
46
50
  {
47
51
  "cell_type": "markdown",
@@ -52,14 +56,14 @@
52
56
  },
53
57
  {
54
58
  "cell_type": "code",
59
+ "execution_count": null,
55
60
  "metadata": {},
56
- "source": [
57
- "while model.running and model.steps < 100:\n",
58
- " model.step()\n",
59
- "print(model.steps) # Show how many steps have actually run"
60
- ],
61
61
  "outputs": [],
62
- "execution_count": null
62
+ "source": [
63
+ "while schelling_model.running and schelling_model.steps < 100:\n",
64
+ " schelling_model.step()\n",
65
+ "print(schelling_model.steps) # Show how many steps have actually run"
66
+ ]
63
67
  },
64
68
  {
65
69
  "cell_type": "markdown",
@@ -70,21 +74,21 @@
70
74
  },
71
75
  {
72
76
  "cell_type": "code",
77
+ "execution_count": null,
73
78
  "metadata": {},
74
- "source": [
75
- "model_out = model.datacollector.get_model_vars_dataframe()"
76
- ],
77
79
  "outputs": [],
78
- "execution_count": null
80
+ "source": [
81
+ "model_out = schelling_model.datacollector.get_model_vars_dataframe()"
82
+ ]
79
83
  },
80
84
  {
81
85
  "cell_type": "code",
86
+ "execution_count": null,
82
87
  "metadata": {},
88
+ "outputs": [],
83
89
  "source": [
84
90
  "model_out.head()"
85
- ],
86
- "outputs": [],
87
- "execution_count": null
91
+ ]
88
92
  },
89
93
  {
90
94
  "cell_type": "markdown",
@@ -95,12 +99,12 @@
95
99
  },
96
100
  {
97
101
  "cell_type": "code",
102
+ "execution_count": null,
98
103
  "metadata": {},
104
+ "outputs": [],
99
105
  "source": [
100
106
  "model_out.happy.plot()"
101
- ],
102
- "outputs": [],
103
- "execution_count": null
107
+ ]
104
108
  },
105
109
  {
106
110
  "cell_type": "markdown",
@@ -115,10 +119,12 @@
115
119
  },
116
120
  {
117
121
  "cell_type": "code",
122
+ "execution_count": null,
118
123
  "metadata": {},
119
- "source": "from mesa.batchrunner import batch_run",
120
124
  "outputs": [],
121
- "execution_count": null
125
+ "source": [
126
+ "from mesa.batchrunner import batch_run"
127
+ ]
122
128
  },
123
129
  {
124
130
  "cell_type": "markdown",
@@ -129,18 +135,20 @@
129
135
  },
130
136
  {
131
137
  "cell_type": "code",
138
+ "execution_count": null,
132
139
  "metadata": {},
140
+ "outputs": [],
133
141
  "source": [
134
142
  "fixed_params = {\"height\": 10, \"width\": 10, \"density\": 0.8, \"minority_pc\": 0.2}\n",
135
143
  "variable_parms = {\"homophily\": range(1, 9)}\n",
136
144
  "all_params = fixed_params | variable_parms"
137
- ],
138
- "outputs": [],
139
- "execution_count": null
145
+ ]
140
146
  },
141
147
  {
142
148
  "cell_type": "code",
149
+ "execution_count": null,
143
150
  "metadata": {},
151
+ "outputs": [],
144
152
  "source": [
145
153
  "results = batch_run(\n",
146
154
  " Schelling,\n",
@@ -148,23 +156,23 @@
148
156
  " iterations=10,\n",
149
157
  " max_steps=200,\n",
150
158
  ")"
151
- ],
152
- "outputs": [],
153
- "execution_count": null
159
+ ]
154
160
  },
155
161
  {
156
- "metadata": {},
157
162
  "cell_type": "code",
163
+ "execution_count": null,
164
+ "metadata": {},
165
+ "outputs": [],
158
166
  "source": [
159
167
  "df = pd.DataFrame(results)\n",
160
168
  "df"
161
- ],
162
- "outputs": [],
163
- "execution_count": null
169
+ ]
164
170
  },
165
171
  {
166
172
  "cell_type": "code",
173
+ "execution_count": null,
167
174
  "metadata": {},
175
+ "outputs": [],
168
176
  "source": [
169
177
  "plt.scatter(df.homophily, df.happy)\n",
170
178
  "plt.xlabel(\"Homophily\")\n",
@@ -172,9 +180,7 @@
172
180
  "plt.grid()\n",
173
181
  "plt.title(\"Effect of Homophily on segregation\")\n",
174
182
  "plt.show()"
175
- ],
176
- "outputs": [],
177
- "execution_count": null
183
+ ]
178
184
  }
179
185
  ],
180
186
  "metadata": {
@@ -26,7 +26,7 @@ model_params = {
26
26
  },
27
27
  "density": Slider("Agent density", 0.8, 0.1, 1.0, 0.1),
28
28
  "minority_pc": Slider("Fraction minority", 0.2, 0.0, 1.0, 0.05),
29
- "homophily": Slider("Homophily", 0.3, 0.0, 0.8, 0.1),
29
+ "homophily": Slider("Homophily", 0.4, 0.0, 1.0, 0.125),
30
30
  "width": 20,
31
31
  "height": 20,
32
32
  }
@@ -9,11 +9,11 @@ class Schelling(Model):
9
9
 
10
10
  def __init__(
11
11
  self,
12
- height: int = 40,
13
- width: int = 40,
12
+ height: int = 20,
13
+ width: int = 20,
14
14
  density: float = 0.8,
15
15
  minority_pc: float = 0.5,
16
- homophily: int = 3,
16
+ homophily: float = 0.4,
17
17
  radius: int = 1,
18
18
  seed=None,
19
19
  ):
@@ -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
+ )