Mesa 3.0.0b0__py3-none-any.whl → 3.0.0b2__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 +2 -1
- mesa/agent.py +37 -27
- mesa/examples/README.md +37 -0
- mesa/examples/__init__.py +21 -0
- mesa/examples/advanced/__init__.py +0 -0
- mesa/examples/advanced/epstein_civil_violence/Epstein Civil Violence.ipynb +116 -0
- mesa/examples/advanced/epstein_civil_violence/Readme.md +34 -0
- mesa/examples/advanced/epstein_civil_violence/__init__.py +0 -0
- mesa/examples/advanced/epstein_civil_violence/agents.py +158 -0
- mesa/examples/advanced/epstein_civil_violence/app.py +72 -0
- mesa/examples/advanced/epstein_civil_violence/model.py +146 -0
- mesa/examples/advanced/pd_grid/Readme.md +43 -0
- mesa/examples/advanced/pd_grid/__init__.py +0 -0
- mesa/examples/advanced/pd_grid/agents.py +50 -0
- mesa/examples/advanced/pd_grid/analysis.ipynb +228 -0
- mesa/examples/advanced/pd_grid/app.py +50 -0
- mesa/examples/advanced/pd_grid/model.py +71 -0
- mesa/examples/advanced/sugarscape_g1mt/Readme.md +64 -0
- mesa/examples/advanced/sugarscape_g1mt/__init__.py +0 -0
- mesa/examples/advanced/sugarscape_g1mt/agents.py +344 -0
- mesa/examples/advanced/sugarscape_g1mt/app.py +70 -0
- mesa/examples/advanced/sugarscape_g1mt/model.py +180 -0
- mesa/examples/advanced/sugarscape_g1mt/sugar-map.txt +50 -0
- mesa/examples/advanced/sugarscape_g1mt/tests.py +69 -0
- mesa/examples/advanced/wolf_sheep/Readme.md +57 -0
- mesa/examples/advanced/wolf_sheep/__init__.py +0 -0
- mesa/examples/advanced/wolf_sheep/agents.py +102 -0
- mesa/examples/advanced/wolf_sheep/app.py +77 -0
- mesa/examples/advanced/wolf_sheep/model.py +137 -0
- mesa/examples/basic/__init__.py +0 -0
- mesa/examples/basic/boid_flockers/Readme.md +22 -0
- mesa/examples/basic/boid_flockers/__init__.py +0 -0
- mesa/examples/basic/boid_flockers/agents.py +71 -0
- mesa/examples/basic/boid_flockers/app.py +58 -0
- mesa/examples/basic/boid_flockers/model.py +69 -0
- mesa/examples/basic/boltzmann_wealth_model/Readme.md +56 -0
- mesa/examples/basic/boltzmann_wealth_model/__init__.py +0 -0
- mesa/examples/basic/boltzmann_wealth_model/agents.py +31 -0
- mesa/examples/basic/boltzmann_wealth_model/app.py +65 -0
- mesa/examples/basic/boltzmann_wealth_model/model.py +43 -0
- mesa/examples/basic/boltzmann_wealth_model/st_app.py +115 -0
- mesa/examples/basic/conways_game_of_life/Readme.md +39 -0
- mesa/examples/basic/conways_game_of_life/__init__.py +0 -0
- mesa/examples/basic/conways_game_of_life/agents.py +47 -0
- mesa/examples/basic/conways_game_of_life/app.py +39 -0
- mesa/examples/basic/conways_game_of_life/model.py +31 -0
- mesa/examples/basic/conways_game_of_life/st_app.py +72 -0
- mesa/examples/basic/schelling/Readme.md +40 -0
- mesa/examples/basic/schelling/__init__.py +0 -0
- mesa/examples/basic/schelling/agents.py +26 -0
- mesa/examples/basic/schelling/analysis.ipynb +205 -0
- mesa/examples/basic/schelling/app.py +42 -0
- mesa/examples/basic/schelling/model.py +59 -0
- mesa/examples/basic/virus_on_network/Readme.md +61 -0
- mesa/examples/basic/virus_on_network/__init__.py +0 -0
- mesa/examples/basic/virus_on_network/agents.py +69 -0
- mesa/examples/basic/virus_on_network/app.py +136 -0
- mesa/examples/basic/virus_on_network/model.py +96 -0
- mesa/experimental/__init__.py +8 -2
- mesa/experimental/cell_space/cell.py +9 -0
- mesa/experimental/cell_space/discrete_space.py +13 -1
- mesa/experimental/cell_space/grid.py +13 -0
- mesa/experimental/cell_space/network.py +3 -0
- mesa/experimental/devs/eventlist.py +6 -0
- mesa/model.py +76 -12
- mesa/space.py +70 -5
- mesa/time.py +5 -3
- mesa/visualization/components/altair.py +87 -19
- mesa/visualization/components/matplotlib.py +65 -16
- mesa/visualization/solara_viz.py +13 -58
- {mesa-3.0.0b0.dist-info → mesa-3.0.0b2.dist-info}/METADATA +1 -3
- mesa-3.0.0b2.dist-info/RECORD +93 -0
- mesa/cookiecutter-mesa/cookiecutter.json +0 -8
- mesa/cookiecutter-mesa/hooks/post_gen_project.py +0 -13
- mesa/cookiecutter-mesa/{{cookiecutter.snake}}/README.md +0 -4
- mesa/cookiecutter-mesa/{{cookiecutter.snake}}/app.pytemplate +0 -27
- mesa/cookiecutter-mesa/{{cookiecutter.snake}}/setup.pytemplate +0 -11
- mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/__init__.py +0 -1
- mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/model.pytemplate +0 -60
- mesa/main.py +0 -65
- mesa-3.0.0b0.dist-info/RECORD +0 -45
- {mesa-3.0.0b0.dist-info → mesa-3.0.0b2.dist-info}/WHEEL +0 -0
- {mesa-3.0.0b0.dist-info → mesa-3.0.0b2.dist-info}/entry_points.txt +0 -0
- {mesa-3.0.0b0.dist-info → mesa-3.0.0b2.dist-info}/licenses/LICENSE +0 -0
- {mesa-3.0.0b0.dist-info → mesa-3.0.0b2.dist-info}/licenses/NOTICE +0 -0
|
@@ -7,6 +7,7 @@ from functools import cached_property
|
|
|
7
7
|
from random import Random
|
|
8
8
|
from typing import Any, Generic, TypeVar
|
|
9
9
|
|
|
10
|
+
from mesa.agent import AgentSet
|
|
10
11
|
from mesa.experimental.cell_space.cell import Cell
|
|
11
12
|
from mesa.experimental.cell_space.cell_collection import CellCollection
|
|
12
13
|
from mesa.space import PropertyLayer
|
|
@@ -22,7 +23,7 @@ class DiscreteSpace(Generic[T]):
|
|
|
22
23
|
all_cells (CellCollection): The cells composing the discrete space
|
|
23
24
|
random (Random): The random number generator
|
|
24
25
|
cell_klass (Type) : the type of cell class
|
|
25
|
-
empties (CellCollection) :
|
|
26
|
+
empties (CellCollection) : collection of all cells that are empty
|
|
26
27
|
property_layers (dict[str, PropertyLayer]): the property layers of the discrete space
|
|
27
28
|
"""
|
|
28
29
|
|
|
@@ -55,6 +56,12 @@ class DiscreteSpace(Generic[T]):
|
|
|
55
56
|
def cutoff_empties(self): # noqa
|
|
56
57
|
return 7.953 * len(self._cells) ** 0.384
|
|
57
58
|
|
|
59
|
+
@property
|
|
60
|
+
def agents(self) -> AgentSet:
|
|
61
|
+
"""Return an AgentSet with the agents in the space."""
|
|
62
|
+
return AgentSet(self.all_cells.agents, random=self.random)
|
|
63
|
+
|
|
64
|
+
def _connect_cells(self): ...
|
|
58
65
|
def _connect_single_cell(self, cell: T): ...
|
|
59
66
|
|
|
60
67
|
@cached_property
|
|
@@ -134,3 +141,8 @@ class DiscreteSpace(Generic[T]):
|
|
|
134
141
|
condition: a function that takes a cell and returns a boolean (used to filter cells)
|
|
135
142
|
"""
|
|
136
143
|
self.property_layers[property_name].modify_cells(operation, value, condition)
|
|
144
|
+
|
|
145
|
+
def __setstate__(self, state):
|
|
146
|
+
"""Set the state of the discrete space and rebuild the connections."""
|
|
147
|
+
self.__dict__ = state
|
|
148
|
+
self._connect_cells()
|
|
@@ -22,8 +22,21 @@ class Grid(DiscreteSpace[T], Generic[T]):
|
|
|
22
22
|
random (Random): the random number generator
|
|
23
23
|
_try_random (bool): whether to get empty cell be repeatedly trying random cell
|
|
24
24
|
|
|
25
|
+
Notes:
|
|
26
|
+
width and height are accessible via properties, higher dimensions can be retrieved via dimensions
|
|
27
|
+
|
|
25
28
|
"""
|
|
26
29
|
|
|
30
|
+
@property
|
|
31
|
+
def width(self) -> int:
|
|
32
|
+
"""Convenience access to the width of the grid."""
|
|
33
|
+
return self.dimensions[0]
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def height(self) -> int:
|
|
37
|
+
"""Convenience access to the height of the grid."""
|
|
38
|
+
return self.dimensions[1]
|
|
39
|
+
|
|
27
40
|
def __init__(
|
|
28
41
|
self,
|
|
29
42
|
dimensions: Sequence[int],
|
|
@@ -33,6 +33,12 @@ class SimulationEvent:
|
|
|
33
33
|
function_args (list[Any]): Argument for the function
|
|
34
34
|
function_kwargs (Dict[str, Any]): Keyword arguments for the function
|
|
35
35
|
|
|
36
|
+
|
|
37
|
+
Notes:
|
|
38
|
+
simulation events use a weak reference to the callable. Therefore, you cannot pass a lambda function in fn.
|
|
39
|
+
A simulation event where the callable no longer exists (e.g., because the agent has been removed from the model)
|
|
40
|
+
will fail silently.
|
|
41
|
+
|
|
36
42
|
"""
|
|
37
43
|
|
|
38
44
|
_ids = itertools.count()
|
mesa/model.py
CHANGED
|
@@ -8,14 +8,21 @@ Core Objects: Model
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import random
|
|
11
|
+
import sys
|
|
11
12
|
import warnings
|
|
13
|
+
from collections.abc import Sequence
|
|
12
14
|
|
|
13
15
|
# mypy
|
|
14
16
|
from typing import Any
|
|
15
17
|
|
|
18
|
+
import numpy as np
|
|
19
|
+
|
|
16
20
|
from mesa.agent import Agent, AgentSet
|
|
17
21
|
from mesa.datacollection import DataCollector
|
|
18
22
|
|
|
23
|
+
SeedLike = int | np.integer | Sequence[int] | np.random.SeedSequence
|
|
24
|
+
RNGLike = np.random.Generator | np.random.BitGenerator
|
|
25
|
+
|
|
19
26
|
|
|
20
27
|
class Model:
|
|
21
28
|
"""Base class for models in the Mesa ABM library.
|
|
@@ -28,7 +35,8 @@ class Model:
|
|
|
28
35
|
running: A boolean indicating if the model should continue running.
|
|
29
36
|
schedule: An object to manage the order and execution of agent steps.
|
|
30
37
|
steps: the number of times `model.step()` has been called.
|
|
31
|
-
random: a seeded random number generator.
|
|
38
|
+
random: a seeded python.random number generator.
|
|
39
|
+
rng : a seeded numpy.random.Generator
|
|
32
40
|
|
|
33
41
|
Notes:
|
|
34
42
|
Model.agents returns the AgentSet containing all agents registered with the model. Changing
|
|
@@ -37,7 +45,13 @@ class Model:
|
|
|
37
45
|
|
|
38
46
|
"""
|
|
39
47
|
|
|
40
|
-
def __init__(
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
*args: Any,
|
|
51
|
+
seed: float | None = None,
|
|
52
|
+
rng: RNGLike | SeedLike | None = None,
|
|
53
|
+
**kwargs: Any,
|
|
54
|
+
) -> None:
|
|
41
55
|
"""Create a new model.
|
|
42
56
|
|
|
43
57
|
Overload this method with the actual code to initialize the model. Always start with super().__init__()
|
|
@@ -46,25 +60,51 @@ class Model:
|
|
|
46
60
|
Args:
|
|
47
61
|
args: arguments to pass onto super
|
|
48
62
|
seed: the seed for the random number generator
|
|
63
|
+
rng : Pseudorandom number generator state. When `rng` is None, a new `numpy.random.Generator` is created
|
|
64
|
+
using entropy from the operating system. Types other than `numpy.random.Generator` are passed to
|
|
65
|
+
`numpy.random.default_rng` to instantiate a `Generator`.
|
|
49
66
|
kwargs: keyword arguments to pass onto super
|
|
67
|
+
|
|
68
|
+
Notes:
|
|
69
|
+
you have to pass either seed or rng, but not both.
|
|
70
|
+
|
|
50
71
|
"""
|
|
51
72
|
super().__init__(*args, **kwargs)
|
|
52
73
|
self.running = True
|
|
53
74
|
self.steps: int = 0
|
|
54
75
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
76
|
+
if (seed is not None) and (rng is not None):
|
|
77
|
+
raise ValueError("you have to pass either rng or seed, not both")
|
|
78
|
+
elif seed is None:
|
|
79
|
+
self.rng: np.random.Generator = np.random.default_rng(rng)
|
|
80
|
+
self._rng = (
|
|
81
|
+
self.rng.bit_generator.state
|
|
82
|
+
) # this allows for reproducing the rng
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
self.random = random.Random(rng)
|
|
86
|
+
except TypeError:
|
|
87
|
+
seed = int(self.rng.integers(np.iinfo(np.int32).max))
|
|
88
|
+
self.random = random.Random(seed)
|
|
89
|
+
self._seed = seed # this allows for reproducing stdlib.random
|
|
90
|
+
elif rng is None:
|
|
91
|
+
self.random = random.Random(seed)
|
|
92
|
+
self._seed = seed # this allows for reproducing stdlib.random
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
self.rng: np.random.Generator = np.random.default_rng(rng)
|
|
96
|
+
except TypeError:
|
|
97
|
+
rng = self.random.randint(0, sys.maxsize)
|
|
98
|
+
self.rng: np.random.Generator = np.random.default_rng(rng)
|
|
99
|
+
self._rng = self.rng.bit_generator.state
|
|
63
100
|
|
|
64
101
|
# Wrap the user-defined step method
|
|
65
102
|
self._user_step = self.step
|
|
66
103
|
self.step = self._wrapped_step
|
|
67
104
|
|
|
105
|
+
# setup agent registration data structures
|
|
106
|
+
self._setup_agent_registration()
|
|
107
|
+
|
|
68
108
|
def _wrapped_step(self, *args: Any, **kwargs: Any) -> None:
|
|
69
109
|
"""Automatically increments time and steps after calling the user's step method."""
|
|
70
110
|
# Automatically increment time and step counters
|
|
@@ -119,7 +159,9 @@ class Model:
|
|
|
119
159
|
self._agents_by_type: dict[
|
|
120
160
|
type[Agent], AgentSet
|
|
121
161
|
] = {} # a dict with an agentset for each class of agents
|
|
122
|
-
self._all_agents = AgentSet(
|
|
162
|
+
self._all_agents = AgentSet(
|
|
163
|
+
[], random=self.random
|
|
164
|
+
) # an agenset with all agents
|
|
123
165
|
|
|
124
166
|
def register_agent(self, agent):
|
|
125
167
|
"""Register the agent with the model.
|
|
@@ -153,7 +195,7 @@ class Model:
|
|
|
153
195
|
[
|
|
154
196
|
agent,
|
|
155
197
|
],
|
|
156
|
-
self,
|
|
198
|
+
random=self.random,
|
|
157
199
|
)
|
|
158
200
|
|
|
159
201
|
self._all_agents.add(agent)
|
|
@@ -194,6 +236,15 @@ class Model:
|
|
|
194
236
|
self.random.seed(seed)
|
|
195
237
|
self._seed = seed
|
|
196
238
|
|
|
239
|
+
def reset_rng(self, rng: RNGLike | SeedLike | None = None) -> None:
|
|
240
|
+
"""Reset the model random number generator.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
rng: A new seed for the RNG; if None, reset using the current seed
|
|
244
|
+
"""
|
|
245
|
+
self.rng = np.random.default_rng(rng)
|
|
246
|
+
self._rng = self.rng.bit_generator.state
|
|
247
|
+
|
|
197
248
|
def initialize_data_collector(
|
|
198
249
|
self,
|
|
199
250
|
model_reporters=None,
|
|
@@ -225,3 +276,16 @@ class Model:
|
|
|
225
276
|
)
|
|
226
277
|
# Collect data for the first time during initialization.
|
|
227
278
|
self.datacollector.collect(self)
|
|
279
|
+
|
|
280
|
+
def remove_all_agents(self):
|
|
281
|
+
"""Remove all agents from the model.
|
|
282
|
+
|
|
283
|
+
Notes:
|
|
284
|
+
This method calls agent.remove for all agents in the model. If you need to remove agents from
|
|
285
|
+
e.g., a SingleGrid, you can either explicitly implement your own agent.remove method or clean this up
|
|
286
|
+
near where you are calling this method.
|
|
287
|
+
|
|
288
|
+
"""
|
|
289
|
+
# we need to wrap keys in a list to avoid a RunTimeError: dictionary changed size during iteration
|
|
290
|
+
for agent in list(self._agents.keys()):
|
|
291
|
+
agent.remove()
|
mesa/space.py
CHANGED
|
@@ -2,10 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
Objects used to add a spatial component to a model.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
.. note::
|
|
6
|
+
All Grid classes (:class:`_Grid`, :class:`SingleGrid`, :class:`MultiGrid`,
|
|
7
|
+
:class:`HexGrid`, etc.) are now in maintenance-only mode. While these classes remain
|
|
8
|
+
fully supported, new development occurs in the experimental cell space module
|
|
9
|
+
(:mod:`mesa.experimental.cell_space`).
|
|
10
|
+
|
|
11
|
+
The :class:`PropertyLayer` and :class:`ContinuousSpace` classes remain fully supported
|
|
12
|
+
and actively developed.
|
|
13
|
+
|
|
14
|
+
Classes
|
|
15
|
+
-------
|
|
16
|
+
* PropertyLayer: A data layer that can be added to Grids to store cell properties
|
|
17
|
+
* SingleGrid: a Grid which strictly enforces one agent per cell.
|
|
18
|
+
* MultiGrid: a Grid where each cell can contain a set of agents.
|
|
19
|
+
* HexGrid: a Grid to handle hexagonal neighbors.
|
|
9
20
|
* ContinuousSpace: a two-dimensional space where each agent has an arbitrary position of `float`'s.
|
|
10
21
|
* NetworkGrid: a network where each node contains zero or more agents.
|
|
11
22
|
"""
|
|
@@ -32,7 +43,7 @@ import numpy as np
|
|
|
32
43
|
import numpy.typing as npt
|
|
33
44
|
|
|
34
45
|
# For Mypy
|
|
35
|
-
from .agent import Agent
|
|
46
|
+
from .agent import Agent, AgentSet
|
|
36
47
|
|
|
37
48
|
# for better performance, we calculate the tuple to use in the is_integer function
|
|
38
49
|
_types_integer = (int, np.integer)
|
|
@@ -153,6 +164,26 @@ class _Grid:
|
|
|
153
164
|
@overload
|
|
154
165
|
def __getitem__(self, index: int | Sequence[Coordinate]) -> list[GridContent]: ...
|
|
155
166
|
|
|
167
|
+
@property
|
|
168
|
+
def agents(self) -> AgentSet:
|
|
169
|
+
"""Return an AgentSet with the agents in the space."""
|
|
170
|
+
agents = []
|
|
171
|
+
for entry in self:
|
|
172
|
+
if not entry:
|
|
173
|
+
continue
|
|
174
|
+
if not isinstance(entry, list):
|
|
175
|
+
entry = [entry] # noqa PLW2901
|
|
176
|
+
for agent in entry:
|
|
177
|
+
agents.append(agent)
|
|
178
|
+
|
|
179
|
+
# getting the rng is a bit hacky because old style spaces don't have the rng
|
|
180
|
+
try:
|
|
181
|
+
rng = agents[0].random
|
|
182
|
+
except IndexError:
|
|
183
|
+
# there are no agents in the space
|
|
184
|
+
rng = None
|
|
185
|
+
return AgentSet(agents, random=rng)
|
|
186
|
+
|
|
156
187
|
@overload
|
|
157
188
|
def __getitem__(
|
|
158
189
|
self, index: tuple[int | slice, int | slice]
|
|
@@ -1333,6 +1364,19 @@ class ContinuousSpace:
|
|
|
1333
1364
|
self._index_to_agent: dict[int, Agent] = {}
|
|
1334
1365
|
self._agent_to_index: dict[Agent, int | None] = {}
|
|
1335
1366
|
|
|
1367
|
+
@property
|
|
1368
|
+
def agents(self) -> AgentSet:
|
|
1369
|
+
"""Return an AgentSet with the agents in the space."""
|
|
1370
|
+
agents = list(self._agent_to_index)
|
|
1371
|
+
|
|
1372
|
+
# getting the rng is a bit hacky because old style spaces don't have the rng
|
|
1373
|
+
try:
|
|
1374
|
+
rng = agents[0].random
|
|
1375
|
+
except IndexError:
|
|
1376
|
+
# there are no agents in the space
|
|
1377
|
+
rng = None
|
|
1378
|
+
return AgentSet(agents, random=rng)
|
|
1379
|
+
|
|
1336
1380
|
def _build_agent_cache(self):
|
|
1337
1381
|
"""Cache agents positions to speed up neighbors calculations."""
|
|
1338
1382
|
self._index_to_agent = {}
|
|
@@ -1506,6 +1550,27 @@ class NetworkGrid:
|
|
|
1506
1550
|
for node_id in self.G.nodes:
|
|
1507
1551
|
g.nodes[node_id]["agent"] = self.default_val()
|
|
1508
1552
|
|
|
1553
|
+
@property
|
|
1554
|
+
def agents(self) -> AgentSet:
|
|
1555
|
+
"""Return an AgentSet with the agents in the space."""
|
|
1556
|
+
agents = []
|
|
1557
|
+
for node_id in self.G.nodes:
|
|
1558
|
+
entry = self.G.nodes[node_id]["agent"]
|
|
1559
|
+
if not entry:
|
|
1560
|
+
continue
|
|
1561
|
+
if not isinstance(entry, list):
|
|
1562
|
+
entry = [entry]
|
|
1563
|
+
for agent in entry:
|
|
1564
|
+
agents.append(agent)
|
|
1565
|
+
|
|
1566
|
+
# getting the rng is a bit hacky because old style spaces don't have the rng
|
|
1567
|
+
try:
|
|
1568
|
+
rng = agents[0].random
|
|
1569
|
+
except IndexError:
|
|
1570
|
+
# there are no agents in the space
|
|
1571
|
+
rng = None
|
|
1572
|
+
return AgentSet(agents, random=rng)
|
|
1573
|
+
|
|
1509
1574
|
@staticmethod
|
|
1510
1575
|
def default_val() -> list:
|
|
1511
1576
|
"""Default value for a new node."""
|
mesa/time.py
CHANGED
|
@@ -77,7 +77,7 @@ class BaseScheduler:
|
|
|
77
77
|
if agents is None:
|
|
78
78
|
agents = []
|
|
79
79
|
|
|
80
|
-
self._agents: AgentSet = AgentSet(agents, model)
|
|
80
|
+
self._agents: AgentSet = AgentSet(agents, model.random)
|
|
81
81
|
|
|
82
82
|
self._remove_warning_given = False
|
|
83
83
|
self._agents_key_warning_given = False
|
|
@@ -312,7 +312,9 @@ class RandomActivationByType(BaseScheduler):
|
|
|
312
312
|
try:
|
|
313
313
|
self._agents_by_type[type(agent)].add(agent)
|
|
314
314
|
except KeyError:
|
|
315
|
-
self._agents_by_type[type(agent)] = AgentSet(
|
|
315
|
+
self._agents_by_type[type(agent)] = AgentSet(
|
|
316
|
+
[agent], self.model.random
|
|
317
|
+
)
|
|
316
318
|
|
|
317
319
|
def add(self, agent: Agent) -> None:
|
|
318
320
|
"""Add an Agent object to the schedule.
|
|
@@ -325,7 +327,7 @@ class RandomActivationByType(BaseScheduler):
|
|
|
325
327
|
try:
|
|
326
328
|
self._agents_by_type[type(agent)].add(agent)
|
|
327
329
|
except KeyError:
|
|
328
|
-
self._agents_by_type[type(agent)] = AgentSet([agent], self.model)
|
|
330
|
+
self._agents_by_type[type(agent)] = AgentSet([agent], self.model.random)
|
|
329
331
|
|
|
330
332
|
def remove(self, agent: Agent) -> None:
|
|
331
333
|
"""Remove all instances of a given agent from the schedule.
|
|
@@ -7,6 +7,8 @@ import solara
|
|
|
7
7
|
with contextlib.suppress(ImportError):
|
|
8
8
|
import altair as alt
|
|
9
9
|
|
|
10
|
+
from mesa.experimental.cell_space import DiscreteSpace, Grid
|
|
11
|
+
from mesa.space import ContinuousSpace, _Grid
|
|
10
12
|
from mesa.visualization.utils import update_counter
|
|
11
13
|
|
|
12
14
|
|
|
@@ -29,35 +31,101 @@ def SpaceAltair(model, agent_portrayal, dependencies: list[any] | None = None):
|
|
|
29
31
|
if space is None:
|
|
30
32
|
# Sometimes the space is defined as model.space instead of model.grid
|
|
31
33
|
space = model.space
|
|
34
|
+
|
|
32
35
|
chart = _draw_grid(space, agent_portrayal)
|
|
33
36
|
solara.FigureAltair(chart)
|
|
34
37
|
|
|
35
38
|
|
|
39
|
+
def _get_agent_data_old__discrete_space(space, agent_portrayal):
|
|
40
|
+
"""Format agent portrayal data for old-style discrete spaces.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
space: the mesa.space._Grid instance
|
|
44
|
+
agent_portrayal: the agent portrayal callable
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
list of dicts
|
|
48
|
+
|
|
49
|
+
"""
|
|
50
|
+
all_agent_data = []
|
|
51
|
+
for content, (x, y) in space.coord_iter():
|
|
52
|
+
if not content:
|
|
53
|
+
continue
|
|
54
|
+
if not hasattr(content, "__iter__"):
|
|
55
|
+
# Is a single grid
|
|
56
|
+
content = [content] # noqa: PLW2901
|
|
57
|
+
for agent in content:
|
|
58
|
+
# use all data from agent portrayal, and add x,y coordinates
|
|
59
|
+
agent_data = agent_portrayal(agent)
|
|
60
|
+
agent_data["x"] = x
|
|
61
|
+
agent_data["y"] = y
|
|
62
|
+
all_agent_data.append(agent_data)
|
|
63
|
+
return all_agent_data
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _get_agent_data_new_discrete_space(space: DiscreteSpace, agent_portrayal):
|
|
67
|
+
"""Format agent portrayal data for new-style discrete spaces.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
space: the mesa.experiment.cell_space.Grid instance
|
|
71
|
+
agent_portrayal: the agent portrayal callable
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
list of dicts
|
|
75
|
+
|
|
76
|
+
"""
|
|
77
|
+
all_agent_data = []
|
|
78
|
+
|
|
79
|
+
for cell in space.all_cells:
|
|
80
|
+
for agent in cell.agents:
|
|
81
|
+
agent_data = agent_portrayal(agent)
|
|
82
|
+
agent_data["x"] = cell.coordinate[0]
|
|
83
|
+
agent_data["y"] = cell.coordinate[1]
|
|
84
|
+
all_agent_data.append(agent_data)
|
|
85
|
+
return all_agent_data
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _get_agent_data_continuous_space(space: ContinuousSpace, agent_portrayal):
|
|
89
|
+
"""Format agent portrayal data for continuous space.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
space: the ContinuousSpace instance
|
|
93
|
+
agent_portrayal: the agent portrayal callable
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
list of dicts
|
|
97
|
+
"""
|
|
98
|
+
all_agent_data = []
|
|
99
|
+
for agent in space._agent_to_index:
|
|
100
|
+
agent_data = agent_portrayal(agent)
|
|
101
|
+
agent_data["x"] = agent.pos[0]
|
|
102
|
+
agent_data["y"] = agent.pos[1]
|
|
103
|
+
all_agent_data.append(agent_data)
|
|
104
|
+
return all_agent_data
|
|
105
|
+
|
|
106
|
+
|
|
36
107
|
def _draw_grid(space, agent_portrayal):
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
agent_data["y"] = y
|
|
50
|
-
all_agent_data.append(agent_data)
|
|
51
|
-
return all_agent_data
|
|
52
|
-
|
|
53
|
-
all_agent_data = portray(space)
|
|
108
|
+
match space:
|
|
109
|
+
case Grid():
|
|
110
|
+
all_agent_data = _get_agent_data_new_discrete_space(space, agent_portrayal)
|
|
111
|
+
case _Grid():
|
|
112
|
+
all_agent_data = _get_agent_data_old__discrete_space(space, agent_portrayal)
|
|
113
|
+
case ContinuousSpace():
|
|
114
|
+
all_agent_data = _get_agent_data_continuous_space(space, agent_portrayal)
|
|
115
|
+
case _:
|
|
116
|
+
raise NotImplementedError(
|
|
117
|
+
f"visualizing {type(space)} is currently not supported through altair"
|
|
118
|
+
)
|
|
119
|
+
|
|
54
120
|
invalid_tooltips = ["color", "size", "x", "y"]
|
|
55
121
|
|
|
122
|
+
x_y_type = "ordinal" if not isinstance(space, ContinuousSpace) else "nominal"
|
|
123
|
+
|
|
56
124
|
encoding_dict = {
|
|
57
125
|
# no x-axis label
|
|
58
|
-
"x": alt.X("x", axis=None, type=
|
|
126
|
+
"x": alt.X("x", axis=None, type=x_y_type),
|
|
59
127
|
# no y-axis label
|
|
60
|
-
"y": alt.Y("y", axis=None, type=
|
|
128
|
+
"y": alt.Y("y", axis=None, type=x_y_type),
|
|
61
129
|
"tooltip": [
|
|
62
130
|
alt.Tooltip(key, type=alt.utils.infer_vegalite_type([value]))
|
|
63
131
|
for key, value in all_agent_data[0].items()
|
|
@@ -11,7 +11,7 @@ from matplotlib.colors import LinearSegmentedColormap, Normalize, to_rgba
|
|
|
11
11
|
from matplotlib.figure import Figure
|
|
12
12
|
|
|
13
13
|
import mesa
|
|
14
|
-
from mesa.experimental.cell_space import VoronoiGrid
|
|
14
|
+
from mesa.experimental.cell_space import Grid, VoronoiGrid
|
|
15
15
|
from mesa.space import PropertyLayer
|
|
16
16
|
from mesa.visualization.utils import update_counter
|
|
17
17
|
|
|
@@ -52,18 +52,26 @@ def SpaceMatplotlib(
|
|
|
52
52
|
if space is None:
|
|
53
53
|
space = getattr(model, "space", None)
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
55
|
+
# https://stackoverflow.com/questions/67524641/convert-multiple-isinstance-checks-to-structural-pattern-matching
|
|
56
|
+
match space:
|
|
57
|
+
case mesa.space._Grid():
|
|
58
|
+
_draw_grid(space, space_ax, agent_portrayal, propertylayer_portrayal, model)
|
|
59
|
+
case mesa.space.ContinuousSpace():
|
|
60
|
+
_draw_continuous_space(space, space_ax, agent_portrayal, model)
|
|
61
|
+
case mesa.space.NetworkGrid():
|
|
62
|
+
_draw_network_grid(space, space_ax, agent_portrayal)
|
|
63
|
+
case VoronoiGrid():
|
|
64
|
+
_draw_voronoi(space, space_ax, agent_portrayal)
|
|
65
|
+
case Grid(): # matches OrthogonalMooreGrid, OrthogonalVonNeumannGrid, and Hexgrid
|
|
66
|
+
# fixme add a separate draw method for hexgrids in the future
|
|
67
|
+
_draw_discrete_space_grid(space, space_ax, agent_portrayal)
|
|
68
|
+
case None:
|
|
69
|
+
if propertylayer_portrayal:
|
|
70
|
+
draw_property_layers(space_ax, space, propertylayer_portrayal, model)
|
|
71
|
+
|
|
72
|
+
solara.FigureMatplotlib(
|
|
73
|
+
space_fig, format="png", bbox_inches="tight", dependencies=dependencies
|
|
74
|
+
)
|
|
67
75
|
|
|
68
76
|
|
|
69
77
|
def draw_property_layers(ax, space, propertylayer_portrayal, model):
|
|
@@ -289,6 +297,44 @@ def _draw_voronoi(space, space_ax, agent_portrayal):
|
|
|
289
297
|
space_ax.plot(*zip(*polygon), color="black") # Plot polygon edges in black
|
|
290
298
|
|
|
291
299
|
|
|
300
|
+
def _draw_discrete_space_grid(space: Grid, space_ax, agent_portrayal):
|
|
301
|
+
if space._ndims != 2:
|
|
302
|
+
raise ValueError("Space must be 2D")
|
|
303
|
+
|
|
304
|
+
def portray(g):
|
|
305
|
+
x = []
|
|
306
|
+
y = []
|
|
307
|
+
s = [] # size
|
|
308
|
+
c = [] # color
|
|
309
|
+
|
|
310
|
+
for cell in g.all_cells:
|
|
311
|
+
for agent in cell.agents:
|
|
312
|
+
data = agent_portrayal(agent)
|
|
313
|
+
x.append(cell.coordinate[0])
|
|
314
|
+
y.append(cell.coordinate[1])
|
|
315
|
+
if "size" in data:
|
|
316
|
+
s.append(data["size"])
|
|
317
|
+
if "color" in data:
|
|
318
|
+
c.append(data["color"])
|
|
319
|
+
out = {"x": x, "y": y}
|
|
320
|
+
out["s"] = s
|
|
321
|
+
if len(c) > 0:
|
|
322
|
+
out["c"] = c
|
|
323
|
+
|
|
324
|
+
return out
|
|
325
|
+
|
|
326
|
+
space_ax.set_xlim(0, space.width)
|
|
327
|
+
space_ax.set_ylim(0, space.height)
|
|
328
|
+
|
|
329
|
+
# Draw grid lines
|
|
330
|
+
for x in range(space.width + 1):
|
|
331
|
+
space_ax.axvline(x, color="gray", linestyle=":")
|
|
332
|
+
for y in range(space.height + 1):
|
|
333
|
+
space_ax.axhline(y, color="gray", linestyle=":")
|
|
334
|
+
|
|
335
|
+
space_ax.scatter(**portray(space))
|
|
336
|
+
|
|
337
|
+
|
|
292
338
|
def make_plot_measure(measure: str | dict[str, str] | list[str] | tuple[str]):
|
|
293
339
|
"""Create a plotting function for a specified measure.
|
|
294
340
|
|
|
@@ -327,11 +373,14 @@ def PlotMatplotlib(model, measure, dependencies: list[any] | None = None):
|
|
|
327
373
|
elif isinstance(measure, dict):
|
|
328
374
|
for m, color in measure.items():
|
|
329
375
|
ax.plot(df.loc[:, m], label=m, color=color)
|
|
330
|
-
|
|
376
|
+
ax.legend(loc="best")
|
|
331
377
|
elif isinstance(measure, list | tuple):
|
|
332
378
|
for m in measure:
|
|
333
379
|
ax.plot(df.loc[:, m], label=m)
|
|
334
|
-
|
|
380
|
+
ax.legend(loc="best")
|
|
381
|
+
ax.set_xlabel("Step")
|
|
335
382
|
# Set integer x axis
|
|
336
383
|
ax.xaxis.set_major_locator(plt.MaxNLocator(integer=True))
|
|
337
|
-
solara.FigureMatplotlib(
|
|
384
|
+
solara.FigureMatplotlib(
|
|
385
|
+
fig, format="png", bbox_inches="tight", dependencies=dependencies
|
|
386
|
+
)
|