Skip to content

Commit ecf23d8

Browse files
author
abhinavKumar0206
committed
feat: add LLM Schelling Segregation example using mesa-llmImplements Schelling's (1971) classic segregation model using LLM agentsinstead of a fixed tolerance threshold.Each agent reasons in natural language about its neighborhood compositionand decides whether to stay ('happy') or relocate ('unhappy'). Thisproduces richer segregation dynamics than the classical threshold rule.Includes:- SchellingAgent extending LLMAgent with CoT reasoning- LLMSchellingModel on OrthogonalMooreGrid with torus=True- Segregation index metric tracked over time- SolaraViz with grid plot, happiness chart, and segregation index- README with comparison table vs classical Schelling modelReference: Schelling, T.C. (1971). Dynamic models of segregation.Journal of Mathematical Sociology, 1(2), 143-186.Related: mesa/mesa-llm#153
1 parent 2ba3032 commit ecf23d8

File tree

6 files changed

+352
-0
lines changed

6 files changed

+352
-0
lines changed

examples/llm_schelling/README.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# LLM Schelling Segregation
2+
3+
An LLM-powered implementation of Schelling's (1971) classic segregation model,
4+
built with [Mesa](https://github.com/projectmesa/mesa) and
5+
[Mesa-LLM](https://github.com/projectmesa/mesa-llm).
6+
7+
## Overview
8+
9+
The Schelling segregation model is one of the most influential agent-based
10+
models ever published. It demonstrates that even mild individual preferences
11+
for same-group neighbors produce strong global segregation — a striking example
12+
of emergent behavior from simple rules.
13+
14+
**Classical model:** An agent moves if fewer than a fixed threshold (e.g. 30%)
15+
of its neighbors share its group.
16+
17+
**This model:** Agents reason in natural language about their neighborhood
18+
composition and decide whether they feel comfortable staying or want to move.
19+
The LLM can weigh contextual factors, producing richer dynamics than a fixed
20+
threshold allows.
21+
22+
## The Model
23+
24+
Agents of two groups (A and B) are placed on a grid. Each step:
25+
1. Each agent observes its Moore neighborhood (up to 8 neighbors)
26+
2. It describes the neighborhood composition in natural language to the LLM
27+
3. The LLM decides: `happy` (stay) or `unhappy` (move)
28+
4. Unhappy agents relocate to a random empty cell
29+
30+
The simulation tracks happiness levels and a segregation index over time.
31+
32+
### Parameters
33+
34+
| Parameter | Description | Default |
35+
|-----------|-------------|---------|
36+
| `width` | Grid width | 10 |
37+
| `height` | Grid height | 10 |
38+
| `density` | Fraction of cells occupied | 0.8 |
39+
| `minority_fraction` | Fraction of agents in Group B | 0.4 |
40+
| `llm_model` | LLM model string | `gemini/gemini-2.0-flash` |
41+
42+
## Running the Model
43+
44+
Set your API key:
45+
```bash
46+
export GEMINI_API_KEY=your_key_here
47+
```
48+
49+
Install dependencies:
50+
```bash
51+
pip install -r requirements.txt
52+
```
53+
54+
Run the visualization:
55+
```bash
56+
solara run app.py
57+
```
58+
59+
## Comparison with Classical Schelling
60+
61+
| Feature | Classical Schelling | LLM Schelling |
62+
|---------|--------------------|--------------------|
63+
| Decision rule | Fixed threshold (e.g. 30%) | LLM natural language reasoning |
64+
| Agent memory | None | Short-term memory of interactions |
65+
| Flexibility | Rigid | Emergent from reasoning |
66+
| Interpretability | Mathematical | Natural language explanations |
67+
68+
## Reference
69+
70+
Schelling, T.C. (1971). Dynamic models of segregation.
71+
*Journal of Mathematical Sociology*, 1(2), 143–186.

examples/llm_schelling/app.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import matplotlib.pyplot as plt
2+
import numpy as np
3+
import solara
4+
from llm_schelling.model import LLMSchellingModel
5+
from mesa.visualization import SolaraViz, make_plot_component
6+
from mesa.visualization.utils import update_counter
7+
8+
GROUP_COLORS = {0: "#2196F3", 1: "#FF5722"} # Blue, Orange
9+
10+
model_params = {
11+
"width": {
12+
"type": "SliderInt",
13+
"value": 10,
14+
"label": "Grid width",
15+
"min": 5,
16+
"max": 20,
17+
"step": 1,
18+
},
19+
"height": {
20+
"type": "SliderInt",
21+
"value": 10,
22+
"label": "Grid height",
23+
"min": 5,
24+
"max": 20,
25+
"step": 1,
26+
},
27+
"density": {
28+
"type": "SliderFloat",
29+
"value": 0.8,
30+
"label": "Population density",
31+
"min": 0.1,
32+
"max": 1.0,
33+
"step": 0.05,
34+
},
35+
"minority_fraction": {
36+
"type": "SliderFloat",
37+
"value": 0.4,
38+
"label": "Minority fraction",
39+
"min": 0.1,
40+
"max": 0.5,
41+
"step": 0.05,
42+
},
43+
}
44+
45+
46+
def SchellingGridPlot(model):
47+
"""Visualize the grid showing agent groups and happiness."""
48+
update_counter.get()
49+
50+
width = model.grid.dimensions[0]
51+
height = model.grid.dimensions[1]
52+
53+
fig, ax = plt.subplots(figsize=(6, 6))
54+
ax.set_xlim(0, width)
55+
ax.set_ylim(0, height)
56+
ax.set_aspect("equal")
57+
ax.set_xticks([])
58+
ax.set_yticks([])
59+
ax.set_title("Schelling Segregation (LLM)\nBlue=Group A, Orange=Group B, X=Unhappy")
60+
61+
for agent in model.agents:
62+
x, y = agent.pos
63+
color = GROUP_COLORS[agent.group]
64+
marker = "o" if agent.is_happy else "x"
65+
ax.plot(x + 0.5, y + 0.5, marker=marker, color=color,
66+
markersize=8, markeredgewidth=2)
67+
68+
return solara.FigureMatplotlib(fig)
69+
70+
71+
HappinessPlot = make_plot_component({"happy": "#4CAF50", "unhappy": "#F44336"})
72+
SegregationPlot = make_plot_component("segregation_index")
73+
74+
model = LLMSchellingModel()
75+
76+
page = SolaraViz(
77+
model,
78+
components=[
79+
SchellingGridPlot,
80+
HappinessPlot,
81+
SegregationPlot,
82+
],
83+
model_params=model_params,
84+
name="LLM Schelling Segregation",
85+
)

examples/llm_schelling/llm_schelling/__init__.py

Whitespace-only changes.
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
from mesa_llm.llm_agent import LLMAgent
2+
from mesa_llm.reasoning.reasoning import Reasoning
3+
4+
5+
class SchellingAgent(LLMAgent):
6+
"""
7+
An LLM-powered agent in the Schelling Segregation model.
8+
9+
Unlike the classical Schelling model where agents move if fewer than
10+
a fixed fraction of neighbors share their group, this agent reasons
11+
about its neighborhood using an LLM and decides whether it feels
12+
comfortable staying or wants to move.
13+
14+
Attributes:
15+
group (int): The agent's group identity (0 or 1).
16+
is_happy (bool): Whether the agent is satisfied with its location.
17+
"""
18+
19+
def __init__(self, model, reasoning: type[Reasoning], group: int):
20+
group_label = "Group A" if group == 0 else "Group B"
21+
other_label = "Group B" if group == 0 else "Group A"
22+
23+
system_prompt = f"""You are an agent in a social simulation. You belong to {group_label}.
24+
You are deciding whether you feel comfortable in your current neighborhood.
25+
Look at your neighbors: if too many belong to {other_label} and too few to {group_label},
26+
you may feel uncomfortable and want to move.
27+
Respond with ONLY one word: 'happy' if you want to stay, or 'unhappy' if you want to move."""
28+
29+
super().__init__(
30+
model=model,
31+
reasoning=reasoning,
32+
system_prompt=system_prompt,
33+
vision=1,
34+
internal_state=["group", "is_happy"],
35+
)
36+
self.group = group
37+
self.is_happy = True
38+
39+
def step(self):
40+
"""Decide whether to move based on LLM reasoning about neighborhood."""
41+
obs = self.generate_obs()
42+
43+
# Count neighbors by group
44+
neighbors = list(self.cell.neighborhood.agents) if self.cell else []
45+
same = sum(1 for n in neighbors if hasattr(n, "group") and n.group == self.group)
46+
total = len(neighbors)
47+
different = total - same
48+
49+
if total == 0:
50+
self.is_happy = True
51+
self.internal_state = [f"group:{self.group}", "is_happy:True"]
52+
return
53+
54+
group_label = "Group A" if self.group == 0 else "Group B"
55+
other_label = "Group B" if self.group == 0 else "Group A"
56+
57+
step_prompt = f"""You belong to {group_label}.
58+
Your current neighborhood has:
59+
- {same} neighbors from {group_label} (your group)
60+
- {different} neighbors from {other_label} (the other group)
61+
- {total} neighbors total
62+
63+
Do you feel comfortable here, or do you want to move to a different location?
64+
Respond with ONLY one word: 'happy' or 'unhappy'."""
65+
66+
plan = self.reasoning.plan(obs, step_prompt=step_prompt)
67+
68+
# Parse LLM response
69+
response_text = ""
70+
try:
71+
if hasattr(plan, "llm_plan") and plan.llm_plan:
72+
for block in plan.llm_plan:
73+
if hasattr(block, "text"):
74+
response_text += block.text.lower()
75+
except Exception:
76+
pass
77+
78+
self.is_happy = "unhappy" not in response_text
79+
self.internal_state = [
80+
f"group:{self.group}",
81+
f"is_happy:{self.is_happy}",
82+
]
83+
84+
# If unhappy, move to a random empty cell
85+
if not self.is_happy:
86+
empty_cells = [
87+
cell
88+
for cell in self.model.grid.all_cells
89+
if len(list(cell.agents)) == 0
90+
]
91+
if empty_cells:
92+
new_cell = self.model.random.choice(empty_cells)
93+
self.cell = new_cell
94+
self.pos = new_cell.coordinate
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import mesa
2+
from mesa.discrete_space import OrthogonalMooreGrid
3+
from mesa_llm.reasoning.cot import CoTReasoning
4+
5+
from .agent import SchellingAgent
6+
7+
8+
class LLMSchellingModel(mesa.Model):
9+
"""
10+
An LLM-powered Schelling Segregation model.
11+
12+
The classical Schelling (1971) segregation model shows that even mild
13+
individual preferences for same-group neighbors lead to strong global
14+
segregation. In the classical model, agents move if fewer than a fixed
15+
threshold fraction of their neighbors share their group.
16+
17+
This model replaces the threshold rule with LLM reasoning: agents
18+
describe their neighborhood in natural language and decide whether they
19+
feel comfortable staying or want to move. This produces richer dynamics
20+
where the decision depends on framing, context, and reasoning — not
21+
just a number.
22+
23+
Reference:
24+
Schelling, T.C. (1971). Dynamic models of segregation.
25+
Journal of Mathematical Sociology, 1(2), 143-186.
26+
27+
Args:
28+
width (int): Grid width.
29+
height (int): Grid height.
30+
density (float): Fraction of cells that are occupied.
31+
minority_fraction (float): Fraction of agents in group 1 (minority).
32+
llm_model (str): LLM model string in 'provider/model' format.
33+
rng: Random number generator.
34+
"""
35+
36+
def __init__(
37+
self,
38+
width: int = 10,
39+
height: int = 10,
40+
density: float = 0.8,
41+
minority_fraction: float = 0.4,
42+
llm_model: str = "gemini/gemini-2.0-flash",
43+
rng=None,
44+
):
45+
super().__init__(rng=rng)
46+
47+
self.grid = OrthogonalMooreGrid(
48+
(width, height), torus=True, random=self.random
49+
)
50+
51+
self.datacollector = mesa.DataCollector(
52+
model_reporters={
53+
"happy": lambda m: sum(
54+
1 for a in m.agents if a.is_happy
55+
),
56+
"unhappy": lambda m: sum(
57+
1 for a in m.agents if not a.is_happy
58+
),
59+
"segregation_index": lambda m: self._segregation_index(m),
60+
}
61+
)
62+
63+
# Place agents on grid
64+
for cell in self.grid.all_cells:
65+
if self.random.random() < density:
66+
group = 1 if self.random.random() < minority_fraction else 0
67+
agent = SchellingAgent(
68+
model=self,
69+
reasoning=CoTReasoning,
70+
group=group,
71+
)
72+
agent.cell = cell
73+
agent.pos = cell.coordinate
74+
75+
self.running = True
76+
self.datacollector.collect(self)
77+
78+
def step(self):
79+
"""Advance the model by one step."""
80+
self.agents.shuffle_do("step")
81+
self.datacollector.collect(self)
82+
83+
# Stop if everyone is happy
84+
if all(a.is_happy for a in self.agents):
85+
self.running = False
86+
87+
@staticmethod
88+
def _segregation_index(model):
89+
"""
90+
Measure segregation as the average fraction of same-group neighbors.
91+
Higher values indicate more segregation.
92+
"""
93+
scores = []
94+
for agent in model.agents:
95+
neighbors = list(agent.cell.neighborhood.agents)
96+
if not neighbors:
97+
continue
98+
same = sum(1 for n in neighbors if n.group == agent.group)
99+
scores.append(same / len(neighbors))
100+
return sum(scores) / len(scores) if scores else 0.0
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
mesa[viz]>=3.0
2+
mesa-llm>=0.1.0

0 commit comments

Comments
 (0)