Epstein Civil Violence Model#
Summary#
This model is based on Joshua Epstein’s simulation of how civil unrest grows and is suppressed. Citizen agents wander the grid randomly, and are endowed with individual risk aversion and hardship levels; there is also a universal regime legitimacy value. There are also Cop agents, who work on behalf of the regime. Cops arrest Citizens who are actively rebelling; Citizens decide whether to rebel based on their hardship and the regime legitimacy, and their perceived probability of arrest.
The model generates mass uprising as self-reinforcing processes: if enough agents are rebelling, the probability of any individual agent being arrested is reduced, making more agents more likely to join the uprising. However, the more rebelling Citizens the Cops arrest, the less likely additional agents become to join.
How to Run#
To run the model interactively, in this directory, run the following command
$ solara run app.py
Files#
model.py
: Core model code.agent.py
: Agent classes.app.py
: Sets up the interactive visualization.Epstein Civil Violence.ipynb
: Jupyter notebook conducting some preliminary analysis of the model.
Further Reading#
This model is based adapted from:
A similar model is also included with NetLogo:
Wilensky, U. (2004). NetLogo Rebellion model. http://ccl.northwestern.edu/netlogo/models/Rebellion. Center for Connected Learning and Computer-Based Modeling, Northwestern University, Evanston, IL.
Agents#
import math
from enum import Enum
import mesa
class CitizenState(Enum):
ACTIVE = 1
QUIET = 2
ARRESTED = 3
class EpsteinAgent(mesa.experimental.cell_space.CellAgent):
def update_neighbors(self):
"""
Look around and see who my neighbors are
"""
self.neighborhood = self.cell.get_neighborhood(radius=self.vision)
self.neighbors = self.neighborhood.agents
self.empty_neighbors = [c for c in self.neighborhood if c.is_empty]
def move(self):
if self.model.movement and self.empty_neighbors:
new_pos = self.random.choice(self.empty_neighbors)
self.move_to(new_pos)
class Citizen(EpsteinAgent):
"""
A member of the general population, may or may not be in active rebellion.
Summary of rule: If grievance - risk > threshold, rebel.
Attributes:
hardship: Agent's 'perceived hardship (i.e., physical or economic
privation).' Exogenous, drawn from U(0,1).
regime_legitimacy: Agent's perception of regime legitimacy, equal
across agents. Exogenous.
risk_aversion: Exogenous, drawn from U(0,1).
threshold: if (grievance - (risk_aversion * arrest_probability)) >
threshold, go/remain Active
vision: number of cells in each direction (N, S, E and W) that agent
can inspect
condition: Can be "Quiescent" or "Active;" deterministic function of
greivance, perceived risk, and
grievance: deterministic function of hardship and regime_legitimacy;
how aggrieved is agent at the regime?
arrest_probability: agent's assessment of arrest probability, given
rebellion
"""
def __init__(
self, model, regime_legitimacy, threshold, vision, arrest_prob_constant
):
"""
Create a new Citizen.
Args:
model: the model to which the agent belongs
hardship: Agent's 'perceived hardship (i.e., physical or economic
privation).' Exogenous, drawn from U(0,1).
regime_legitimacy: Agent's perception of regime legitimacy, equal
across agents. Exogenous.
risk_aversion: Exogenous, drawn from U(0,1).
threshold: if (grievance - (risk_aversion * arrest_probability)) >
threshold, go/remain Active
vision: number of cells in each direction (N, S, E and W) that
agent can inspect. Exogenous.
model: model instance
"""
super().__init__(model)
self.hardship = self.random.random()
self.risk_aversion = self.random.random()
self.regime_legitimacy = regime_legitimacy
self.threshold = threshold
self.state = CitizenState.QUIET
self.vision = vision
self.jail_sentence = 0
self.grievance = self.hardship * (1 - self.regime_legitimacy)
self.arrest_prob_constant = arrest_prob_constant
self.arrest_probability = None
self.neighborhood = []
self.neighbors = []
self.empty_neighbors = []
def step(self):
"""
Decide whether to activate, then move if applicable.
"""
if self.jail_sentence:
self.jail_sentence -= 1
return # no other changes or movements if agent is in jail.
self.update_neighbors()
self.update_estimated_arrest_probability()
net_risk = self.risk_aversion * self.arrest_probability
if (self.grievance - net_risk) > self.threshold:
self.state = CitizenState.ACTIVE
else:
self.state = CitizenState.QUIET
self.move()
def update_estimated_arrest_probability(self):
"""
Based on the ratio of cops to actives in my neighborhood, estimate the
p(Arrest | I go active).
"""
cops_in_vision = 0
actives_in_vision = 1 # citizen counts herself
for neighbor in self.neighbors:
if isinstance(neighbor, Cop):
cops_in_vision += 1
elif neighbor.state == CitizenState.ACTIVE:
actives_in_vision += 1
# there is a body of literature on this equation
# the round is not in the pnas paper but without it, its impossible to replicate
# the dynamics shown there.
self.arrest_probability = 1 - math.exp(
-1 * self.arrest_prob_constant * round(cops_in_vision / actives_in_vision)
)
class Cop(EpsteinAgent):
"""
A cop for life. No defection.
Summary of rule: Inspect local vision and arrest a random active agent.
Attributes:
unique_id: unique int
x, y: Grid coordinates
vision: number of cells in each direction (N, S, E and W) that cop is
able to inspect
"""
def __init__(self, model, vision, max_jail_term):
"""
Create a new Cop.
Args:
x, y: Grid coordinates
vision: number of cells in each direction (N, S, E and W) that
agent can inspect. Exogenous.
model: model instance
"""
super().__init__(model)
self.vision = vision
self.max_jail_term = max_jail_term
def step(self):
"""
Inspect local vision and arrest a random active agent. Move if
applicable.
"""
self.update_neighbors()
active_neighbors = []
for agent in self.neighbors:
if isinstance(agent, Citizen) and agent.state == CitizenState.ACTIVE:
active_neighbors.append(agent)
if active_neighbors:
arrestee = self.random.choice(active_neighbors)
arrestee.jail_sentence = self.random.randint(0, self.max_jail_term)
arrestee.state = CitizenState.ARRESTED
self.move()
Model#
import mesa
from mesa.examples.advanced.epstein_civil_violence.agents import (
Citizen,
CitizenState,
Cop,
)
class EpsteinCivilViolence(mesa.Model):
"""
Model 1 from "Modeling civil violence: An agent-based computational
approach," by Joshua Epstein.
http://www.pnas.org/content/99/suppl_3/7243.full
Args:
height: grid height
width: grid width
citizen_density: approximate % of cells occupied by citizens.
cop_density: approximate % of cells occupied by cops.
citizen_vision: number of cells in each direction (N, S, E and W) that
citizen can inspect
cop_vision: number of cells in each direction (N, S, E and W) that cop
can inspect
legitimacy: (L) citizens' perception of regime legitimacy, equal
across all citizens
max_jail_term: (J_max)
active_threshold: if (grievance - (risk_aversion * arrest_probability))
> threshold, citizen rebels
arrest_prob_constant: set to ensure agents make plausible arrest
probability estimates
movement: binary, whether agents try to move at step end
max_iters: model may not have a natural stopping point, so we set a
max.
"""
def __init__(
self,
width=40,
height=40,
citizen_density=0.7,
cop_density=0.074,
citizen_vision=7,
cop_vision=7,
legitimacy=0.8,
max_jail_term=1000,
active_threshold=0.1,
arrest_prob_constant=2.3,
movement=True,
max_iters=1000,
seed=None,
):
super().__init__(seed=seed)
self.movement = movement
self.max_iters = max_iters
self.grid = mesa.experimental.cell_space.OrthogonalVonNeumannGrid(
(width, height), capacity=1, torus=True, random=self.random
)
model_reporters = {
"active": CitizenState.ACTIVE.name,
"quiet": CitizenState.QUIET.name,
"arrested": CitizenState.ARRESTED.name,
}
agent_reporters = {
"jail_sentence": lambda a: getattr(a, "jail_sentence", None),
"arrest_probability": lambda a: getattr(a, "arrest_probability", None),
}
self.datacollector = mesa.DataCollector(
model_reporters=model_reporters, agent_reporters=agent_reporters
)
if cop_density + citizen_density > 1:
raise ValueError("Cop density + citizen density must be less than 1")
for cell in self.grid.all_cells:
klass = self.random.choices(
[Citizen, Cop, None],
cum_weights=[citizen_density, citizen_density + cop_density, 1],
)[0]
if klass == Cop:
cop = Cop(self, vision=cop_vision, max_jail_term=max_jail_term)
cop.move_to(cell)
elif klass == Citizen:
citizen = Citizen(
self,
regime_legitimacy=legitimacy,
threshold=active_threshold,
vision=citizen_vision,
arrest_prob_constant=arrest_prob_constant,
)
citizen.move_to(cell)
self.running = True
self._update_counts()
self.datacollector.collect(self)
def step(self):
"""
Advance the model by one step and collect data.
"""
self.agents.shuffle_do("step")
self._update_counts()
self.datacollector.collect(self)
if self.steps > self.max_iters:
self.running = False
def _update_counts(self):
"""Helper function for counting nr. of citizens in given state."""
counts = self.agents_by_type[Citizen].groupby("state").count()
for state in CitizenState:
setattr(self, state.name, counts.get(state, 0))
App#
from mesa.examples.advanced.epstein_civil_violence.agents import (
Citizen,
CitizenState,
Cop,
)
from mesa.examples.advanced.epstein_civil_violence.model import EpsteinCivilViolence
from mesa.visualization import (
Slider,
SolaraViz,
make_plot_component,
make_space_component,
)
COP_COLOR = "#000000"
agent_colors = {
CitizenState.ACTIVE: "#FE6100",
CitizenState.QUIET: "#648FFF",
CitizenState.ARRESTED: "#808080",
}
def citizen_cop_portrayal(agent):
if agent is None:
return
portrayal = {
"size": 50,
}
if isinstance(agent, Citizen):
portrayal["color"] = agent_colors[agent.state]
elif isinstance(agent, Cop):
portrayal["color"] = COP_COLOR
return portrayal
def post_process(ax):
ax.set_aspect("equal")
ax.set_xticks([])
ax.set_yticks([])
ax.get_figure().set_size_inches(10, 10)
model_params = {
"seed": {
"type": "InputText",
"value": 42,
"label": "Random Seed",
},
"height": 40,
"width": 40,
"citizen_density": Slider("Initial Agent Density", 0.7, 0.0, 0.9, 0.1),
"cop_density": Slider("Initial Cop Density", 0.04, 0.0, 0.1, 0.01),
"citizen_vision": Slider("Citizen Vision", 7, 1, 10, 1),
"cop_vision": Slider("Cop Vision", 7, 1, 10, 1),
"legitimacy": Slider("Government Legitimacy", 0.82, 0.0, 1, 0.01),
"max_jail_term": Slider("Max Jail Term", 30, 0, 50, 1),
}
space_component = make_space_component(
citizen_cop_portrayal, post_process=post_process, draw_grid=False
)
chart_component = make_plot_component(
{state.name.lower(): agent_colors[state] for state in CitizenState}
)
epstein_model = EpsteinCivilViolence()
page = SolaraViz(
epstein_model,
components=[space_component, chart_component],
model_params=model_params,
name="Epstein Civil Violence",
)
page # noqa