# 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: [Epstein, J. “Modeling civil violence: An agent-based computational approach”, Proceedings of the National Academy of Sciences, Vol. 99, Suppl. 3, May 14, 2002](https://doi.org/10.1073/pnas.092080199) 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 ```python import math from enum import Enum import mesa class CitizenState(Enum): ACTIVE = 1 QUIET = 2 ARRESTED = 3 class EpsteinAgent(mesa.discrete_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 ```python 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.discrete_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 ```python 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, SpaceRenderer, make_plot_component, ) from mesa.visualization.components import AgentPortrayalStyle 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 = AgentPortrayalStyle(size=200) if isinstance(agent, Citizen): portrayal.update(("color", agent_colors[agent.state])) elif isinstance(agent, Cop): portrayal.update(("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.1, 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), } chart_component = make_plot_component( {state.name.lower(): agent_colors[state] for state in CitizenState} ) epstein_model = EpsteinCivilViolence() renderer = SpaceRenderer(epstein_model, backend="matplotlib") renderer.draw_agents(citizen_cop_portrayal) renderer.post_process = post_process page = SolaraViz( epstein_model, renderer, components=[chart_component], model_params=model_params, name="Epstein Civil Violence", ) page # noqa ```