Mesa 2.4.0__py3-none-any.whl → 3.0.0__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.

Files changed (110) hide show
  1. mesa/__init__.py +3 -5
  2. mesa/agent.py +105 -92
  3. mesa/batchrunner.py +55 -31
  4. mesa/datacollection.py +10 -14
  5. mesa/examples/README.md +37 -0
  6. mesa/examples/__init__.py +21 -0
  7. mesa/examples/advanced/epstein_civil_violence/Epstein Civil Violence.ipynb +116 -0
  8. mesa/examples/advanced/epstein_civil_violence/Readme.md +34 -0
  9. mesa/examples/advanced/epstein_civil_violence/__init__.py +0 -0
  10. mesa/examples/advanced/epstein_civil_violence/agents.py +164 -0
  11. mesa/examples/advanced/epstein_civil_violence/app.py +73 -0
  12. mesa/examples/advanced/epstein_civil_violence/model.py +114 -0
  13. mesa/examples/advanced/pd_grid/Readme.md +43 -0
  14. mesa/examples/advanced/pd_grid/__init__.py +0 -0
  15. mesa/examples/advanced/pd_grid/agents.py +50 -0
  16. mesa/examples/advanced/pd_grid/analysis.ipynb +228 -0
  17. mesa/examples/advanced/pd_grid/app.py +54 -0
  18. mesa/examples/advanced/pd_grid/model.py +71 -0
  19. mesa/examples/advanced/sugarscape_g1mt/Readme.md +64 -0
  20. mesa/examples/advanced/sugarscape_g1mt/__init__.py +0 -0
  21. mesa/examples/advanced/sugarscape_g1mt/agents.py +344 -0
  22. mesa/examples/advanced/sugarscape_g1mt/app.py +62 -0
  23. mesa/examples/advanced/sugarscape_g1mt/model.py +180 -0
  24. mesa/examples/advanced/sugarscape_g1mt/sugar-map.txt +50 -0
  25. mesa/examples/advanced/sugarscape_g1mt/tests.py +69 -0
  26. mesa/examples/advanced/wolf_sheep/Readme.md +57 -0
  27. mesa/examples/advanced/wolf_sheep/__init__.py +0 -0
  28. mesa/examples/advanced/wolf_sheep/agents.py +102 -0
  29. mesa/examples/advanced/wolf_sheep/app.py +84 -0
  30. mesa/examples/advanced/wolf_sheep/model.py +137 -0
  31. mesa/examples/basic/__init__.py +0 -0
  32. mesa/examples/basic/boid_flockers/Readme.md +22 -0
  33. mesa/examples/basic/boid_flockers/__init__.py +0 -0
  34. mesa/examples/basic/boid_flockers/agents.py +71 -0
  35. mesa/examples/basic/boid_flockers/app.py +58 -0
  36. mesa/examples/basic/boid_flockers/model.py +69 -0
  37. mesa/examples/basic/boltzmann_wealth_model/Readme.md +56 -0
  38. mesa/examples/basic/boltzmann_wealth_model/__init__.py +0 -0
  39. mesa/examples/basic/boltzmann_wealth_model/agents.py +31 -0
  40. mesa/examples/basic/boltzmann_wealth_model/app.py +74 -0
  41. mesa/examples/basic/boltzmann_wealth_model/model.py +43 -0
  42. mesa/examples/basic/boltzmann_wealth_model/st_app.py +115 -0
  43. mesa/examples/basic/conways_game_of_life/Readme.md +39 -0
  44. mesa/examples/basic/conways_game_of_life/__init__.py +0 -0
  45. mesa/examples/basic/conways_game_of_life/agents.py +47 -0
  46. mesa/examples/basic/conways_game_of_life/app.py +51 -0
  47. mesa/examples/basic/conways_game_of_life/model.py +31 -0
  48. mesa/examples/basic/conways_game_of_life/st_app.py +72 -0
  49. mesa/examples/basic/schelling/Readme.md +40 -0
  50. mesa/examples/basic/schelling/__init__.py +0 -0
  51. mesa/examples/basic/schelling/agents.py +26 -0
  52. mesa/examples/basic/schelling/analysis.ipynb +205 -0
  53. mesa/examples/basic/schelling/app.py +42 -0
  54. mesa/examples/basic/schelling/model.py +59 -0
  55. mesa/examples/basic/virus_on_network/Readme.md +61 -0
  56. mesa/examples/basic/virus_on_network/__init__.py +0 -0
  57. mesa/examples/basic/virus_on_network/agents.py +69 -0
  58. mesa/examples/basic/virus_on_network/app.py +114 -0
  59. mesa/examples/basic/virus_on_network/model.py +96 -0
  60. mesa/experimental/UserParam.py +18 -7
  61. mesa/experimental/__init__.py +10 -2
  62. mesa/experimental/cell_space/__init__.py +16 -1
  63. mesa/experimental/cell_space/cell.py +93 -23
  64. mesa/experimental/cell_space/cell_agent.py +117 -21
  65. mesa/experimental/cell_space/cell_collection.py +56 -19
  66. mesa/experimental/cell_space/discrete_space.py +92 -8
  67. mesa/experimental/cell_space/grid.py +33 -9
  68. mesa/experimental/cell_space/network.py +15 -10
  69. mesa/experimental/cell_space/voronoi.py +257 -0
  70. mesa/experimental/components/altair.py +11 -2
  71. mesa/experimental/components/matplotlib.py +132 -26
  72. mesa/experimental/devs/__init__.py +2 -0
  73. mesa/experimental/devs/eventlist.py +54 -15
  74. mesa/experimental/devs/examples/epstein_civil_violence.py +69 -38
  75. mesa/experimental/devs/examples/wolf_sheep.py +42 -43
  76. mesa/experimental/devs/simulator.py +57 -16
  77. mesa/experimental/{jupyter_viz.py → solara_viz.py} +151 -99
  78. mesa/model.py +136 -78
  79. mesa/space.py +208 -148
  80. mesa/time.py +63 -80
  81. mesa/visualization/__init__.py +25 -6
  82. mesa/visualization/components/__init__.py +83 -0
  83. mesa/visualization/components/altair_components.py +188 -0
  84. mesa/visualization/components/matplotlib_components.py +175 -0
  85. mesa/visualization/mpl_space_drawing.py +593 -0
  86. mesa/visualization/solara_viz.py +458 -0
  87. mesa/visualization/user_param.py +69 -0
  88. mesa/visualization/utils.py +9 -0
  89. {mesa-2.4.0.dist-info → mesa-3.0.0.dist-info}/METADATA +62 -17
  90. mesa-3.0.0.dist-info/RECORD +95 -0
  91. mesa-3.0.0.dist-info/licenses/LICENSE +202 -0
  92. mesa-2.4.0.dist-info/licenses/LICENSE → mesa-3.0.0.dist-info/licenses/NOTICE +2 -2
  93. mesa/cookiecutter-mesa/cookiecutter.json +0 -8
  94. mesa/cookiecutter-mesa/hooks/post_gen_project.py +0 -11
  95. mesa/cookiecutter-mesa/{{cookiecutter.snake}}/README.md +0 -4
  96. mesa/cookiecutter-mesa/{{cookiecutter.snake}}/run.pytemplate +0 -3
  97. mesa/cookiecutter-mesa/{{cookiecutter.snake}}/setup.pytemplate +0 -11
  98. mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/model.pytemplate +0 -60
  99. mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/server.pytemplate +0 -36
  100. mesa/flat/__init__.py +0 -6
  101. mesa/flat/visualization.py +0 -5
  102. mesa/main.py +0 -63
  103. mesa/visualization/ModularVisualization.py +0 -1
  104. mesa/visualization/TextVisualization.py +0 -1
  105. mesa/visualization/UserParam.py +0 -1
  106. mesa/visualization/modules.py +0 -1
  107. mesa-2.4.0.dist-info/RECORD +0 -45
  108. /mesa/{cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}} → examples/advanced}/__init__.py +0 -0
  109. {mesa-2.4.0.dist-info → mesa-3.0.0.dist-info}/WHEEL +0 -0
  110. {mesa-2.4.0.dist-info → mesa-3.0.0.dist-info}/entry_points.txt +0 -0
@@ -1,5 +1,6 @@
1
+ """Altair components."""
2
+
1
3
  import contextlib
2
- from typing import Optional
3
4
 
4
5
  import solara
5
6
 
@@ -8,7 +9,15 @@ with contextlib.suppress(ImportError):
8
9
 
9
10
 
10
11
  @solara.component
11
- def SpaceAltair(model, agent_portrayal, dependencies: Optional[list[any]] = None):
12
+ def SpaceAltair(model, agent_portrayal, dependencies: list[any] | None = None):
13
+ """A component that renders a Space using Altair.
14
+
15
+ Args:
16
+ model: a model instance
17
+ agent_portrayal: agent portray specification
18
+ dependencies: optional list of dependencies (currently not used)
19
+
20
+ """
12
21
  space = getattr(model, "grid", None)
13
22
  if space is None:
14
23
  # Sometimes the space is defined as model.space instead of model.grid
@@ -1,4 +1,6 @@
1
- from typing import Optional
1
+ """Support for using matplotlib to draw spaces."""
2
+
3
+ from collections import defaultdict
2
4
 
3
5
  import networkx as nx
4
6
  import solara
@@ -6,10 +8,19 @@ from matplotlib.figure import Figure
6
8
  from matplotlib.ticker import MaxNLocator
7
9
 
8
10
  import mesa
11
+ from mesa.experimental.cell_space import VoronoiGrid
9
12
 
10
13
 
11
14
  @solara.component
12
- def SpaceMatplotlib(model, agent_portrayal, dependencies: Optional[list[any]] = None):
15
+ def SpaceMatplotlib(model, agent_portrayal, dependencies: list[any] | None = None):
16
+ """A component for rendering a space using Matplotlib.
17
+
18
+ Args:
19
+ model: a model instance
20
+ agent_portrayal: a specification of how to portray an agent.
21
+ dependencies: list of dependencies.
22
+
23
+ """
13
24
  space_fig = Figure()
14
25
  space_ax = space_fig.subplots()
15
26
  space = getattr(model, "grid", None)
@@ -20,17 +31,51 @@ def SpaceMatplotlib(model, agent_portrayal, dependencies: Optional[list[any]] =
20
31
  _draw_network_grid(space, space_ax, agent_portrayal)
21
32
  elif isinstance(space, mesa.space.ContinuousSpace):
22
33
  _draw_continuous_space(space, space_ax, agent_portrayal)
34
+ elif isinstance(space, VoronoiGrid):
35
+ _draw_voronoi(space, space_ax, agent_portrayal)
23
36
  else:
24
37
  _draw_grid(space, space_ax, agent_portrayal)
25
38
  solara.FigureMatplotlib(space_fig, format="png", dependencies=dependencies)
26
39
 
27
40
 
41
+ # matplotlib scatter does not allow for multiple shapes in one call
42
+ def _split_and_scatter(portray_data, space_ax):
43
+ grouped_data = defaultdict(lambda: {"x": [], "y": [], "s": [], "c": []})
44
+
45
+ # Extract data from the dictionary
46
+ x = portray_data["x"]
47
+ y = portray_data["y"]
48
+ s = portray_data["s"]
49
+ c = portray_data["c"]
50
+ m = portray_data["m"]
51
+
52
+ if not (len(x) == len(y) == len(s) == len(c) == len(m)):
53
+ raise ValueError(
54
+ "Length mismatch in portrayal data lists: "
55
+ f"x: {len(x)}, y: {len(y)}, size: {len(s)}, "
56
+ f"color: {len(c)}, marker: {len(m)}"
57
+ )
58
+
59
+ # Group the data by marker
60
+ for i in range(len(x)):
61
+ marker = m[i]
62
+ grouped_data[marker]["x"].append(x[i])
63
+ grouped_data[marker]["y"].append(y[i])
64
+ grouped_data[marker]["s"].append(s[i])
65
+ grouped_data[marker]["c"].append(c[i])
66
+
67
+ # Plot each group with the same marker
68
+ for marker, data in grouped_data.items():
69
+ space_ax.scatter(data["x"], data["y"], s=data["s"], c=data["c"], marker=marker)
70
+
71
+
28
72
  def _draw_grid(space, space_ax, agent_portrayal):
29
73
  def portray(g):
30
74
  x = []
31
75
  y = []
32
76
  s = [] # size
33
77
  c = [] # color
78
+ m = [] # shape
34
79
  for i in range(g.width):
35
80
  for j in range(g.height):
36
81
  content = g._grid[i][j]
@@ -43,23 +88,23 @@ def _draw_grid(space, space_ax, agent_portrayal):
43
88
  data = agent_portrayal(agent)
44
89
  x.append(i)
45
90
  y.append(j)
46
- if "size" in data:
47
- s.append(data["size"])
48
- if "color" in data:
49
- c.append(data["color"])
50
- out = {"x": x, "y": y}
51
- # This is the default value for the marker size, which auto-scales
52
- # according to the grid area.
53
- out["s"] = (180 / max(g.width, g.height)) ** 2
54
- if len(s) > 0:
55
- out["s"] = s
56
- if len(c) > 0:
57
- out["c"] = c
91
+
92
+ # This is the default value for the marker size, which auto-scales
93
+ # according to the grid area.
94
+ default_size = (180 / max(g.width, g.height)) ** 2
95
+ # establishing a default prevents misalignment if some agents are not given size, color, etc.
96
+ size = data.get("size", default_size)
97
+ s.append(size)
98
+ color = data.get("color", "tab:blue")
99
+ c.append(color)
100
+ mark = data.get("shape", "o")
101
+ m.append(mark)
102
+ out = {"x": x, "y": y, "s": s, "c": c, "m": m}
58
103
  return out
59
104
 
60
105
  space_ax.set_xlim(-1, space.width)
61
106
  space_ax.set_ylim(-1, space.height)
62
- space_ax.scatter(**portray(space))
107
+ _split_and_scatter(portray(space), space_ax)
63
108
 
64
109
 
65
110
  def _draw_network_grid(space, space_ax, agent_portrayal):
@@ -79,20 +124,23 @@ def _draw_continuous_space(space, space_ax, agent_portrayal):
79
124
  y = []
80
125
  s = [] # size
81
126
  c = [] # color
127
+ m = [] # shape
82
128
  for agent in space._agent_to_index:
83
129
  data = agent_portrayal(agent)
84
130
  _x, _y = agent.pos
85
131
  x.append(_x)
86
132
  y.append(_y)
87
- if "size" in data:
88
- s.append(data["size"])
89
- if "color" in data:
90
- c.append(data["color"])
91
- out = {"x": x, "y": y}
92
- if len(s) > 0:
93
- out["s"] = s
94
- if len(c) > 0:
95
- out["c"] = c
133
+
134
+ # This is matplotlib's default marker size
135
+ default_size = 20
136
+ # establishing a default prevents misalignment if some agents are not given size, color, etc.
137
+ size = data.get("size", default_size)
138
+ s.append(size)
139
+ color = data.get("color", "tab:blue")
140
+ c.append(color)
141
+ mark = data.get("shape", "o")
142
+ m.append(mark)
143
+ out = {"x": x, "y": y, "s": s, "c": c, "m": m}
96
144
  return out
97
145
 
98
146
  # Determine border style based on space.torus
@@ -112,11 +160,69 @@ def _draw_continuous_space(space, space_ax, agent_portrayal):
112
160
  space_ax.set_ylim(space.y_min - y_padding, space.y_max + y_padding)
113
161
 
114
162
  # Portray and scatter the agents in the space
163
+ _split_and_scatter(portray(space), space_ax)
164
+
165
+
166
+ def _draw_voronoi(space, space_ax, agent_portrayal):
167
+ def portray(g):
168
+ x = []
169
+ y = []
170
+ s = [] # size
171
+ c = [] # color
172
+
173
+ for cell in g.all_cells:
174
+ for agent in cell.agents:
175
+ data = agent_portrayal(agent)
176
+ x.append(cell.coordinate[0])
177
+ y.append(cell.coordinate[1])
178
+ if "size" in data:
179
+ s.append(data["size"])
180
+ if "color" in data:
181
+ c.append(data["color"])
182
+ out = {"x": x, "y": y}
183
+ # This is the default value for the marker size, which auto-scales
184
+ # according to the grid area.
185
+ out["s"] = s
186
+ if len(c) > 0:
187
+ out["c"] = c
188
+
189
+ return out
190
+
191
+ x_list = [i[0] for i in space.centroids_coordinates]
192
+ y_list = [i[1] for i in space.centroids_coordinates]
193
+ x_max = max(x_list)
194
+ x_min = min(x_list)
195
+ y_max = max(y_list)
196
+ y_min = min(y_list)
197
+
198
+ width = x_max - x_min
199
+ x_padding = width / 20
200
+ height = y_max - y_min
201
+ y_padding = height / 20
202
+ space_ax.set_xlim(x_min - x_padding, x_max + x_padding)
203
+ space_ax.set_ylim(y_min - y_padding, y_max + y_padding)
115
204
  space_ax.scatter(**portray(space))
116
205
 
206
+ for cell in space.all_cells:
207
+ polygon = cell.properties["polygon"]
208
+ space_ax.fill(
209
+ *zip(*polygon),
210
+ alpha=min(1, cell.properties[space.cell_coloring_property]),
211
+ c="red",
212
+ ) # Plot filled polygon
213
+ space_ax.plot(*zip(*polygon), color="black") # Plot polygon edges in red
214
+
117
215
 
118
216
  @solara.component
119
- def PlotMatplotlib(model, measure, dependencies: Optional[list[any]] = None):
217
+ def PlotMatplotlib(model, measure, dependencies: list[any] | None = None):
218
+ """A solara component for creating a matplotlib figure.
219
+
220
+ Args:
221
+ model: Model instance
222
+ measure: measure to plot
223
+ dependencies: list of additional dependencies
224
+
225
+ """
120
226
  fig = Figure()
121
227
  ax = fig.subplots()
122
228
  df = model.datacollector.get_model_vars_dataframe()
@@ -127,7 +233,7 @@ def PlotMatplotlib(model, measure, dependencies: Optional[list[any]] = None):
127
233
  for m, color in measure.items():
128
234
  ax.plot(df.loc[:, m], label=m, color=color)
129
235
  fig.legend()
130
- elif isinstance(measure, (list, tuple)):
236
+ elif isinstance(measure, list | tuple):
131
237
  for m in measure:
132
238
  ax.plot(df.loc[:, m], label=m)
133
239
  fig.legend()
@@ -1,3 +1,5 @@
1
+ """Support for event scheduling."""
2
+
1
3
  from .eventlist import Priority, SimulationEvent
2
4
  from .simulator import ABMSimulator, DEVSimulator
3
5
 
@@ -1,23 +1,28 @@
1
+ """Eventlist which is at the core of event scheduling."""
2
+
1
3
  from __future__ import annotations
2
4
 
3
5
  import itertools
6
+ from collections.abc import Callable
4
7
  from enum import IntEnum
5
8
  from heapq import heapify, heappop, heappush
6
9
  from types import MethodType
7
- from typing import Any, Callable
10
+ from typing import Any
8
11
  from weakref import WeakMethod, ref
9
12
 
10
13
 
11
14
  class Priority(IntEnum):
15
+ """Enumeration of priority levels."""
16
+
12
17
  LOW = 10
13
18
  DEFAULT = 5
14
19
  HIGH = 1
15
20
 
16
21
 
17
22
  class SimulationEvent:
18
- """A simulation event
23
+ """A simulation event.
19
24
 
20
- the callable is wrapped using weakref, so there is no need to explicitly cancel event if e.g., an agent
25
+ The callable is wrapped using weakref, so there is no need to explicitly cancel event if e.g., an agent
21
26
  is removed from the simulation.
22
27
 
23
28
  Attributes:
@@ -28,12 +33,18 @@ class SimulationEvent:
28
33
  function_args (list[Any]): Argument for the function
29
34
  function_kwargs (Dict[str, Any]): Keyword arguments for the function
30
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
+
31
42
  """
32
43
 
33
44
  _ids = itertools.count()
34
45
 
35
46
  @property
36
- def CANCELED(self) -> bool:
47
+ def CANCELED(self) -> bool: # noqa: D102
37
48
  return self._canceled
38
49
 
39
50
  def __init__(
@@ -44,6 +55,15 @@ class SimulationEvent:
44
55
  function_args: list[Any] | None = None,
45
56
  function_kwargs: dict[str, Any] | None = None,
46
57
  ) -> None:
58
+ """Initialize a simulation event.
59
+
60
+ Args:
61
+ time: the instant of time of the simulation event
62
+ function: the callable to invoke
63
+ priority: the priority of the event
64
+ function_args: arguments for callable
65
+ function_kwargs: keyword arguments for the callable
66
+ """
47
67
  super().__init__()
48
68
  if not callable(function):
49
69
  raise Exception()
@@ -63,20 +83,20 @@ class SimulationEvent:
63
83
  self.function_kwargs = function_kwargs if function_kwargs else {}
64
84
 
65
85
  def execute(self):
66
- """execute this event"""
86
+ """Execute this event."""
67
87
  if not self._canceled:
68
88
  fn = self.fn()
69
89
  if fn is not None:
70
90
  fn(*self.function_args, **self.function_kwargs)
71
91
 
72
92
  def cancel(self) -> None:
73
- """cancel this event"""
93
+ """Cancel this event."""
74
94
  self._canceled = True
75
95
  self.fn = None
76
96
  self.function_args = []
77
97
  self.function_kwargs = {}
78
98
 
79
- def __lt__(self, other):
99
+ def __lt__(self, other): # noqa
80
100
  # Define a total ordering for events to be used by the heapq
81
101
  return (self.time, self.priority, self.unique_id) < (
82
102
  other.time,
@@ -86,30 +106,31 @@ class SimulationEvent:
86
106
 
87
107
 
88
108
  class EventList:
89
- """An event list
109
+ """An event list.
90
110
 
91
111
  This is a heap queue sorted list of events. Events are always removed from the left, so heapq is a performant and
92
112
  appropriate data structure. Events are sorted based on their time stamp, their priority, and their unique_id
93
113
  as a tie-breaker, guaranteeing a complete ordering.
94
114
 
115
+
95
116
  """
96
117
 
97
118
  def __init__(self):
119
+ """Initialize an event list."""
98
120
  self._events: list[SimulationEvent] = []
99
121
  heapify(self._events)
100
122
 
101
123
  def add_event(self, event: SimulationEvent):
102
- """Add the event to the event list
124
+ """Add the event to the event list.
103
125
 
104
126
  Args:
105
127
  event (SimulationEvent): The event to be added
106
128
 
107
129
  """
108
-
109
130
  heappush(self._events, event)
110
131
 
111
132
  def peak_ahead(self, n: int = 1) -> list[SimulationEvent]:
112
- """Look at the first n non-canceled event in the event list
133
+ """Look at the first n non-canceled event in the event list.
113
134
 
114
135
  Args:
115
136
  n (int): The number of events to look ahead
@@ -138,7 +159,7 @@ class EventList:
138
159
  return peek
139
160
 
140
161
  def pop_event(self) -> SimulationEvent:
141
- """pop the first element from the event list"""
162
+ """Pop the first element from the event list."""
142
163
  while self._events:
143
164
  event = heappop(self._events)
144
165
  if not event.CANCELED:
@@ -146,16 +167,33 @@ class EventList:
146
167
  raise IndexError("Event list is empty")
147
168
 
148
169
  def is_empty(self) -> bool:
170
+ """Return whether the event list is empty."""
149
171
  return len(self) == 0
150
172
 
151
- def __contains__(self, event: SimulationEvent) -> bool:
173
+ def __contains__(self, event: SimulationEvent) -> bool: # noqa
152
174
  return event in self._events
153
175
 
154
- def __len__(self) -> int:
176
+ def __len__(self) -> int: # noqa
155
177
  return len(self._events)
156
178
 
179
+ def __repr__(self) -> str:
180
+ """Return a string representation of the event list."""
181
+ events_str = ", ".join(
182
+ [
183
+ f"Event(time={e.time}, priority={e.priority}, id={e.unique_id})"
184
+ for e in self._events
185
+ if not e.CANCELED
186
+ ]
187
+ )
188
+ return f"EventList([{events_str}])"
189
+
157
190
  def remove(self, event: SimulationEvent) -> None:
158
- """remove an event from the event list"""
191
+ """Remove an event from the event list.
192
+
193
+ Args:
194
+ event (SimulationEvent): The event to be removed
195
+
196
+ """
159
197
  # we cannot simply remove items from _eventlist because this breaks
160
198
  # heap structure invariant. So, we use a form of lazy deletion.
161
199
  # SimEvents have a CANCELED flag that we set to True, while popping and peak_ahead
@@ -163,4 +201,5 @@ class EventList:
163
201
  event.cancel()
164
202
 
165
203
  def clear(self):
204
+ """Clear the event list."""
166
205
  self._events.clear()
@@ -1,3 +1,5 @@
1
+ """Epstein civil violence example using ABMSimulator."""
2
+
1
3
  import enum
2
4
  import math
3
5
 
@@ -7,21 +9,32 @@ from mesa.space import SingleGrid
7
9
 
8
10
 
9
11
  class EpsteinAgent(Agent):
10
- def __init__(self, unique_id, model, vision, movement):
11
- super().__init__(unique_id, model)
12
+ """Epstein Agent."""
13
+
14
+ def __init__(self, model, vision, movement):
15
+ """Initialize the agent.
16
+
17
+ Args:
18
+ model: a model instance
19
+ vision: size of neighborhood
20
+ movement: boolean whether agent can move or not
21
+ """
22
+ super().__init__(model)
12
23
  self.vision = vision
13
24
  self.movement = movement
14
25
 
15
26
 
16
27
  class AgentState(enum.IntEnum):
28
+ """Agent states."""
29
+
17
30
  QUIESCENT = enum.auto()
18
31
  ARRESTED = enum.auto()
19
32
  ACTIVE = enum.auto()
20
33
 
21
34
 
22
35
  class Citizen(EpsteinAgent):
23
- """
24
- A member of the general population, may or may not be in active rebellion.
36
+ """A member of the general population, may or may not be in active rebellion.
37
+
25
38
  Summary of rule: If grievance - risk > threshold, rebel.
26
39
 
27
40
  Attributes:
@@ -46,7 +59,6 @@ class Citizen(EpsteinAgent):
46
59
 
47
60
  def __init__(
48
61
  self,
49
- unique_id,
50
62
  model,
51
63
  vision,
52
64
  movement,
@@ -56,11 +68,13 @@ class Citizen(EpsteinAgent):
56
68
  threshold,
57
69
  arrest_prob_constant,
58
70
  ):
59
- """
60
- Create a new Citizen.
71
+ """Create a new Citizen.
72
+
61
73
  Args:
62
- unique_id: unique int
63
74
  model : model instance
75
+ vision: number of cells in each direction (N, S, E and W) that
76
+ agent can inspect. Exogenous.
77
+ movement: whether agent can move or not
64
78
  hardship: Agent's 'perceived hardship (i.e., physical or economic
65
79
  privation).' Exogenous, drawn from U(0,1).
66
80
  regime_legitimacy: Agent's perception of regime legitimacy, equal
@@ -68,10 +82,10 @@ class Citizen(EpsteinAgent):
68
82
  risk_aversion: Exogenous, drawn from U(0,1).
69
83
  threshold: if (grievance - (risk_aversion * arrest_probability)) >
70
84
  threshold, go/remain Active
71
- vision: number of cells in each direction (N, S, E and W) that
72
- agent can inspect. Exogenous.
85
+ arrest_prob_constant : agent's assessment of arrest probability
86
+
73
87
  """
74
- super().__init__(unique_id, model, vision, movement)
88
+ super().__init__(model, vision, movement)
75
89
  self.hardship = hardship
76
90
  self.regime_legitimacy = regime_legitimacy
77
91
  self.risk_aversion = risk_aversion
@@ -82,9 +96,7 @@ class Citizen(EpsteinAgent):
82
96
  self.arrest_prob_constant = arrest_prob_constant
83
97
 
84
98
  def step(self):
85
- """
86
- Decide whether to activate, then move if applicable.
87
- """
99
+ """Decide whether to activate, then move if applicable."""
88
100
  self.update_neighbors()
89
101
  self.update_estimated_arrest_probability()
90
102
  net_risk = self.risk_aversion * self.arrest_probability
@@ -97,9 +109,7 @@ class Citizen(EpsteinAgent):
97
109
  self.model.grid.move_agent(self, new_pos)
98
110
 
99
111
  def update_neighbors(self):
100
- """
101
- Look around and see who my neighbors are
102
- """
112
+ """Look around and see who my neighbors are."""
103
113
  self.neighborhood = self.model.grid.get_neighborhood(
104
114
  self.pos, moore=True, radius=self.vision
105
115
  )
@@ -109,10 +119,7 @@ class Citizen(EpsteinAgent):
109
119
  ]
110
120
 
111
121
  def update_estimated_arrest_probability(self):
112
- """
113
- Based on the ratio of cops to actives in my neighborhood, estimate the
114
- p(Arrest | I go active).
115
- """
122
+ """Based on the ratio of cops to actives in my neighborhood, estimate the p(Arrest | I go active)."""
116
123
  cops_in_vision = len([c for c in self.neighbors if isinstance(c, Cop)])
117
124
  actives_in_vision = 1.0 # citizen counts herself
118
125
  for c in self.neighbors:
@@ -123,18 +130,25 @@ class Citizen(EpsteinAgent):
123
130
  )
124
131
 
125
132
  def sent_to_jail(self, value):
133
+ """Sent agent to jail.
134
+
135
+ Args:
136
+ value: duration of jail sentence
137
+
138
+ """
126
139
  self.model.active_agents.remove(self)
127
140
  self.condition = AgentState.ARRESTED
128
141
  self.model.simulator.schedule_event_relative(self.release_from_jail, value)
129
142
 
130
143
  def release_from_jail(self):
144
+ """Release agent from jail."""
131
145
  self.model.active_agents.add(self)
132
146
  self.condition = AgentState.QUIESCENT
133
147
 
134
148
 
135
149
  class Cop(EpsteinAgent):
136
- """
137
- A cop for life. No defection.
150
+ """A cop for life. No defection.
151
+
138
152
  Summary of rule: Inspect local vision and arrest a random active agent.
139
153
 
140
154
  Attributes:
@@ -144,15 +158,20 @@ class Cop(EpsteinAgent):
144
158
  able to inspect
145
159
  """
146
160
 
147
- def __init__(self, unique_id, model, vision, movement, max_jail_term):
148
- super().__init__(unique_id, model, vision, movement)
161
+ def __init__(self, model, vision, movement, max_jail_term):
162
+ """Initialize a Cop agent.
163
+
164
+ Args:
165
+ model: a model instance
166
+ vision: size of neighborhood
167
+ movement: whether agent can move or not
168
+ max_jail_term: maximum jail sentence
169
+ """
170
+ super().__init__(model, vision, movement)
149
171
  self.max_jail_term = max_jail_term
150
172
 
151
173
  def step(self):
152
- """
153
- Inspect local vision and arrest a random active agent. Move if
154
- applicable.
155
- """
174
+ """Inspect local vision and arrest a random active agent. Move if applicable."""
156
175
  self.update_neighbors()
157
176
  active_neighbors = []
158
177
  for agent in self.neighbors:
@@ -166,9 +185,7 @@ class Cop(EpsteinAgent):
166
185
  self.model.grid.move_agent(self, new_pos)
167
186
 
168
187
  def update_neighbors(self):
169
- """
170
- Look around and see who my neighbors are.
171
- """
188
+ """Look around and see who my neighbors are."""
172
189
  self.neighborhood = self.model.grid.get_neighborhood(
173
190
  self.pos, moore=True, radius=self.vision
174
191
  )
@@ -179,9 +196,8 @@ class Cop(EpsteinAgent):
179
196
 
180
197
 
181
198
  class EpsteinCivilViolence(Model):
182
- """
183
- Model 1 from "Modeling civil violence: An agent-based computational
184
- approach," by Joshua Epstein.
199
+ """Model 1 from "Modeling civil violence: An agent-based computational approach," by Joshua Epstein.
200
+
185
201
  http://www.pnas.org/content/99/suppl_3/7243.full
186
202
  Attributes:
187
203
  height: grid height
@@ -220,6 +236,23 @@ class EpsteinCivilViolence(Model):
220
236
  max_iters=1000,
221
237
  seed=None,
222
238
  ):
239
+ """Initialize the Eppstein civil violence model.
240
+
241
+ Args:
242
+ width: the width of the grid
243
+ height: the height of the grid
244
+ citizen_density: density of citizens
245
+ cop_density: density of cops
246
+ citizen_vision: size of citizen vision
247
+ cop_vision: size of cop vision
248
+ legitimacy: perceived legitimacy
249
+ max_jail_term: maximum jail term
250
+ active_threshold: threshold for citizen to become active
251
+ arrest_prob_constant: arrest probability
252
+ movement: allow agent movement or not
253
+ max_iters: number of iterations
254
+ seed: seed for random number generator
255
+ """
223
256
  super().__init__(seed)
224
257
  if cop_density + citizen_density > 1:
225
258
  raise ValueError("Cop density + citizen density must be less than 1")
@@ -236,7 +269,6 @@ class EpsteinCivilViolence(Model):
236
269
  for _, pos in self.grid.coord_iter():
237
270
  if self.random.random() < self.cop_density:
238
271
  agent = Cop(
239
- self.next_id(),
240
272
  self,
241
273
  cop_vision,
242
274
  movement,
@@ -244,7 +276,6 @@ class EpsteinCivilViolence(Model):
244
276
  )
245
277
  elif self.random.random() < (self.cop_density + self.citizen_density):
246
278
  agent = Citizen(
247
- self.next_id(),
248
279
  self,
249
280
  citizen_vision,
250
281
  movement,
@@ -271,4 +302,4 @@ if __name__ == "__main__":
271
302
 
272
303
  simulator.setup(model)
273
304
 
274
- simulator.run(time_delta=100)
305
+ simulator.run_for(time_delta=100)