diff --git a/README.md b/README.md index f30f818cb..777533c25 100644 --- a/README.md +++ b/README.md @@ -71,13 +71,19 @@ This project is an agent-based model implemented using the Mesa framework in Pyt ### [Emperor's Dilemma](https://github.com/mesa/mesa-examples/tree/main/examples/emperor_dilemma) This project simulates how unpopular norms can dominate a society even when the vast majority of individuals privately reject them. It demonstrates the "illusion of consensus" where agents, driven by a fear of appearing disloyal, not only comply with a rule they hate but also aggressively enforce it on their neighbors. This phenomenon creates a "trap" of False Enforcement, where the loudest defenders of a norm are often its secret opponents. + ### [Humanitarian Aid Distribution Model](https://github.com/mesa/mesa-examples/tree/main/examples/humanitarian_aid_distribution) This model simulates a humanitarian aid distribution scenario using a needs-based behavioral architecture. Beneficiaries have dynamic needs (water, food) and trucks distribute aid using a hybrid triage system. + ### [Rumor Mill Model](https://github.com/mesa/mesa-examples/tree/main/examples/rumor_mill) A simple agent-based simulation showing how rumors spread through a population based on the spread chance and initial knowing percentage, implemented with the Mesa framework and adapted from NetLogo [Rumor mill](https://www.netlogoweb.org/launch#https://www.netlogoweb.org/assets/modelslib/Sample%20Models/Social%20Science/Rumor%20Mill.nlogox). +### [Axelrod Culture Model](https://github.com/mesa/mesa-examples/tree/main/examples/axelrod_culture) + +An implementation of Axelrod's model of cultural dissemination. Agents on a grid hold multi-feature cultural profiles and interact with neighbors based on cultural similarity, producing emergent cultural regions. Demonstrates how local convergence and global polarization can coexist. + ## Continuous Space Examples _No user examples available yet._ @@ -135,4 +141,4 @@ This folder contains an implementation of El Farol restaurant model. Agents (res ### [Schelling Model with Caching and Replay](https://github.com/mesa/mesa-examples/tree/main/examples/caching_and_replay) -This example applies caching on the Mesa [Schelling](https://github.com/mesa/mesa-examples/tree/main/examples/schelling) example. It enables a simulation run to be "cached" or in other words recorded. The recorded simulation run is persisted on the local file system and can be replayed at any later point. +This example applies caching on the Mesa [Schelling](https://github.com/mesa/mesa-examples/tree/main/examples/schelling) example. It enables a simulation run to be "cached" or in other words recorded. The recorded simulation run is persisted on the local file system and can be replayed at any later point. \ No newline at end of file diff --git a/examples/axelrod_culture/README.md b/examples/axelrod_culture/README.md new file mode 100644 index 000000000..a8cd671b3 --- /dev/null +++ b/examples/axelrod_culture/README.md @@ -0,0 +1,27 @@ +# Axelrod Culture Model + +## Summary + +An implementation of Axelrod's model of cultural dissemination. Each agent occupies a cell on a grid and holds a "culture" consisting of **F** features, each taking one of **Q** possible integer traits. At each step, an agent randomly selects a neighbor and interacts with probability equal to their cultural similarity (fraction of shared features). If interaction occurs, the agent copies one of the neighbor's differing traits. + +Despite the local tendency toward convergence, stable cultural regions can persist globally — Axelrod's key insight: local homogenization and global polarization can coexist. The number of stable regions increases with Q (more possible traits → more diversity) and decreases with F (more features → more overlap → faster convergence). + +## How to Run + +To install the dependencies use pip and the requirements.txt in this directory: + + $ pip install -r requirements.txt + +To run the model interactively, in this directory, run the following command: + + $ solara run app.py + +## Files + +* [agents.py](axelrod_culture/agents.py): Defines `CultureAgent` with a cultural profile and interaction logic +* [model.py](axelrod_culture/model.py): Sets up the grid, initializes random cultures, and tracks cultural regions +* [app.py](app.py): Solara based visualization showing the culture grid and region count over time + +## Further Reading + +* Axelrod, R. (1997). The dissemination of culture: A model with local convergence and global polarization. *Journal of Conflict Resolution*, 41(2), 203–226. https://doi.org/10.1177/0022002797041002001 \ No newline at end of file diff --git a/examples/axelrod_culture/app.py b/examples/axelrod_culture/app.py new file mode 100644 index 000000000..2f6bd1f6c --- /dev/null +++ b/examples/axelrod_culture/app.py @@ -0,0 +1,77 @@ +import hashlib + +import numpy as np +import solara +from axelrod_culture.model import AxelrodModel, number_of_cultural_regions +from matplotlib.figure import Figure +from mesa.visualization import SolaraViz, make_plot_component + + +def culture_to_color(culture): + key = str(culture).encode() + h = hashlib.sha256(key).hexdigest() + return (int(h[0:2], 16) / 255, int(h[2:4], 16) / 255, int(h[4:6], 16) / 255) + + +def make_culture_grid(model): + fig = Figure(figsize=(5, 5)) + fig.subplots_adjust(left=0.05, right=0.95, top=0.92, bottom=0.05) + ax = fig.add_subplot(111) + grid = np.zeros((model.height, model.width, 3)) + for agent in model.agents: + x, y = int(agent.cell.coordinate[0]), int(agent.cell.coordinate[1]) + grid[y][x] = culture_to_color(agent.culture) + ax.imshow(grid, origin="lower", interpolation="nearest") + ax.set_title(f"Cultural Regions: {number_of_cultural_regions(model)}", fontsize=11) + ax.set_xticks([]) + ax.set_yticks([]) + return solara.FigureMatplotlib(fig) + + +RegionsPlot = make_plot_component({"Cultural Regions": "#e63946"}) + +model_params = { + "rng": {"type": "InputText", "value": 42, "label": "Random Seed"}, + "width": { + "type": "SliderInt", + "value": 10, + "label": "Grid Width", + "min": 5, + "max": 20, + "step": 1, + }, + "height": { + "type": "SliderInt", + "value": 10, + "label": "Grid Height", + "min": 5, + "max": 20, + "step": 1, + }, + "f": { + "type": "SliderInt", + "value": 3, + "label": "Features (F)", + "min": 2, + "max": 10, + "step": 1, + }, + "q": { + "type": "SliderInt", + "value": 3, + "label": "Traits per feature (Q)", + "min": 2, + "max": 15, + "step": 1, + }, +} + +model = AxelrodModel() + +page = SolaraViz( + model, + components=[make_culture_grid, RegionsPlot], + model_params=model_params, + name="Axelrod Culture Model", +) +page # noqa diff --git a/examples/axelrod_culture/axelrod_culture/__init__.py b/examples/axelrod_culture/axelrod_culture/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/axelrod_culture/axelrod_culture/agents.py b/examples/axelrod_culture/axelrod_culture/agents.py new file mode 100644 index 000000000..7190d9942 --- /dev/null +++ b/examples/axelrod_culture/axelrod_culture/agents.py @@ -0,0 +1,36 @@ +from mesa import Agent + + +class CultureAgent(Agent): + """An agent with a cultural profile made of f features, each with q possible traits. + + Two agents interact with probability equal to their cultural similarity + (fraction of features they share). If they interact, the focal agent + copies one randomly chosen differing feature from the neighbor. + + Attributes: + culture (list[int]): List of f integers, each in range [0, q) + """ + + def __init__(self, model, culture): + super().__init__(model) + self.culture = list(culture) + + def similarity(self, other): + """Return fraction of features shared with another agent (0.0 to 1.0).""" + matches = sum(a == b for a, b in zip(self.culture, other.culture)) + return matches / len(self.culture) + + def interact_with(self, other): + """Interact with another agent based on cultural similarity.""" + sim = self.similarity(other) + if sim == 0.0 or sim == 1.0: + return + if self.random.random() < sim: + differing = [ + i + for i in range(len(self.culture)) + if self.culture[i] != other.culture[i] + ] + feature = self.random.choice(differing) + self.culture[feature] = other.culture[feature] diff --git a/examples/axelrod_culture/axelrod_culture/model.py b/examples/axelrod_culture/axelrod_culture/model.py new file mode 100644 index 000000000..066b60a7b --- /dev/null +++ b/examples/axelrod_culture/axelrod_culture/model.py @@ -0,0 +1,105 @@ +""" +Axelrod Culture Model +===================== + +Models how local cultural interactions can produce global polarization. +Each agent has a "culture" consisting of f features, each taking one of +q possible integer traits. At each step, an agent picks a random neighbor +and interacts with probability equal to their cultural similarity. If they +interact, the agent copies one of the neighbor's differing traits. + +Despite the tendency toward local convergence, stable cultural regions +can persist -- a phenomenon Axelrod called the "culture problem": +local homogenization coexisting with global diversity. + +Key result: the number of stable cultural regions decreases with f +(more features -> more convergence) and increases with q (more traits +per feature -> more diversity and fragmentation). + +Reference: + Axelrod, R. (1997). The dissemination of culture: A model with local + convergence and global polarization. Journal of Conflict Resolution, + 41(2), 203-226. +""" + +from mesa import Model +from mesa.datacollection import DataCollector +from mesa.discrete_space import OrthogonalVonNeumannGrid + +from axelrod_culture.agents import CultureAgent + + +def number_of_cultural_regions(model): + """Count distinct stable cultural regions using flood fill on the grid.""" + visited = set() + regions = 0 + agent_by_pos = { + (int(a.cell.coordinate[0]), int(a.cell.coordinate[1])): a for a in model.agents + } + for pos in agent_by_pos: + if pos in visited: + continue + queue = [pos] + visited.add(pos) + while queue: + cx, cy = queue.pop() + for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]: + npos = (cx + dx, cy + dy) + if ( + npos not in visited + and npos in agent_by_pos + and agent_by_pos[npos].culture == agent_by_pos[(cx, cy)].culture + ): + visited.add(npos) + queue.append(npos) + regions += 1 + return regions + + +class AxelrodModel(Model): + """Axelrod's model of cultural dissemination on a grid. + + Attributes: + width (int): Grid width + height (int): Grid height + f (int): Number of cultural features per agent + q (int): Number of possible traits per feature + grid: OrthogonalVonNeumannGrid containing agents + """ + + def __init__(self, width=10, height=10, f=3, q=3, rng=None): + super().__init__(rng=rng) + + self.width = width + self.height = height + self.f = f + self.q = q + + self.grid = OrthogonalVonNeumannGrid( + (width, height), torus=False, random=self.random + ) + + cultures = [ + [self.random.randrange(q) for _ in range(f)] for _ in range(width * height) + ] + + CultureAgent.create_agents(self, width * height, cultures) + + for agent, cell in zip(self.agents, self.grid.all_cells.cells): + agent.cell = cell + + self.datacollector = DataCollector( + model_reporters={"Cultural Regions": number_of_cultural_regions} + ) + self.running = True + self.datacollector.collect(self) + + def step(self): + agent_list = list(self.agents) + for _ in range(self.width * self.height): + agent = self.random.choice(agent_list) + neighbors = [a for a in agent.cell.neighborhood.agents if a is not agent] + if neighbors: + neighbor = self.random.choice(neighbors) + agent.interact_with(neighbor) + self.datacollector.collect(self) diff --git a/examples/axelrod_culture/requirements.txt b/examples/axelrod_culture/requirements.txt new file mode 100644 index 000000000..46c19f181 --- /dev/null +++ b/examples/axelrod_culture/requirements.txt @@ -0,0 +1,3 @@ +mesa +matplotlib +solara \ No newline at end of file