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.
@@ -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()
@@ -1,9 +1,8 @@
1
1
  """Solara based visualization for Mesa models.
2
2
 
3
3
  .. note::
4
- SolaraViz is experimental and still in active development for Mesa 3.0. While we attempt to minimize them, there might be API breaking changes between Mesa 3.0 and 3.1.
4
+ SolaraViz is experimental and still in active development in Mesa 3.x. While we attempt to minimize them, there might be API breaking changes in minor releases.
5
5
 
6
- There won't be breaking changes between Mesa 3.0.x patch releases.
7
6
  """
8
7
 
9
8
  from mesa.visualization.mpl_space_drawing import (
@@ -1,13 +1,10 @@
1
1
  """Altair based solara components for visualization mesa spaces."""
2
2
 
3
- import contextlib
4
3
  import warnings
5
4
 
5
+ import altair as alt
6
6
  import solara
7
7
 
8
- with contextlib.suppress(ImportError):
9
- import altair as alt
10
-
11
8
  from mesa.experimental.cell_space import DiscreteSpace, Grid
12
9
  from mesa.space import ContinuousSpace, _Grid
13
10
  from mesa.visualization.utils import update_counter
@@ -30,7 +27,7 @@ def make_altair_space(
30
27
  Args:
31
28
  agent_portrayal: Function to portray agents.
32
29
  propertylayer_portrayal: not yet implemented
33
- post_process :not yet implemented
30
+ post_process :A user specified callable that will be called with the Chart instance from Altair. Allows for fine tuning plots (e.g., control ticks)
34
31
  space_drawing_kwargs : not yet implemented
35
32
 
36
33
  ``agent_portrayal`` is called with an agent and should return a dict. Valid fields in this dict are "color",
@@ -46,13 +43,15 @@ def make_altair_space(
46
43
  return {"id": a.unique_id}
47
44
 
48
45
  def MakeSpaceAltair(model):
49
- return SpaceAltair(model, agent_portrayal)
46
+ return SpaceAltair(model, agent_portrayal, post_process=post_process)
50
47
 
51
48
  return MakeSpaceAltair
52
49
 
53
50
 
54
51
  @solara.component
55
- def SpaceAltair(model, agent_portrayal, dependencies: list[any] | None = None):
52
+ def SpaceAltair(
53
+ model, agent_portrayal, dependencies: list[any] | None = None, post_process=None
54
+ ):
56
55
  """Create an Altair-based space visualization component.
57
56
 
58
57
  Returns:
@@ -65,6 +64,9 @@ def SpaceAltair(model, agent_portrayal, dependencies: list[any] | None = None):
65
64
  space = model.space
66
65
 
67
66
  chart = _draw_grid(space, agent_portrayal)
67
+ # Apply post-processing if provided
68
+ if post_process is not None:
69
+ chart = post_process(chart)
68
70
  solara.FigureAltair(chart)
69
71
 
70
72
 
@@ -159,7 +161,7 @@ def _draw_grid(space, agent_portrayal):
159
161
  # no y-axis label
160
162
  "y": alt.Y("y", axis=None, type=x_y_type),
161
163
  "tooltip": [
162
- alt.Tooltip(key, type=alt.utils.infer_vegalite_type([value]))
164
+ alt.Tooltip(key, type=alt.utils.infer_vegalite_type_for_pandas([value]))
163
165
  for key, value in all_agent_data[0].items()
164
166
  if key not in invalid_tooltips
165
167
  ],