Mesa 3.0.0a3__py3-none-any.whl → 3.0.0a4__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 +1 -1
- mesa/agent.py +92 -19
- mesa/batchrunner.py +3 -0
- mesa/experimental/UserParam.py +56 -0
- mesa/experimental/__init__.py +3 -1
- mesa/experimental/cell_space/cell_agent.py +2 -2
- mesa/experimental/components/altair.py +71 -0
- mesa/experimental/components/matplotlib.py +224 -0
- mesa/experimental/devs/examples/epstein_civil_violence.py +6 -10
- mesa/experimental/devs/examples/wolf_sheep.py +7 -12
- mesa/experimental/solara_viz.py +462 -0
- mesa/model.py +24 -19
- mesa/space.py +9 -3
- mesa/visualization/__init__.py +13 -2
- mesa/visualization/components/altair.py +15 -0
- mesa/visualization/components/matplotlib.py +22 -0
- mesa/visualization/solara_viz.py +121 -189
- mesa/visualization/utils.py +7 -0
- {mesa-3.0.0a3.dist-info → mesa-3.0.0a4.dist-info}/METADATA +2 -1
- {mesa-3.0.0a3.dist-info → mesa-3.0.0a4.dist-info}/RECORD +23 -18
- {mesa-3.0.0a3.dist-info → mesa-3.0.0a4.dist-info}/WHEEL +0 -0
- {mesa-3.0.0a3.dist-info → mesa-3.0.0a4.dist-info}/entry_points.txt +0 -0
- {mesa-3.0.0a3.dist-info → mesa-3.0.0a4.dist-info}/licenses/LICENSE +0 -0
mesa/__init__.py
CHANGED
|
@@ -24,7 +24,7 @@ __all__ = [
|
|
|
24
24
|
]
|
|
25
25
|
|
|
26
26
|
__title__ = "mesa"
|
|
27
|
-
__version__ = "3.0.
|
|
27
|
+
__version__ = "3.0.0a4"
|
|
28
28
|
__license__ = "Apache 2.0"
|
|
29
29
|
_this_year = datetime.datetime.now(tz=datetime.timezone.utc).date().year
|
|
30
30
|
__copyright__ = f"Copyright {_this_year} Project Mesa Team"
|
mesa/agent.py
CHANGED
|
@@ -10,6 +10,8 @@ from __future__ import annotations
|
|
|
10
10
|
|
|
11
11
|
import contextlib
|
|
12
12
|
import copy
|
|
13
|
+
import functools
|
|
14
|
+
import itertools
|
|
13
15
|
import operator
|
|
14
16
|
import warnings
|
|
15
17
|
import weakref
|
|
@@ -18,7 +20,7 @@ from collections.abc import Callable, Iterable, Iterator, MutableSet, Sequence
|
|
|
18
20
|
from random import Random
|
|
19
21
|
|
|
20
22
|
# mypy
|
|
21
|
-
from typing import TYPE_CHECKING, Any
|
|
23
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
22
24
|
|
|
23
25
|
if TYPE_CHECKING:
|
|
24
26
|
# We ensure that these are not imported during runtime to prevent cyclic
|
|
@@ -32,21 +34,49 @@ class Agent:
|
|
|
32
34
|
Base class for a model agent in Mesa.
|
|
33
35
|
|
|
34
36
|
Attributes:
|
|
35
|
-
unique_id (int): A unique identifier for this agent.
|
|
36
37
|
model (Model): A reference to the model instance.
|
|
37
|
-
|
|
38
|
+
unique_id (int): A unique identifier for this agent.
|
|
39
|
+
pos (Position): A reference to the position where this agent is located.
|
|
40
|
+
|
|
41
|
+
Notes:
|
|
42
|
+
unique_id is unique relative to a model instance and starts from 1
|
|
43
|
+
|
|
38
44
|
"""
|
|
39
45
|
|
|
40
|
-
|
|
46
|
+
# this is a class level attribute
|
|
47
|
+
# it is a dictionary, indexed by model instance
|
|
48
|
+
# so, unique_id is unique relative to a model, and counting starts from 1
|
|
49
|
+
_ids = defaultdict(functools.partial(itertools.count, 1))
|
|
50
|
+
|
|
51
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
41
52
|
"""
|
|
42
53
|
Create a new agent.
|
|
43
54
|
|
|
44
55
|
Args:
|
|
45
|
-
unique_id (int): A unique identifier for this agent.
|
|
46
56
|
model (Model): The model instance in which the agent exists.
|
|
47
57
|
"""
|
|
48
|
-
|
|
49
|
-
|
|
58
|
+
# TODO: Cleanup in future Mesa version (3.1+)
|
|
59
|
+
match args:
|
|
60
|
+
# Case 1: Only the model is provided. The new correct behavior.
|
|
61
|
+
case [model]:
|
|
62
|
+
self.model = model
|
|
63
|
+
self.unique_id = next(self._ids[model])
|
|
64
|
+
# Case 2: Both unique_id and model are provided, deprecated
|
|
65
|
+
case [_, model]:
|
|
66
|
+
warnings.warn(
|
|
67
|
+
"unique ids are assigned automatically to Agents in Mesa 3. The use of custom unique_id is "
|
|
68
|
+
"deprecated. Only input a model when calling `super()__init__(model)`. The unique_id inputted is not used.",
|
|
69
|
+
DeprecationWarning,
|
|
70
|
+
stacklevel=2,
|
|
71
|
+
)
|
|
72
|
+
self.model = model
|
|
73
|
+
self.unique_id = next(self._ids[model])
|
|
74
|
+
# Case 3: Anything else, raise an error
|
|
75
|
+
case _:
|
|
76
|
+
raise ValueError(
|
|
77
|
+
"Invalid arguments provided to initialize the Agent. Only input a model: `super()__init__(model)`."
|
|
78
|
+
)
|
|
79
|
+
|
|
50
80
|
self.pos: Position | None = None
|
|
51
81
|
|
|
52
82
|
self.model.register_agent(self)
|
|
@@ -304,29 +334,72 @@ class AgentSet(MutableSet, Sequence):
|
|
|
304
334
|
|
|
305
335
|
return res
|
|
306
336
|
|
|
307
|
-
def
|
|
337
|
+
def agg(self, attribute: str, func: Callable) -> Any:
|
|
338
|
+
"""
|
|
339
|
+
Aggregate an attribute of all agents in the AgentSet using a specified function.
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
attribute (str): The name of the attribute to aggregate.
|
|
343
|
+
func (Callable): The function to apply to the attribute values (e.g., min, max, sum, np.mean).
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
Any: The result of applying the function to the attribute values. Often a single value.
|
|
347
|
+
"""
|
|
348
|
+
values = self.get(attribute)
|
|
349
|
+
return func(values)
|
|
350
|
+
|
|
351
|
+
def get(
|
|
352
|
+
self,
|
|
353
|
+
attr_names: str | list[str],
|
|
354
|
+
handle_missing: Literal["error", "default"] = "error",
|
|
355
|
+
default_value: Any = None,
|
|
356
|
+
) -> list[Any] | list[list[Any]]:
|
|
308
357
|
"""
|
|
309
358
|
Retrieve the specified attribute(s) from each agent in the AgentSet.
|
|
310
359
|
|
|
311
360
|
Args:
|
|
312
361
|
attr_names (str | list[str]): The name(s) of the attribute(s) to retrieve from each agent.
|
|
362
|
+
handle_missing (str, optional): How to handle missing attributes. Can be:
|
|
363
|
+
- 'error' (default): raises an AttributeError if attribute is missing.
|
|
364
|
+
- 'default': returns the specified default_value.
|
|
365
|
+
default_value (Any, optional): The default value to return if 'handle_missing' is set to 'default'
|
|
366
|
+
and the agent does not have the attribute.
|
|
313
367
|
|
|
314
368
|
Returns:
|
|
315
|
-
list[Any]: A list with the attribute value for each agent
|
|
316
|
-
list[list[Any]]: A list with a
|
|
369
|
+
list[Any]: A list with the attribute value for each agent if attr_names is a str.
|
|
370
|
+
list[list[Any]]: A list with a lists of attribute values for each agent if attr_names is a list of str.
|
|
317
371
|
|
|
318
372
|
Raises:
|
|
319
|
-
AttributeError
|
|
320
|
-
|
|
321
|
-
"""
|
|
373
|
+
AttributeError: If 'handle_missing' is 'error' and the agent does not have the specified attribute(s).
|
|
374
|
+
ValueError: If an unknown 'handle_missing' option is provided.
|
|
375
|
+
"""
|
|
376
|
+
is_single_attr = isinstance(attr_names, str)
|
|
377
|
+
|
|
378
|
+
if handle_missing == "error":
|
|
379
|
+
if is_single_attr:
|
|
380
|
+
return [getattr(agent, attr_names) for agent in self._agents]
|
|
381
|
+
else:
|
|
382
|
+
return [
|
|
383
|
+
[getattr(agent, attr) for attr in attr_names]
|
|
384
|
+
for agent in self._agents
|
|
385
|
+
]
|
|
386
|
+
|
|
387
|
+
elif handle_missing == "default":
|
|
388
|
+
if is_single_attr:
|
|
389
|
+
return [
|
|
390
|
+
getattr(agent, attr_names, default_value) for agent in self._agents
|
|
391
|
+
]
|
|
392
|
+
else:
|
|
393
|
+
return [
|
|
394
|
+
[getattr(agent, attr, default_value) for attr in attr_names]
|
|
395
|
+
for agent in self._agents
|
|
396
|
+
]
|
|
322
397
|
|
|
323
|
-
if isinstance(attr_names, str):
|
|
324
|
-
return [getattr(agent, attr_names) for agent in self._agents]
|
|
325
398
|
else:
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
399
|
+
raise ValueError(
|
|
400
|
+
f"Unknown handle_missing option: {handle_missing}, "
|
|
401
|
+
"should be one of 'error' or 'default'"
|
|
402
|
+
)
|
|
330
403
|
|
|
331
404
|
def set(self, attr_name: str, value: Any) -> AgentSet:
|
|
332
405
|
"""
|
mesa/batchrunner.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import itertools
|
|
2
|
+
import multiprocessing
|
|
2
3
|
from collections.abc import Iterable, Mapping
|
|
3
4
|
from functools import partial
|
|
4
5
|
from multiprocessing import Pool
|
|
@@ -8,6 +9,8 @@ from tqdm.auto import tqdm
|
|
|
8
9
|
|
|
9
10
|
from mesa.model import Model
|
|
10
11
|
|
|
12
|
+
multiprocessing.set_start_method("spawn", force=True)
|
|
13
|
+
|
|
11
14
|
|
|
12
15
|
def batch_run(
|
|
13
16
|
model_cls: type[Model],
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
class UserParam:
|
|
2
|
+
_ERROR_MESSAGE = "Missing or malformed inputs for '{}' Option '{}'"
|
|
3
|
+
|
|
4
|
+
def maybe_raise_error(self, param_type, valid):
|
|
5
|
+
if valid:
|
|
6
|
+
return
|
|
7
|
+
msg = self._ERROR_MESSAGE.format(param_type, self.label)
|
|
8
|
+
raise ValueError(msg)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Slider(UserParam):
|
|
12
|
+
"""
|
|
13
|
+
A number-based slider input with settable increment.
|
|
14
|
+
|
|
15
|
+
Example:
|
|
16
|
+
|
|
17
|
+
slider_option = Slider("My Slider", value=123, min=10, max=200, step=0.1)
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
label: The displayed label in the UI
|
|
21
|
+
value: The initial value of the slider
|
|
22
|
+
min: The minimum possible value of the slider
|
|
23
|
+
max: The maximum possible value of the slider
|
|
24
|
+
step: The step between min and max for a range of possible values
|
|
25
|
+
dtype: either int or float
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
label="",
|
|
31
|
+
value=None,
|
|
32
|
+
min=None,
|
|
33
|
+
max=None,
|
|
34
|
+
step=1,
|
|
35
|
+
dtype=None,
|
|
36
|
+
):
|
|
37
|
+
self.label = label
|
|
38
|
+
self.value = value
|
|
39
|
+
self.min = min
|
|
40
|
+
self.max = max
|
|
41
|
+
self.step = step
|
|
42
|
+
|
|
43
|
+
# Validate option type to make sure values are supplied properly
|
|
44
|
+
valid = not (self.value is None or self.min is None or self.max is None)
|
|
45
|
+
self.maybe_raise_error("slider", valid)
|
|
46
|
+
|
|
47
|
+
if dtype is None:
|
|
48
|
+
self.is_float_slider = self._check_values_are_float(value, min, max, step)
|
|
49
|
+
else:
|
|
50
|
+
self.is_float_slider = dtype is float
|
|
51
|
+
|
|
52
|
+
def _check_values_are_float(self, value, min, max, step):
|
|
53
|
+
return any(isinstance(n, float) for n in (value, min, max, step))
|
|
54
|
+
|
|
55
|
+
def get(self, attr):
|
|
56
|
+
return getattr(self, attr)
|
mesa/experimental/__init__.py
CHANGED
|
@@ -19,7 +19,7 @@ class CellAgent(Agent):
|
|
|
19
19
|
cell: (Cell | None): the cell which the agent occupies
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
|
-
def __init__(self,
|
|
22
|
+
def __init__(self, model: Model) -> None:
|
|
23
23
|
"""
|
|
24
24
|
Create a new agent.
|
|
25
25
|
|
|
@@ -27,7 +27,7 @@ class CellAgent(Agent):
|
|
|
27
27
|
unique_id (int): A unique identifier for this agent.
|
|
28
28
|
model (Model): The model instance in which the agent exists.
|
|
29
29
|
"""
|
|
30
|
-
super().__init__(
|
|
30
|
+
super().__init__(model)
|
|
31
31
|
self.cell: Cell | None = None
|
|
32
32
|
|
|
33
33
|
def move_to(self, cell) -> None:
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
|
|
3
|
+
import solara
|
|
4
|
+
|
|
5
|
+
with contextlib.suppress(ImportError):
|
|
6
|
+
import altair as alt
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@solara.component
|
|
10
|
+
def SpaceAltair(model, agent_portrayal, dependencies: list[any] | None = None):
|
|
11
|
+
space = getattr(model, "grid", None)
|
|
12
|
+
if space is None:
|
|
13
|
+
# Sometimes the space is defined as model.space instead of model.grid
|
|
14
|
+
space = model.space
|
|
15
|
+
chart = _draw_grid(space, agent_portrayal)
|
|
16
|
+
solara.FigureAltair(chart)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _draw_grid(space, agent_portrayal):
|
|
20
|
+
def portray(g):
|
|
21
|
+
all_agent_data = []
|
|
22
|
+
for content, (x, y) in g.coord_iter():
|
|
23
|
+
if not content:
|
|
24
|
+
continue
|
|
25
|
+
if not hasattr(content, "__iter__"):
|
|
26
|
+
# Is a single grid
|
|
27
|
+
content = [content] # noqa: PLW2901
|
|
28
|
+
for agent in content:
|
|
29
|
+
# use all data from agent portrayal, and add x,y coordinates
|
|
30
|
+
agent_data = agent_portrayal(agent)
|
|
31
|
+
agent_data["x"] = x
|
|
32
|
+
agent_data["y"] = y
|
|
33
|
+
all_agent_data.append(agent_data)
|
|
34
|
+
return all_agent_data
|
|
35
|
+
|
|
36
|
+
all_agent_data = portray(space)
|
|
37
|
+
invalid_tooltips = ["color", "size", "x", "y"]
|
|
38
|
+
|
|
39
|
+
encoding_dict = {
|
|
40
|
+
# no x-axis label
|
|
41
|
+
"x": alt.X("x", axis=None, type="ordinal"),
|
|
42
|
+
# no y-axis label
|
|
43
|
+
"y": alt.Y("y", axis=None, type="ordinal"),
|
|
44
|
+
"tooltip": [
|
|
45
|
+
alt.Tooltip(key, type=alt.utils.infer_vegalite_type([value]))
|
|
46
|
+
for key, value in all_agent_data[0].items()
|
|
47
|
+
if key not in invalid_tooltips
|
|
48
|
+
],
|
|
49
|
+
}
|
|
50
|
+
has_color = "color" in all_agent_data[0]
|
|
51
|
+
if has_color:
|
|
52
|
+
encoding_dict["color"] = alt.Color("color", type="nominal")
|
|
53
|
+
has_size = "size" in all_agent_data[0]
|
|
54
|
+
if has_size:
|
|
55
|
+
encoding_dict["size"] = alt.Size("size", type="quantitative")
|
|
56
|
+
|
|
57
|
+
chart = (
|
|
58
|
+
alt.Chart(
|
|
59
|
+
alt.Data(values=all_agent_data), encoding=alt.Encoding(**encoding_dict)
|
|
60
|
+
)
|
|
61
|
+
.mark_point(filled=True)
|
|
62
|
+
.properties(width=280, height=280)
|
|
63
|
+
# .configure_view(strokeOpacity=0) # hide grid/chart lines
|
|
64
|
+
)
|
|
65
|
+
# This is the default value for the marker size, which auto-scales
|
|
66
|
+
# according to the grid area.
|
|
67
|
+
if not has_size:
|
|
68
|
+
length = min(space.width, space.height)
|
|
69
|
+
chart = chart.mark_point(size=30000 / length**2, filled=True)
|
|
70
|
+
|
|
71
|
+
return chart
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
|
|
3
|
+
import networkx as nx
|
|
4
|
+
import solara
|
|
5
|
+
from matplotlib.figure import Figure
|
|
6
|
+
from matplotlib.ticker import MaxNLocator
|
|
7
|
+
|
|
8
|
+
import mesa
|
|
9
|
+
from mesa.experimental.cell_space import VoronoiGrid
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@solara.component
|
|
13
|
+
def SpaceMatplotlib(model, agent_portrayal, dependencies: list[any] | None = None):
|
|
14
|
+
space_fig = Figure()
|
|
15
|
+
space_ax = space_fig.subplots()
|
|
16
|
+
space = getattr(model, "grid", None)
|
|
17
|
+
if space is None:
|
|
18
|
+
# Sometimes the space is defined as model.space instead of model.grid
|
|
19
|
+
space = model.space
|
|
20
|
+
if isinstance(space, mesa.space.NetworkGrid):
|
|
21
|
+
_draw_network_grid(space, space_ax, agent_portrayal)
|
|
22
|
+
elif isinstance(space, mesa.space.ContinuousSpace):
|
|
23
|
+
_draw_continuous_space(space, space_ax, agent_portrayal)
|
|
24
|
+
elif isinstance(space, VoronoiGrid):
|
|
25
|
+
_draw_voronoi(space, space_ax, agent_portrayal)
|
|
26
|
+
else:
|
|
27
|
+
_draw_grid(space, space_ax, agent_portrayal)
|
|
28
|
+
solara.FigureMatplotlib(space_fig, format="png", dependencies=dependencies)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# matplotlib scatter does not allow for multiple shapes in one call
|
|
32
|
+
def _split_and_scatter(portray_data, space_ax):
|
|
33
|
+
grouped_data = defaultdict(lambda: {"x": [], "y": [], "s": [], "c": []})
|
|
34
|
+
|
|
35
|
+
# Extract data from the dictionary
|
|
36
|
+
x = portray_data["x"]
|
|
37
|
+
y = portray_data["y"]
|
|
38
|
+
s = portray_data["s"]
|
|
39
|
+
c = portray_data["c"]
|
|
40
|
+
m = portray_data["m"]
|
|
41
|
+
|
|
42
|
+
if not (len(x) == len(y) == len(s) == len(c) == len(m)):
|
|
43
|
+
raise ValueError(
|
|
44
|
+
"Length mismatch in portrayal data lists: "
|
|
45
|
+
f"x: {len(x)}, y: {len(y)}, size: {len(s)}, "
|
|
46
|
+
f"color: {len(c)}, marker: {len(m)}"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Group the data by marker
|
|
50
|
+
for i in range(len(x)):
|
|
51
|
+
marker = m[i]
|
|
52
|
+
grouped_data[marker]["x"].append(x[i])
|
|
53
|
+
grouped_data[marker]["y"].append(y[i])
|
|
54
|
+
grouped_data[marker]["s"].append(s[i])
|
|
55
|
+
grouped_data[marker]["c"].append(c[i])
|
|
56
|
+
|
|
57
|
+
# Plot each group with the same marker
|
|
58
|
+
for marker, data in grouped_data.items():
|
|
59
|
+
space_ax.scatter(data["x"], data["y"], s=data["s"], c=data["c"], marker=marker)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _draw_grid(space, space_ax, agent_portrayal):
|
|
63
|
+
def portray(g):
|
|
64
|
+
x = []
|
|
65
|
+
y = []
|
|
66
|
+
s = [] # size
|
|
67
|
+
c = [] # color
|
|
68
|
+
m = [] # shape
|
|
69
|
+
for i in range(g.width):
|
|
70
|
+
for j in range(g.height):
|
|
71
|
+
content = g._grid[i][j]
|
|
72
|
+
if not content:
|
|
73
|
+
continue
|
|
74
|
+
if not hasattr(content, "__iter__"):
|
|
75
|
+
# Is a single grid
|
|
76
|
+
content = [content]
|
|
77
|
+
for agent in content:
|
|
78
|
+
data = agent_portrayal(agent)
|
|
79
|
+
x.append(i)
|
|
80
|
+
y.append(j)
|
|
81
|
+
|
|
82
|
+
# This is the default value for the marker size, which auto-scales
|
|
83
|
+
# according to the grid area.
|
|
84
|
+
default_size = (180 / max(g.width, g.height)) ** 2
|
|
85
|
+
# establishing a default prevents misalignment if some agents are not given size, color, etc.
|
|
86
|
+
size = data.get("size", default_size)
|
|
87
|
+
s.append(size)
|
|
88
|
+
color = data.get("color", "tab:blue")
|
|
89
|
+
c.append(color)
|
|
90
|
+
mark = data.get("shape", "o")
|
|
91
|
+
m.append(mark)
|
|
92
|
+
out = {"x": x, "y": y, "s": s, "c": c, "m": m}
|
|
93
|
+
return out
|
|
94
|
+
|
|
95
|
+
space_ax.set_xlim(-1, space.width)
|
|
96
|
+
space_ax.set_ylim(-1, space.height)
|
|
97
|
+
_split_and_scatter(portray(space), space_ax)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _draw_network_grid(space, space_ax, agent_portrayal):
|
|
101
|
+
graph = space.G
|
|
102
|
+
pos = nx.spring_layout(graph, seed=0)
|
|
103
|
+
nx.draw(
|
|
104
|
+
graph,
|
|
105
|
+
ax=space_ax,
|
|
106
|
+
pos=pos,
|
|
107
|
+
**agent_portrayal(graph),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _draw_continuous_space(space, space_ax, agent_portrayal):
|
|
112
|
+
def portray(space):
|
|
113
|
+
x = []
|
|
114
|
+
y = []
|
|
115
|
+
s = [] # size
|
|
116
|
+
c = [] # color
|
|
117
|
+
m = [] # shape
|
|
118
|
+
for agent in space._agent_to_index:
|
|
119
|
+
data = agent_portrayal(agent)
|
|
120
|
+
_x, _y = agent.pos
|
|
121
|
+
x.append(_x)
|
|
122
|
+
y.append(_y)
|
|
123
|
+
|
|
124
|
+
# This is matplotlib's default marker size
|
|
125
|
+
default_size = 20
|
|
126
|
+
# establishing a default prevents misalignment if some agents are not given size, color, etc.
|
|
127
|
+
size = data.get("size", default_size)
|
|
128
|
+
s.append(size)
|
|
129
|
+
color = data.get("color", "tab:blue")
|
|
130
|
+
c.append(color)
|
|
131
|
+
mark = data.get("shape", "o")
|
|
132
|
+
m.append(mark)
|
|
133
|
+
out = {"x": x, "y": y, "s": s, "c": c, "m": m}
|
|
134
|
+
return out
|
|
135
|
+
|
|
136
|
+
# Determine border style based on space.torus
|
|
137
|
+
border_style = "solid" if not space.torus else (0, (5, 10))
|
|
138
|
+
|
|
139
|
+
# Set the border of the plot
|
|
140
|
+
for spine in space_ax.spines.values():
|
|
141
|
+
spine.set_linewidth(1.5)
|
|
142
|
+
spine.set_color("black")
|
|
143
|
+
spine.set_linestyle(border_style)
|
|
144
|
+
|
|
145
|
+
width = space.x_max - space.x_min
|
|
146
|
+
x_padding = width / 20
|
|
147
|
+
height = space.y_max - space.y_min
|
|
148
|
+
y_padding = height / 20
|
|
149
|
+
space_ax.set_xlim(space.x_min - x_padding, space.x_max + x_padding)
|
|
150
|
+
space_ax.set_ylim(space.y_min - y_padding, space.y_max + y_padding)
|
|
151
|
+
|
|
152
|
+
# Portray and scatter the agents in the space
|
|
153
|
+
_split_and_scatter(portray(space), space_ax)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _draw_voronoi(space, space_ax, agent_portrayal):
|
|
157
|
+
def portray(g):
|
|
158
|
+
x = []
|
|
159
|
+
y = []
|
|
160
|
+
s = [] # size
|
|
161
|
+
c = [] # color
|
|
162
|
+
|
|
163
|
+
for cell in g.all_cells:
|
|
164
|
+
for agent in cell.agents:
|
|
165
|
+
data = agent_portrayal(agent)
|
|
166
|
+
x.append(cell.coordinate[0])
|
|
167
|
+
y.append(cell.coordinate[1])
|
|
168
|
+
if "size" in data:
|
|
169
|
+
s.append(data["size"])
|
|
170
|
+
if "color" in data:
|
|
171
|
+
c.append(data["color"])
|
|
172
|
+
out = {"x": x, "y": y}
|
|
173
|
+
# This is the default value for the marker size, which auto-scales
|
|
174
|
+
# according to the grid area.
|
|
175
|
+
out["s"] = s
|
|
176
|
+
if len(c) > 0:
|
|
177
|
+
out["c"] = c
|
|
178
|
+
|
|
179
|
+
return out
|
|
180
|
+
|
|
181
|
+
x_list = [i[0] for i in space.centroids_coordinates]
|
|
182
|
+
y_list = [i[1] for i in space.centroids_coordinates]
|
|
183
|
+
x_max = max(x_list)
|
|
184
|
+
x_min = min(x_list)
|
|
185
|
+
y_max = max(y_list)
|
|
186
|
+
y_min = min(y_list)
|
|
187
|
+
|
|
188
|
+
width = x_max - x_min
|
|
189
|
+
x_padding = width / 20
|
|
190
|
+
height = y_max - y_min
|
|
191
|
+
y_padding = height / 20
|
|
192
|
+
space_ax.set_xlim(x_min - x_padding, x_max + x_padding)
|
|
193
|
+
space_ax.set_ylim(y_min - y_padding, y_max + y_padding)
|
|
194
|
+
space_ax.scatter(**portray(space))
|
|
195
|
+
|
|
196
|
+
for cell in space.all_cells:
|
|
197
|
+
polygon = cell.properties["polygon"]
|
|
198
|
+
space_ax.fill(
|
|
199
|
+
*zip(*polygon),
|
|
200
|
+
alpha=min(1, cell.properties[space.cell_coloring_property]),
|
|
201
|
+
c="red",
|
|
202
|
+
) # Plot filled polygon
|
|
203
|
+
space_ax.plot(*zip(*polygon), color="black") # Plot polygon edges in red
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@solara.component
|
|
207
|
+
def PlotMatplotlib(model, measure, dependencies: list[any] | None = None):
|
|
208
|
+
fig = Figure()
|
|
209
|
+
ax = fig.subplots()
|
|
210
|
+
df = model.datacollector.get_model_vars_dataframe()
|
|
211
|
+
if isinstance(measure, str):
|
|
212
|
+
ax.plot(df.loc[:, measure])
|
|
213
|
+
ax.set_ylabel(measure)
|
|
214
|
+
elif isinstance(measure, dict):
|
|
215
|
+
for m, color in measure.items():
|
|
216
|
+
ax.plot(df.loc[:, m], label=m, color=color)
|
|
217
|
+
fig.legend()
|
|
218
|
+
elif isinstance(measure, list | tuple):
|
|
219
|
+
for m in measure:
|
|
220
|
+
ax.plot(df.loc[:, m], label=m)
|
|
221
|
+
fig.legend()
|
|
222
|
+
# Set integer x axis
|
|
223
|
+
ax.xaxis.set_major_locator(MaxNLocator(integer=True))
|
|
224
|
+
solara.FigureMatplotlib(fig, dependencies=dependencies)
|
|
@@ -7,8 +7,8 @@ from mesa.space import SingleGrid
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class EpsteinAgent(Agent):
|
|
10
|
-
def __init__(self,
|
|
11
|
-
super().__init__(
|
|
10
|
+
def __init__(self, model, vision, movement):
|
|
11
|
+
super().__init__(model)
|
|
12
12
|
self.vision = vision
|
|
13
13
|
self.movement = movement
|
|
14
14
|
|
|
@@ -46,7 +46,6 @@ class Citizen(EpsteinAgent):
|
|
|
46
46
|
|
|
47
47
|
def __init__(
|
|
48
48
|
self,
|
|
49
|
-
unique_id,
|
|
50
49
|
model,
|
|
51
50
|
vision,
|
|
52
51
|
movement,
|
|
@@ -59,7 +58,6 @@ class Citizen(EpsteinAgent):
|
|
|
59
58
|
"""
|
|
60
59
|
Create a new Citizen.
|
|
61
60
|
Args:
|
|
62
|
-
unique_id: unique int
|
|
63
61
|
model : model instance
|
|
64
62
|
hardship: Agent's 'perceived hardship (i.e., physical or economic
|
|
65
63
|
privation).' Exogenous, drawn from U(0,1).
|
|
@@ -71,7 +69,7 @@ class Citizen(EpsteinAgent):
|
|
|
71
69
|
vision: number of cells in each direction (N, S, E and W) that
|
|
72
70
|
agent can inspect. Exogenous.
|
|
73
71
|
"""
|
|
74
|
-
super().__init__(
|
|
72
|
+
super().__init__(model, vision, movement)
|
|
75
73
|
self.hardship = hardship
|
|
76
74
|
self.regime_legitimacy = regime_legitimacy
|
|
77
75
|
self.risk_aversion = risk_aversion
|
|
@@ -144,8 +142,8 @@ class Cop(EpsteinAgent):
|
|
|
144
142
|
able to inspect
|
|
145
143
|
"""
|
|
146
144
|
|
|
147
|
-
def __init__(self,
|
|
148
|
-
super().__init__(
|
|
145
|
+
def __init__(self, model, vision, movement, max_jail_term):
|
|
146
|
+
super().__init__(model, vision, movement)
|
|
149
147
|
self.max_jail_term = max_jail_term
|
|
150
148
|
|
|
151
149
|
def step(self):
|
|
@@ -236,7 +234,6 @@ class EpsteinCivilViolence(Model):
|
|
|
236
234
|
for _, pos in self.grid.coord_iter():
|
|
237
235
|
if self.random.random() < self.cop_density:
|
|
238
236
|
agent = Cop(
|
|
239
|
-
self.next_id(),
|
|
240
237
|
self,
|
|
241
238
|
cop_vision,
|
|
242
239
|
movement,
|
|
@@ -244,7 +241,6 @@ class EpsteinCivilViolence(Model):
|
|
|
244
241
|
)
|
|
245
242
|
elif self.random.random() < (self.cop_density + self.citizen_density):
|
|
246
243
|
agent = Citizen(
|
|
247
|
-
self.next_id(),
|
|
248
244
|
self,
|
|
249
245
|
citizen_vision,
|
|
250
246
|
movement,
|
|
@@ -270,4 +266,4 @@ if __name__ == "__main__":
|
|
|
270
266
|
|
|
271
267
|
simulator.setup(model)
|
|
272
268
|
|
|
273
|
-
simulator.
|
|
269
|
+
simulator.run_for(time_delta=100)
|