Mesa 2.3.0.dev0__py3-none-any.whl → 2.3.1__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 +2 -16
- mesa/datacollection.py +12 -6
- mesa/experimental/__init__.py +4 -0
- mesa/experimental/cell_space/__init__.py +23 -0
- mesa/experimental/cell_space/cell.py +152 -0
- mesa/experimental/cell_space/cell_agent.py +37 -0
- mesa/experimental/cell_space/cell_collection.py +81 -0
- mesa/experimental/cell_space/discrete_space.py +64 -0
- mesa/experimental/cell_space/grid.py +204 -0
- mesa/experimental/cell_space/network.py +40 -0
- mesa/experimental/components/altair.py +13 -1
- mesa/experimental/components/matplotlib.py +3 -0
- mesa/experimental/devs/__init__.py +4 -0
- mesa/experimental/devs/eventlist.py +166 -0
- mesa/experimental/devs/examples/epstein_civil_violence.py +273 -0
- mesa/experimental/devs/examples/wolf_sheep.py +250 -0
- mesa/experimental/devs/simulator.py +293 -0
- mesa/experimental/jupyter_viz.py +51 -31
- mesa/model.py +2 -4
- mesa/space.py +27 -13
- mesa/time.py +3 -110
- {mesa-2.3.0.dev0.dist-info → mesa-2.3.1.dist-info}/METADATA +4 -3
- mesa-2.3.1.dist-info/RECORD +45 -0
- {mesa-2.3.0.dev0.dist-info → mesa-2.3.1.dist-info}/WHEEL +1 -1
- mesa-2.3.0.dev0.dist-info/RECORD +0 -33
- {mesa-2.3.0.dev0.dist-info → mesa-2.3.1.dist-info}/entry_points.txt +0 -0
- {mesa-2.3.0.dev0.dist-info → mesa-2.3.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from random import Random
|
|
2
|
+
from typing import Any, Optional
|
|
3
|
+
|
|
4
|
+
from mesa.experimental.cell_space.cell import Cell
|
|
5
|
+
from mesa.experimental.cell_space.discrete_space import DiscreteSpace
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Network(DiscreteSpace):
|
|
9
|
+
"""A networked discrete space"""
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
G: Any, # noqa: N803
|
|
14
|
+
capacity: Optional[int] = None,
|
|
15
|
+
random: Optional[Random] = None,
|
|
16
|
+
cell_klass: type[Cell] = Cell,
|
|
17
|
+
) -> None:
|
|
18
|
+
"""A Networked grid
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
G: a NetworkX Graph instance.
|
|
22
|
+
capacity (int) : the capacity of the cell
|
|
23
|
+
random (Random):
|
|
24
|
+
CellKlass (type[Cell]): The base Cell class to use in the Network
|
|
25
|
+
|
|
26
|
+
"""
|
|
27
|
+
super().__init__(capacity=capacity, random=random, cell_klass=cell_klass)
|
|
28
|
+
self.G = G
|
|
29
|
+
|
|
30
|
+
for node_id in self.G.nodes:
|
|
31
|
+
self._cells[node_id] = self.cell_klass(
|
|
32
|
+
node_id, capacity, random=self.random
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
for cell in self.all_cells:
|
|
36
|
+
self._connect_single_cell(cell)
|
|
37
|
+
|
|
38
|
+
def _connect_single_cell(self, cell):
|
|
39
|
+
for node_id in self.G.neighbors(cell.coordinate):
|
|
40
|
+
cell.connect(self._cells[node_id])
|
|
@@ -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
|
|
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,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)
|