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 +1 -1
- mesa/datacollection.py +62 -2
- mesa/examples/advanced/sugarscape_g1mt/app.py +15 -37
- mesa/examples/basic/boid_flockers/agents.py +26 -38
- mesa/examples/basic/boid_flockers/app.py +6 -1
- mesa/examples/basic/boid_flockers/model.py +30 -37
- mesa/experimental/__init__.py +2 -2
- mesa/experimental/cell_space/grid.py +8 -8
- mesa/experimental/cell_space/voronoi.py +1 -4
- mesa/experimental/continuous_space/__init__.py +8 -0
- mesa/experimental/continuous_space/continuous_space.py +273 -0
- mesa/experimental/continuous_space/continuous_space_agents.py +101 -0
- mesa/model.py +1 -1
- mesa/space.py +7 -0
- mesa/visualization/__init__.py +1 -2
- mesa/visualization/components/altair_components.py +10 -8
- mesa/visualization/mpl_space_drawing.py +160 -101
- mesa/visualization/solara_viz.py +35 -8
- {mesa-3.1.2.dist-info → mesa-3.1.4.dist-info}/METADATA +14 -10
- {mesa-3.1.2.dist-info → mesa-3.1.4.dist-info}/RECORD +23 -21
- mesa-3.1.2.dist-info/entry_points.txt +0 -2
- {mesa-3.1.2.dist-info → mesa-3.1.4.dist-info}/WHEEL +0 -0
- {mesa-3.1.2.dist-info → mesa-3.1.4.dist-info}/licenses/LICENSE +0 -0
- {mesa-3.1.2.dist-info → mesa-3.1.4.dist-info}/licenses/NOTICE +0 -0
|
@@ -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(
|
|
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()
|
mesa/visualization/__init__.py
CHANGED
|
@@ -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
|
|
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 :
|
|
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(
|
|
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.
|
|
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
|
],
|