Skip to content

Commit aaf9026

Browse files
Updates to Epstein example (#2429)
* rework of epstein * shift to von neuman grid * update of perceived risk with round operator * introduction of enum for state * cleanup of statistics gathering to use enum * update of visualization code to be consistent with what is currently supported when drawing new_style discrete grids * further cleaning * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * cleanup * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * shift to dict for colors * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent e525de3 commit aaf9026

File tree

3 files changed

+90
-127
lines changed

3 files changed

+90
-127
lines changed

mesa/examples/advanced/epstein_civil_violence/agents.py

Lines changed: 44 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,29 @@
11
import math
2+
from enum import Enum
23

34
import mesa
45

56

7+
class CitizenState(Enum):
8+
ACTIVE = 1
9+
QUIET = 2
10+
ARRESTED = 3
11+
12+
613
class EpsteinAgent(mesa.experimental.cell_space.CellAgent):
714
def update_neighbors(self):
815
"""
916
Look around and see who my neighbors are
1017
"""
1118
self.neighborhood = self.cell.get_neighborhood(radius=self.vision)
12-
1319
self.neighbors = self.neighborhood.agents
1420
self.empty_neighbors = [c for c in self.neighborhood if c.is_empty]
1521

22+
def move(self):
23+
if self.model.movement and self.empty_neighbors:
24+
new_pos = self.random.choice(self.empty_neighbors)
25+
self.move_to(new_pos)
26+
1627

1728
class Citizen(EpsteinAgent):
1829
"""
@@ -38,13 +49,7 @@ class Citizen(EpsteinAgent):
3849
"""
3950

4051
def __init__(
41-
self,
42-
model,
43-
hardship,
44-
regime_legitimacy,
45-
risk_aversion,
46-
threshold,
47-
vision,
52+
self, model, regime_legitimacy, threshold, vision, arrest_prob_constant
4853
):
4954
"""
5055
Create a new Citizen.
@@ -62,16 +67,21 @@ def __init__(
6267
model: model instance
6368
"""
6469
super().__init__(model)
65-
self.hardship = hardship
70+
self.hardship = self.random.random()
71+
self.risk_aversion = self.random.random()
6672
self.regime_legitimacy = regime_legitimacy
67-
self.risk_aversion = risk_aversion
6873
self.threshold = threshold
69-
self.condition = "Quiescent"
74+
self.state = CitizenState.QUIET
7075
self.vision = vision
7176
self.jail_sentence = 0
7277
self.grievance = self.hardship * (1 - self.regime_legitimacy)
78+
self.arrest_prob_constant = arrest_prob_constant
7379
self.arrest_probability = None
7480

81+
self.neighborhood = []
82+
self.neighbors = []
83+
self.empty_neighbors = []
84+
7585
def step(self):
7686
"""
7787
Decide whether to activate, then move if applicable.
@@ -81,32 +91,33 @@ def step(self):
8191
return # no other changes or movements if agent is in jail.
8292
self.update_neighbors()
8393
self.update_estimated_arrest_probability()
94+
8495
net_risk = self.risk_aversion * self.arrest_probability
85-
if self.grievance - net_risk > self.threshold:
86-
self.condition = "Active"
96+
if (self.grievance - net_risk) > self.threshold:
97+
self.state = CitizenState.ACTIVE
8798
else:
88-
self.condition = "Quiescent"
99+
self.state = CitizenState.QUIET
89100

90-
if self.model.movement and self.empty_neighbors:
91-
new_cell = self.random.choice(self.empty_neighbors)
92-
self.move_to(new_cell)
101+
self.move()
93102

94103
def update_estimated_arrest_probability(self):
95104
"""
96105
Based on the ratio of cops to actives in my neighborhood, estimate the
97106
p(Arrest | I go active).
98107
"""
99-
cops_in_vision = len([c for c in self.neighbors if isinstance(c, Cop)])
100-
actives_in_vision = 1.0 # citizen counts herself
101-
for c in self.neighbors:
102-
if (
103-
isinstance(c, Citizen)
104-
and c.condition == "Active"
105-
and c.jail_sentence == 0
106-
):
108+
cops_in_vision = 0
109+
actives_in_vision = 1 # citizen counts herself
110+
for neighbor in self.neighbors:
111+
if isinstance(neighbor, Cop):
112+
cops_in_vision += 1
113+
elif neighbor.state == CitizenState.ACTIVE:
107114
actives_in_vision += 1
115+
116+
# there is a body of literature on this equation
117+
# the round is not in the pnas paper but without it, its impossible to replicate
118+
# the dynamics shown there.
108119
self.arrest_probability = 1 - math.exp(
109-
-1 * self.model.arrest_prob_constant * (cops_in_vision / actives_in_vision)
120+
-1 * self.arrest_prob_constant * round(cops_in_vision / actives_in_vision)
110121
)
111122

112123

@@ -122,7 +133,7 @@ class Cop(EpsteinAgent):
122133
able to inspect
123134
"""
124135

125-
def __init__(self, model, vision):
136+
def __init__(self, model, vision, max_jail_term):
126137
"""
127138
Create a new Cop.
128139
Args:
@@ -133,6 +144,7 @@ def __init__(self, model, vision):
133144
"""
134145
super().__init__(model)
135146
self.vision = vision
147+
self.max_jail_term = max_jail_term
136148

137149
def step(self):
138150
"""
@@ -142,17 +154,11 @@ def step(self):
142154
self.update_neighbors()
143155
active_neighbors = []
144156
for agent in self.neighbors:
145-
if (
146-
isinstance(agent, Citizen)
147-
and agent.condition == "Active"
148-
and agent.jail_sentence == 0
149-
):
157+
if isinstance(agent, Citizen) and agent.state == CitizenState.ACTIVE:
150158
active_neighbors.append(agent)
151159
if active_neighbors:
152160
arrestee = self.random.choice(active_neighbors)
153-
sentence = self.random.randint(0, self.model.max_jail_term)
154-
arrestee.jail_sentence = sentence
155-
arrestee.condition = "Quiescent"
156-
if self.model.movement and self.empty_neighbors:
157-
new_pos = self.random.choice(self.empty_neighbors)
158-
self.move_to(new_pos)
161+
arrestee.jail_sentence = self.random.randint(0, self.max_jail_term)
162+
arrestee.state = CitizenState.ARRESTED
163+
164+
self.move()

mesa/examples/advanced/epstein_civil_violence/app.py

Lines changed: 13 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
from mesa.examples.advanced.epstein_civil_violence.agents import Citizen, Cop
1+
from mesa.examples.advanced.epstein_civil_violence.agents import (
2+
Citizen,
3+
CitizenState,
4+
Cop,
5+
)
26
from mesa.examples.advanced.epstein_civil_violence.model import EpsteinCivilViolence
37
from mesa.visualization import (
48
Slider,
@@ -8,10 +12,12 @@
812
)
913

1014
COP_COLOR = "#000000"
11-
AGENT_QUIET_COLOR = "#648FFF"
12-
AGENT_REBEL_COLOR = "#FE6100"
13-
JAIL_COLOR = "#808080"
14-
JAIL_SHAPE = "rect"
15+
16+
agent_colors = {
17+
CitizenState.ACTIVE: "#FE6100",
18+
CitizenState.QUIET: "#648FFF",
19+
CitizenState.ARRESTED: "#808080",
20+
}
1521

1622

1723
def citizen_cop_portrayal(agent):
@@ -20,29 +26,12 @@ def citizen_cop_portrayal(agent):
2026

2127
portrayal = {
2228
"size": 25,
23-
"shape": "s", # square marker
2429
}
2530

2631
if isinstance(agent, Citizen):
27-
color = (
28-
AGENT_QUIET_COLOR if agent.condition == "Quiescent" else AGENT_REBEL_COLOR
29-
)
30-
color = JAIL_COLOR if agent.jail_sentence else color
31-
shape = JAIL_SHAPE if agent.jail_sentence else "circle"
32-
portrayal["color"] = color
33-
portrayal["shape"] = shape
34-
if shape == "s":
35-
portrayal["w"] = 0.9
36-
portrayal["h"] = 0.9
37-
else:
38-
portrayal["r"] = 0.5
39-
portrayal["filled"] = False
40-
portrayal["layer"] = 0
41-
32+
portrayal["color"] = agent_colors[agent.state]
4233
elif isinstance(agent, Cop):
4334
portrayal["color"] = COP_COLOR
44-
portrayal["r"] = 0.9
45-
portrayal["layer"] = 1
4635

4736
return portrayal
4837

@@ -59,7 +48,7 @@ def citizen_cop_portrayal(agent):
5948
}
6049

6150
space_component = make_space_matplotlib(citizen_cop_portrayal)
62-
chart_component = make_plot_measure(["Quiescent", "Active", "Jailed"])
51+
chart_component = make_plot_measure([state.name.lower() for state in CitizenState])
6352

6453
epstein_model = EpsteinCivilViolence()
6554

Lines changed: 33 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
import mesa
2-
from mesa.examples.advanced.epstein_civil_violence.agents import Citizen, Cop
2+
from mesa.examples.advanced.epstein_civil_violence.agents import (
3+
Citizen,
4+
CitizenState,
5+
Cop,
6+
)
37

48

59
class EpsteinCivilViolence(mesa.Model):
610
"""
711
Model 1 from "Modeling civil violence: An agent-based computational
812
approach," by Joshua Epstein.
913
http://www.pnas.org/content/99/suppl_3/7243.full
10-
Attributes:
14+
15+
Args:
1116
height: grid height
1217
width: grid width
1318
citizen_density: approximate % of cells occupied by citizens.
@@ -45,102 +50,65 @@ def __init__(
4550
seed=None,
4651
):
4752
super().__init__(seed=seed)
48-
self.width = width
49-
self.height = height
50-
self.citizen_density = citizen_density
51-
self.cop_density = cop_density
52-
self.citizen_vision = citizen_vision
53-
self.cop_vision = cop_vision
54-
self.legitimacy = legitimacy
55-
self.max_jail_term = max_jail_term
56-
self.active_threshold = active_threshold
57-
self.arrest_prob_constant = arrest_prob_constant
5853
self.movement = movement
5954
self.max_iters = max_iters
60-
self.iteration = 0
6155

62-
self.grid = mesa.experimental.cell_space.OrthogonalMooreGrid(
56+
self.grid = mesa.experimental.cell_space.OrthogonalVonNeumannGrid(
6357
(width, height), capacity=1, torus=True, random=self.random
6458
)
6559

6660
model_reporters = {
67-
"Quiescent": lambda m: self.count_type_citizens(m, "Quiescent"),
68-
"Active": lambda m: self.count_type_citizens(m, "Active"),
69-
"Jailed": self.count_jailed,
70-
"Cops": self.count_cops,
61+
"active": CitizenState.ACTIVE.name,
62+
"quiet": CitizenState.QUIET.name,
63+
"arrested": CitizenState.ARRESTED.name,
7164
}
7265
agent_reporters = {
73-
"x": lambda a: a.cell.coordinate[0],
74-
"y": lambda a: a.cell.coordinate[1],
75-
"breed": lambda a: type(a).__name__,
7666
"jail_sentence": lambda a: getattr(a, "jail_sentence", None),
77-
"condition": lambda a: getattr(a, "condition", None),
7867
"arrest_probability": lambda a: getattr(a, "arrest_probability", None),
7968
}
8069
self.datacollector = mesa.DataCollector(
8170
model_reporters=model_reporters, agent_reporters=agent_reporters
8271
)
83-
if self.cop_density + self.citizen_density > 1:
72+
if cop_density + citizen_density > 1:
8473
raise ValueError("Cop density + citizen density must be less than 1")
8574

8675
for cell in self.grid.all_cells:
87-
if self.random.random() < self.cop_density:
88-
cop = Cop(self, vision=self.cop_vision)
89-
cop.move_to(cell)
76+
klass = self.random.choices(
77+
[Citizen, Cop, None],
78+
cum_weights=[citizen_density, citizen_density + cop_density, 1],
79+
)[0]
9080

91-
elif self.random.random() < (self.cop_density + self.citizen_density):
81+
if klass == Cop:
82+
cop = Cop(self, vision=cop_vision, max_jail_term=max_jail_term)
83+
cop.move_to(cell)
84+
elif klass == Citizen:
9285
citizen = Citizen(
9386
self,
94-
hardship=self.random.random(),
95-
regime_legitimacy=self.legitimacy,
96-
risk_aversion=self.random.random(),
97-
threshold=self.active_threshold,
98-
vision=self.citizen_vision,
87+
regime_legitimacy=legitimacy,
88+
threshold=active_threshold,
89+
vision=citizen_vision,
90+
arrest_prob_constant=arrest_prob_constant,
9991
)
10092
citizen.move_to(cell)
10193

10294
self.running = True
95+
self._update_counts()
10396
self.datacollector.collect(self)
10497

10598
def step(self):
10699
"""
107100
Advance the model by one step and collect data.
108101
"""
109102
self.agents.shuffle_do("step")
110-
# collect data
103+
self._update_counts()
111104
self.datacollector.collect(self)
112-
self.iteration += 1
113-
if self.iteration > self.max_iters:
114-
self.running = False
115-
116-
@staticmethod
117-
def count_type_citizens(model, condition, exclude_jailed=True):
118-
"""
119-
Helper method to count agents by Quiescent/Active.
120-
"""
121-
citizens = model.agents_by_type[Citizen]
122105

123-
if exclude_jailed:
124-
return len(
125-
[
126-
c
127-
for c in citizens
128-
if (c.condition == condition) and (c.jail_sentence == 0)
129-
]
130-
)
131-
else:
132-
return len([c for c in citizens if c.condition == condition])
106+
if self.steps > self.max_iters:
107+
self.running = False
133108

134-
@staticmethod
135-
def count_jailed(model):
136-
"""
137-
Helper method to count jailed agents.
138-
"""
139-
return len([a for a in model.agents_by_type[Citizen] if a.jail_sentence > 0])
109+
def _update_counts(self):
110+
"""Helper function for counting nr. of citizens in given state."""
111+
counts = self.agents_by_type[Citizen].groupby("state").count()
140112

141-
@staticmethod
142-
def count_cops(model):
143-
"""
144-
Helper method to count jailed agents.
145-
"""
146-
return len(model.agents_by_type[Cop])
113+
for state in CitizenState:
114+
setattr(self, state.name, counts.get(state, 0))

0 commit comments

Comments
 (0)