Mesa 2.3.0.dev0__py3-none-any.whl → 2.3.0rc1__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.

@@ -20,7 +20,7 @@ def SpaceAltair(model, agent_portrayal, dependencies: Optional[list[any]] = None
20
20
  def _draw_grid(space, agent_portrayal):
21
21
  def portray(g):
22
22
  all_agent_data = []
23
- for content, (x, y) in space.coord_iter():
23
+ for content, (x, y) in g.coord_iter():
24
24
  if not content:
25
25
  continue
26
26
  if not hasattr(content, "__iter__"):
@@ -35,11 +35,18 @@ def _draw_grid(space, agent_portrayal):
35
35
  return all_agent_data
36
36
 
37
37
  all_agent_data = portray(space)
38
+ invalid_tooltips = ["color", "size", "x", "y"]
39
+
38
40
  encoding_dict = {
39
41
  # no x-axis label
40
42
  "x": alt.X("x", axis=None, type="ordinal"),
41
43
  # no y-axis label
42
44
  "y": alt.Y("y", axis=None, type="ordinal"),
45
+ "tooltip": [
46
+ alt.Tooltip(key, type=alt.utils.infer_vegalite_type([value]))
47
+ for key, value in all_agent_data[0].items()
48
+ if key not in invalid_tooltips
49
+ ],
43
50
  }
44
51
  has_color = "color" in all_agent_data[0]
45
52
  if has_color:
@@ -56,5 +63,10 @@ def _draw_grid(space, agent_portrayal):
56
63
  .properties(width=280, height=280)
57
64
  # .configure_view(strokeOpacity=0) # hide grid/chart lines
58
65
  )
66
+ # This is the default value for the marker size, which auto-scales
67
+ # according to the grid area.
68
+ if not has_size:
69
+ length = min(space.width, space.height)
70
+ chart = chart.mark_point(size=30000 / length**2, filled=True)
59
71
 
60
72
  return chart
@@ -48,6 +48,9 @@ def _draw_grid(space, space_ax, agent_portrayal):
48
48
  if "color" in data:
49
49
  c.append(data["color"])
50
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 / min(g.width, g.height)) ** 2
51
54
  if len(s) > 0:
52
55
  out["s"] = s
53
56
  if len(c) > 0:
@@ -0,0 +1,4 @@
1
+ from .eventlist import Priority, SimulationEvent
2
+ from .simulator import ABMSimulator, DEVSimulator
3
+
4
+ __all__ = ["ABMSimulator", "DEVSimulator", "SimulationEvent", "Priority"]
@@ -0,0 +1,166 @@
1
+ from __future__ import annotations
2
+
3
+ import itertools
4
+ from enum import IntEnum
5
+ from heapq import heapify, heappop, heappush
6
+ from types import MethodType
7
+ from typing import Any, Callable
8
+ from weakref import WeakMethod, ref
9
+
10
+
11
+ class Priority(IntEnum):
12
+ LOW = 10
13
+ DEFAULT = 5
14
+ HIGH = 1
15
+
16
+
17
+ class SimulationEvent:
18
+ """A simulation event
19
+
20
+ the callable is wrapped using weakref, so there is no need to explicitly cancel event if e.g., an agent
21
+ is removed from the simulation.
22
+
23
+ Attributes:
24
+ time (float): The simulation time of the event
25
+ fn (Callable): The function to execute for this event
26
+ priority (Priority): The priority of the event
27
+ unique_id (int) the unique identifier of the event
28
+ function_args (list[Any]): Argument for the function
29
+ function_kwargs (Dict[str, Any]): Keyword arguments for the function
30
+
31
+ """
32
+
33
+ _ids = itertools.count()
34
+
35
+ @property
36
+ def CANCELED(self) -> bool:
37
+ return self._canceled
38
+
39
+ def __init__(
40
+ self,
41
+ time: int | float,
42
+ function: Callable,
43
+ priority: Priority = Priority.DEFAULT,
44
+ function_args: list[Any] | None = None,
45
+ function_kwargs: dict[str, Any] | None = None,
46
+ ) -> None:
47
+ super().__init__()
48
+ if not callable(function):
49
+ raise Exception()
50
+
51
+ self.time = time
52
+ self.priority = priority.value
53
+ self._canceled = False
54
+
55
+ if isinstance(function, MethodType):
56
+ function = WeakMethod(function)
57
+ else:
58
+ function = ref(function)
59
+
60
+ self.fn = function
61
+ self.unique_id = next(self._ids)
62
+ self.function_args = function_args if function_args else []
63
+ self.function_kwargs = function_kwargs if function_kwargs else {}
64
+
65
+ def execute(self):
66
+ """execute this event"""
67
+ if not self._canceled:
68
+ fn = self.fn()
69
+ if fn is not None:
70
+ fn(*self.function_args, **self.function_kwargs)
71
+
72
+ def cancel(self) -> None:
73
+ """cancel this event"""
74
+ self._canceled = True
75
+ self.fn = None
76
+ self.function_args = []
77
+ self.function_kwargs = {}
78
+
79
+ def __lt__(self, other):
80
+ # Define a total ordering for events to be used by the heapq
81
+ return (self.time, self.priority, self.unique_id) < (
82
+ other.time,
83
+ other.priority,
84
+ other.unique_id,
85
+ )
86
+
87
+
88
+ class EventList:
89
+ """An event list
90
+
91
+ This is a heap queue sorted list of events. Events are always removed from the left, so heapq is a performant and
92
+ appropriate data structure. Events are sorted based on their time stamp, their priority, and their unique_id
93
+ as a tie-breaker, guaranteeing a complete ordering.
94
+
95
+ """
96
+
97
+ def __init__(self):
98
+ self._events: list[SimulationEvent] = []
99
+ heapify(self._events)
100
+
101
+ def add_event(self, event: SimulationEvent):
102
+ """Add the event to the event list
103
+
104
+ Args:
105
+ event (SimulationEvent): The event to be added
106
+
107
+ """
108
+
109
+ heappush(self._events, event)
110
+
111
+ def peak_ahead(self, n: int = 1) -> list[SimulationEvent]:
112
+ """Look at the first n non-canceled event in the event list
113
+
114
+ Args:
115
+ n (int): The number of events to look ahead
116
+
117
+ Returns:
118
+ list[SimulationEvent]
119
+
120
+ Raises:
121
+ IndexError: If the eventlist is empty
122
+
123
+ Notes:
124
+ this method can return a list shorted then n if the number of non-canceled events on the event list
125
+ is less than n.
126
+
127
+ """
128
+ # look n events ahead
129
+ if self.is_empty():
130
+ raise IndexError("event list is empty")
131
+
132
+ peek: list[SimulationEvent] = []
133
+ for event in self._events:
134
+ if not event.CANCELED:
135
+ peek.append(event)
136
+ if len(peek) >= n:
137
+ return peek
138
+ return peek
139
+
140
+ def pop_event(self) -> SimulationEvent:
141
+ """pop the first element from the event list"""
142
+ while self._events:
143
+ event = heappop(self._events)
144
+ if not event.CANCELED:
145
+ return event
146
+ raise IndexError("Event list is empty")
147
+
148
+ def is_empty(self) -> bool:
149
+ return len(self) == 0
150
+
151
+ def __contains__(self, event: SimulationEvent) -> bool:
152
+ return event in self._events
153
+
154
+ def __len__(self) -> int:
155
+ return len(self._events)
156
+
157
+ def remove(self, event: SimulationEvent) -> None:
158
+ """remove an event from the event list"""
159
+ # we cannot simply remove items from _eventlist because this breaks
160
+ # heap structure invariant. So, we use a form of lazy deletion.
161
+ # SimEvents have a CANCELED flag that we set to True, while popping and peak_ahead
162
+ # silently ignore canceled events
163
+ event.cancel()
164
+
165
+ def clear(self):
166
+ self._events.clear()
@@ -0,0 +1,273 @@
1
+ import enum
2
+ import math
3
+
4
+ from mesa import Agent, Model
5
+ from mesa.experimental.devs.simulator import ABMSimulator
6
+ from mesa.space import SingleGrid
7
+
8
+
9
+ class EpsteinAgent(Agent):
10
+ def __init__(self, unique_id, model, vision, movement):
11
+ super().__init__(unique_id, model)
12
+ self.vision = vision
13
+ self.movement = movement
14
+
15
+
16
+ class AgentState(enum.IntEnum):
17
+ QUIESCENT = enum.auto()
18
+ ARRESTED = enum.auto()
19
+ ACTIVE = enum.auto()
20
+
21
+
22
+ class Citizen(EpsteinAgent):
23
+ """
24
+ A member of the general population, may or may not be in active rebellion.
25
+ Summary of rule: If grievance - risk > threshold, rebel.
26
+
27
+ Attributes:
28
+ unique_id: unique int
29
+ model :
30
+ hardship: Agent's 'perceived hardship (i.e., physical or economic
31
+ privation).' Exogenous, drawn from U(0,1).
32
+ regime_legitimacy: Agent's perception of regime legitimacy, equal
33
+ across agents. Exogenous.
34
+ risk_aversion: Exogenous, drawn from U(0,1).
35
+ threshold: if (grievance - (risk_aversion * arrest_probability)) >
36
+ threshold, go/remain Active
37
+ vision: number of cells in each direction (N, S, E and W) that agent
38
+ can inspect
39
+ condition: Can be "Quiescent" or "Active;" deterministic function of
40
+ greivance, perceived risk, and
41
+ grievance: deterministic function of hardship and regime_legitimacy;
42
+ how aggrieved is agent at the regime?
43
+ arrest_probability: agent's assessment of arrest probability, given
44
+ rebellion
45
+ """
46
+
47
+ def __init__(
48
+ self,
49
+ unique_id,
50
+ model,
51
+ vision,
52
+ movement,
53
+ hardship,
54
+ regime_legitimacy,
55
+ risk_aversion,
56
+ threshold,
57
+ arrest_prob_constant,
58
+ ):
59
+ """
60
+ Create a new Citizen.
61
+ Args:
62
+ unique_id: unique int
63
+ model : model instance
64
+ hardship: Agent's 'perceived hardship (i.e., physical or economic
65
+ privation).' Exogenous, drawn from U(0,1).
66
+ regime_legitimacy: Agent's perception of regime legitimacy, equal
67
+ across agents. Exogenous.
68
+ risk_aversion: Exogenous, drawn from U(0,1).
69
+ threshold: if (grievance - (risk_aversion * arrest_probability)) >
70
+ threshold, go/remain Active
71
+ vision: number of cells in each direction (N, S, E and W) that
72
+ agent can inspect. Exogenous.
73
+ """
74
+ super().__init__(unique_id, model, vision, movement)
75
+ self.hardship = hardship
76
+ self.regime_legitimacy = regime_legitimacy
77
+ self.risk_aversion = risk_aversion
78
+ self.threshold = threshold
79
+ self.condition = AgentState.QUIESCENT
80
+ self.grievance = self.hardship * (1 - self.regime_legitimacy)
81
+ self.arrest_probability = None
82
+ self.arrest_prob_constant = arrest_prob_constant
83
+
84
+ def step(self):
85
+ """
86
+ Decide whether to activate, then move if applicable.
87
+ """
88
+ self.update_neighbors()
89
+ self.update_estimated_arrest_probability()
90
+ net_risk = self.risk_aversion * self.arrest_probability
91
+ if self.grievance - net_risk > self.threshold:
92
+ self.condition = AgentState.ACTIVE
93
+ else:
94
+ self.condition = AgentState.QUIESCENT
95
+ if self.movement and self.empty_neighbors:
96
+ new_pos = self.random.choice(self.empty_neighbors)
97
+ self.model.grid.move_agent(self, new_pos)
98
+
99
+ def update_neighbors(self):
100
+ """
101
+ Look around and see who my neighbors are
102
+ """
103
+ self.neighborhood = self.model.grid.get_neighborhood(
104
+ self.pos, moore=True, radius=self.vision
105
+ )
106
+ self.neighbors = self.model.grid.get_cell_list_contents(self.neighborhood)
107
+ self.empty_neighbors = [
108
+ c for c in self.neighborhood if self.model.grid.is_cell_empty(c)
109
+ ]
110
+
111
+ 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
+ """
116
+ cops_in_vision = len([c for c in self.neighbors if isinstance(c, Cop)])
117
+ actives_in_vision = 1.0 # citizen counts herself
118
+ for c in self.neighbors:
119
+ if isinstance(c, Citizen) and c.condition == AgentState.ACTIVE:
120
+ actives_in_vision += 1
121
+ self.arrest_probability = 1 - math.exp(
122
+ -1 * self.arrest_prob_constant * (cops_in_vision / actives_in_vision)
123
+ )
124
+
125
+ def sent_to_jail(self, value):
126
+ self.model.active_agents.remove(self)
127
+ self.condition = AgentState.ARRESTED
128
+ self.model.simulator.schedule_event_relative(self.release_from_jail, value)
129
+
130
+ def release_from_jail(self):
131
+ self.model.active_agents.add(self)
132
+ self.condition = AgentState.QUIESCENT
133
+
134
+
135
+ class Cop(EpsteinAgent):
136
+ """
137
+ A cop for life. No defection.
138
+ Summary of rule: Inspect local vision and arrest a random active agent.
139
+
140
+ Attributes:
141
+ unique_id: unique int
142
+ x, y: Grid coordinates
143
+ vision: number of cells in each direction (N, S, E and W) that cop is
144
+ able to inspect
145
+ """
146
+
147
+ def __init__(self, unique_id, model, vision, movement, max_jail_term):
148
+ super().__init__(unique_id, model, vision, movement)
149
+ self.max_jail_term = max_jail_term
150
+
151
+ def step(self):
152
+ """
153
+ Inspect local vision and arrest a random active agent. Move if
154
+ applicable.
155
+ """
156
+ self.update_neighbors()
157
+ active_neighbors = []
158
+ for agent in self.neighbors:
159
+ if isinstance(agent, Citizen) and agent.condition == "Active":
160
+ active_neighbors.append(agent)
161
+ if active_neighbors:
162
+ arrestee = self.random.choice(active_neighbors)
163
+ arrestee.sent_to_jail(self.random.randint(0, self.max_jail_term))
164
+ if self.movement and self.empty_neighbors:
165
+ new_pos = self.random.choice(self.empty_neighbors)
166
+ self.model.grid.move_agent(self, new_pos)
167
+
168
+ def update_neighbors(self):
169
+ """
170
+ Look around and see who my neighbors are.
171
+ """
172
+ self.neighborhood = self.model.grid.get_neighborhood(
173
+ self.pos, moore=True, radius=self.vision
174
+ )
175
+ self.neighbors = self.model.grid.get_cell_list_contents(self.neighborhood)
176
+ self.empty_neighbors = [
177
+ c for c in self.neighborhood if self.model.grid.is_cell_empty(c)
178
+ ]
179
+
180
+
181
+ class EpsteinCivilViolence(Model):
182
+ """
183
+ Model 1 from "Modeling civil violence: An agent-based computational
184
+ approach," by Joshua Epstein.
185
+ http://www.pnas.org/content/99/suppl_3/7243.full
186
+ Attributes:
187
+ height: grid height
188
+ width: grid width
189
+ citizen_density: approximate % of cells occupied by citizens.
190
+ cop_density: approximate % of cells occupied by cops.
191
+ citizen_vision: number of cells in each direction (N, S, E and W) that
192
+ citizen can inspect
193
+ cop_vision: number of cells in each direction (N, S, E and W) that cop
194
+ can inspect
195
+ legitimacy: (L) citizens' perception of regime legitimacy, equal
196
+ across all citizens
197
+ max_jail_term: (J_max)
198
+ active_threshold: if (grievance - (risk_aversion * arrest_probability))
199
+ > threshold, citizen rebels
200
+ arrest_prob_constant: set to ensure agents make plausible arrest
201
+ probability estimates
202
+ movement: binary, whether agents try to move at step end
203
+ max_iters: model may not have a natural stopping point, so we set a
204
+ max.
205
+ """
206
+
207
+ def __init__(
208
+ self,
209
+ width=40,
210
+ height=40,
211
+ citizen_density=0.7,
212
+ cop_density=0.074,
213
+ citizen_vision=7,
214
+ cop_vision=7,
215
+ legitimacy=0.8,
216
+ max_jail_term=1000,
217
+ active_threshold=0.1,
218
+ arrest_prob_constant=2.3,
219
+ movement=True,
220
+ max_iters=1000,
221
+ seed=None,
222
+ ):
223
+ super().__init__(seed)
224
+ if cop_density + citizen_density > 1:
225
+ raise ValueError("Cop density + citizen density must be less than 1")
226
+
227
+ self.width = width
228
+ self.height = height
229
+ self.citizen_density = citizen_density
230
+ self.cop_density = cop_density
231
+
232
+ self.max_iters = max_iters
233
+
234
+ self.grid = SingleGrid(self.width, self.height, torus=True)
235
+
236
+ for _, pos in self.grid.coord_iter():
237
+ if self.random.random() < self.cop_density:
238
+ agent = Cop(
239
+ self.next_id(),
240
+ self,
241
+ cop_vision,
242
+ movement,
243
+ max_jail_term,
244
+ )
245
+ elif self.random.random() < (self.cop_density + self.citizen_density):
246
+ agent = Citizen(
247
+ self.next_id(),
248
+ self,
249
+ citizen_vision,
250
+ movement,
251
+ hardship=self.random.random(),
252
+ regime_legitimacy=legitimacy,
253
+ risk_aversion=self.random.random(),
254
+ threshold=active_threshold,
255
+ arrest_prob_constant=arrest_prob_constant,
256
+ )
257
+ else:
258
+ continue
259
+ self.grid.place_agent(agent, pos)
260
+
261
+ self.active_agents = self.agents
262
+
263
+ def step(self):
264
+ self.active_agents.shuffle(inplace=True).do("step")
265
+
266
+
267
+ if __name__ == "__main__":
268
+ model = EpsteinCivilViolence(seed=15)
269
+ simulator = ABMSimulator()
270
+
271
+ simulator.setup(model)
272
+
273
+ simulator.run(time_delta=100)