Last updated on 16th July 2024
In this tutorial we will present an implementation of the classic Schelling model of segregation. This model illustrates how the interaction of people with "mild" in-group preference towards their own group can lead to highly segregated societies.
This model is embedded in a 2-dimensional grid forming the model environment. This model environment is inhabited by two types of agents (blue and red agents). Every agent occupies one cell in the 2-d grid. All agents have a preference for living in a neighborhood with agents of the same type, with a threshold parameter defining the level of similarity needed for an agent to be satisfied. If there are not enough similar agents (i.e. agents of the same colour) in a neighborhood, agents can decide to move to a cell where the neighborhood has more agents of similar type.
The model simulates the segregation of neighborhoods over time based on the agents' preferences for similarity.
The code for this tutorial is available as a zip download, containing a complete model as a Maven project. This project should be able to be run from any Maven or Java IDE environment.
Download Tutorial FilesIn the Simudyne SDK implementation, the model has the following core components:
The following global variables are defined in the Schelling model.
SchellingModel.java
public static final class Globals extends GlobalState {
@Input(name = "Grid Size")
public int gridSize = 40;
@Input(name = "Empty Cells Proportion")
public double emptyCellsPcg = 0.1;
@Input(name = "Similarity Threshold")
public double similarityThreshold = 0.4;
@Input(name = "Data Export Tick")
public int dataExportTick = 49;
public GridParameters gridParameters;}
gridSize
defines the length of the dimension of the grid environment. For example, a gridSize
of 40 will result in a 40*40 environment, i.e. creating 1600 cells. emptyCellsPcg
defines the percentage of empty cells overall. For instance, a emptyCellsPcg
of 0.1 will result in a 10% empty cells. similarityThreshold
defines the threshold of percentage of similar agents needed in a neighborhood to satisfy the agents. If the similarity in a neighborhood drops below this threshold the agent will decide to move neighborhoods. For example, if the similarity threshold is 0.4 and an agents neighborhood has 3 agents of the same color and 5 agents of the opposite type (i.e. 37.5% similarity) the agent will decide to move. dataExportTick
defines the tick on which the data should be exported. Please ensure here that this tick is the last tick of the iteration. gridParameters
initializes an object of class GridParameters, which defines the 2-d grid.In the setup, the environment, all agents and all links are initialized to setup the initial state of the model. Creating the initial agents is done through the concept of a topology of groups. A group represents a set of agents, where each group has a type of agent, a number of agents in that group, and optionally an initialiser, which can customise the state for each agent.
In this model, we have a group of Schelling agents and an environment.
SchellingModel.java
Group<BlueAgent> blueAgentGroup = generateGroup(BlueAgent.class, getGlobals().gridParameters.nbBlue,
blueAgent -> blueAgent.similarityThreshold = getGlobals().similarityThreshold);
Group<RedAgent> redAgentGroup = generateGroup(RedAgent.class, getGlobals().gridParameters.nbRed,
redAgent -> redAgent.similarityThreshold = getGlobals().similarityThreshold);
The Schelling agents are further divided into a group of blue and red agents, whereby both groups have the same number of agents which is defined by the number of cells minus the percentage of empty cells. On initialization, each agents similarityThreshold
is equated to the variable similarityThreshold
defined in Globals.
SchellingModel.java
getGlobals().gridParameters = new GridParameters(getGlobals().gridSize, getGlobals().emptyCellsPcg);
Group<Environment> environmentGroup = generateGroup(Environment.class, 1, Environment::initEnvironment);
In addition to the agent groups, there is also the environment which saves the information about the 2-d grid, the type of agent occupying a cell, the similarity of neighborhood and a list of empty cells. the The environment can provide the Schelling agents with information about their surrounding and point towards empty cells in case an agent decides to move.
SchellingModel.java
environmentGroup.fullyConnected(blueAgentGroup, Links.EnvironmentToSchellingLink.class);
environmentGroup.fullyConnected(redAgentGroup, Links.EnvironmentToSchellingLink.class);
blueAgentGroup.fullyConnected(environmentGroup, Links.SchellingToEnvironmentLink.class);
redAgentGroup.fullyConnected(environmentGroup, Links.SchellingToEnvironmentLink.class);
To enable the interaction between agents and environment, all agents display a connection to the environment and conversely the environment has a connection will all agents.
The step takes care of the logic that happens between each tick, in an ABM this is mostly concerned with running behavioural sequences over the system to move the system forwards through time.
In this model, the main components of each step are evaluating an agents environment and moving cells in case an agent is unhappy with its neighborhood.
The line run(Environment.updateAgentStates(), SchellingAgent.updateState());
runs an evaluation of the agents environment based on movement that occurred in the previous step. After this process, each agent has an updated measure of the percentage of similar agents in its neighborhood.
After the update, each agent will evaluate whether its neighborhood is similar enough, and if not move to a different cell. This evaluation is conducted in the SchellingAgent.step()
function. If an agent decides to move, the movement to a new empty cell is coordinated by the Environment.moveAgents()
function.
Moreover, on the first step, the function run(SchellingAgent.registerToEnvironment(), Environment.receiveRegistrations())
is executed in order to assign an initial location for each agent in the 2-d grid.
Each of these behaviors will be explained in more detail in the following section.
We'll run through the actions in this sequence in order, to see how they progress. When we see each agent for the first time, we'll also have a quick look at how it's defined.
SchellingAgent.java
public abstract class SchellingAgent extends Agent<SchellingModel.Globals> {
public final AgentState.AgentRace race;
public double similarityThreshold;
public AgentState state;
public SchellingAgent(AgentState.AgentRace race) {
this.race = race;
}
Each Schelling Agent has a race
which describes whether the agent is of type blue or red. Moreover, the Schelling agents have a similarityThreshold
which is the same for all Schelling agents and is defined in the Globals. Moreover, each agent has a state
which describes whether this agent is happy or unhappy in its environment (i.e. whether the agents wants to move or not), its current location and the similarity of its neighborhood.
SchellingAgent.java
public static Action<SchellingAgent> registerToEnvironment() {
return Action.create(SchellingAgent.class, schellingAgent -> {
schellingAgent.state = new AgentState(schellingAgent.race);
AgentState state = new AgentState(schellingAgent.race);
schellingAgent.getLinks(Links.SchellingToEnvironmentLink.class)
.send(Messages.StateMessage.class,
(msg, link) -> msg.state = state);
});
}
On the first tick of each simulation, each Schelling agent registers to the environment by sending a StateMessage
to the environment.
Messages.java
public static class StateMessage extends Message {
public AgentState state;}
Such StateMessage
entails a state
object which encorporates all relevant information about the agent, such as whether the agent is happy or not and its type.
The environment stores all information about the 2-d grid and the agents such as their position and state.
Environment.java
public class Environmenenter link description heret extends Agent<SchellingModel.Globals> {
public HashMap<Long, AgentState> agentMap;
public Random random;
public Grid grid;
public DataOutput dataOutput;
@Variable
public double averageSimilarity;
@Variable
public int unhappyAgentCount;
In the field agentMap
the environment saves a hashmap with each agents identifier as key and their state as value.
Moreover, the environment saves the grid
and overall statistics such as averageSimilarity
and unhappyAgentCount
which are displayed as outputs in the console.
public static Action<Environment> receiveRegistrations() {
return Action.create(Environment.class, environment ->
environment.getMessagesOfType(Messages.StateMessage.class).forEach(stateMessage ->
environment.agentMap.put(stateMessage.getSender(), stateMessage.state)));}
When the agents register to the environment, each agent is added to the agentMap
saving their identifier and their state.
During the first step, the environment also assigns an initial location to each agent to create the starting conditions of the 2-d grid.
Environment.java
public static Action<Environment> initPositions() {
return Action.create(Environment.class, environment -> environment.agentMap.forEach((agentID, agentState) -> {
Optional<Cell> optionalCell = environment.grid.cellList.stream()
.filter(x -> x.sameAgentState(agentState.race) && !x.occupied).findFirst();
if (!optionalCell.isPresent())
throw new IllegalArgumentException("No position found.");
Cell cell = optionalCell.get();
cell.switchState(agentState.race);
agentState.position = cell;
}));
}
Here, the environment searches for empty cells that are also in line with the agents preferences (i.e. enough similar agents in the surrounding neighborhood) and if a cell exists that satisfies these constraints the agent is assigned to this cell.
After the initialization, the first action in each tick is to update the state associated with each agent.
Environment.java
public static Action<Environment> updateAgentStates() {
return Action.create(Environment.class, environment -> {
environment.averageSimilarity = 0;
environment.agentMap.forEach((agentID, agentState) -> {
double similarityMetric = environment.calculateSimilarityMetric(agentState.position.coordinates, agentState.race);
agentState.changeSimilarityMetric(similarityMetric);
environment.averageSimilarity += similarityMetric;
environment.getLinksTo(agentID).send(Messages.StateMessage.class, (msg, link) ->
msg.state = agentState); });
environment.averageSimilarity = environment.averageSimilarity / environment.agentMap.size();
});
}
For each agent, the similarity of agents in the neighborhood is calculated and each agent is informed about the new similarity of its surrounding by the environment. This is done by sending a StateMessage
entailing an altered state
object which incorporates the new similarityMetric
for the agent's cell.
SchellingAgent.java
public static Action<SchellingAgent> updateState() {
return Action.create(SchellingAgent.class, schellingAgent ->
schellingAgent.state.update(schellingAgent.getMessageOfType(Messages.StateMessage.class).state));
}
Based on the newly calculated similarity of each agents' neighborhood the agents' update their states to incorporate this new information about their environment.
SchellingAgent.java
public static Action<SchellingAgent> step() {
return Action.create(SchellingAgent.class, schellingAgent -> {
schellingAgent.state.changeSatisfaction(schellingAgent.state.similarityMetric < schellingAgent.similarityThreshold ? AgentState.Satisfaction.UNHAPPY : AgentState.Satisfaction.HAPPY);
if (schellingAgent.state.satisfaction == AgentState.Satisfaction.UNHAPPY) {
schellingAgent.getLinks(Links.SchellingToEnvironmentLink.class).send(Messages.UnhappyMessage.class);
}
});
}
After updating the similarity of its neighborhood, each agent needs to decide whether to move or not. For this, the agent compares its neighborhood similarity to its similarity threshold and decides whether it is still happy in the current cell. If the agent is unhappy, it will send a message to the environment indicating that it would like to move.
Messages.java
public static class UnhappyMessage extends Message.Empty {}
The UnhappyMessage
is an empty message, as no additional information is needed besides the agent's wish to move cells.
Environment.java
public static Action<Environment> moveAgents() {
return Action.create(Environment.class, environment -> {
ArrayList<Long> unhappyAgents = new ArrayList<>();
environment.getMessagesOfType(Messages.UnhappyMessage.class).forEach(msg -> unhappyAgents.add(msg.getSender()));
Collections.sort(unhappyAgents);
Collections.shuffle(unhappyAgents, environment.random);
environment.unhappyAgentCount = unhappyAgents.size();
unhappyAgents.forEach(agentID -> {
AgentState.AgentRace race = environment.agentMap.get(agentID).race;
Optional<Cell> optionalCell = environment.grid.cellList.stream().filter(x ->
!x.occupied && environment.calculateSimilarityMetric(x.coordinates, race)>= environment.getGlobals().similarityThreshold).findAny();
if (!optionalCell.isPresent())
return;
Cell oldCell = environment.agentMap.get(agentID).position;
oldCell.vacate();
Cell newCell = optionalCell.get();
newCell.switchState(race);
environment.agentMap.get(agentID).changePosition(newCell);
}
);
});
}
The environment will receive messages from all agents that wish to move their cell. Based on this list of agents, the environment aims to find an appropriate cell available for each agent. This means the environment goes through the list of empty cells and evaluates whether this empty cell is in line with the similarity requirements of the agent. If a suitable cell is available, the environment indicates this to the agent and the agent moves to the new location.
At the end of each tick, the configuration of the 2-d grid is saved to enable exporting data for additional analysis outside the SDK.
public static Action<Environment> writeData() {
return Action.create(Environment.class, environment -> {
environment.dataOutput.addDataValue(environment.getContext().getTick(), new DataValue(environment.grid.cells));
});
}
At the end of the simulation (i.e. on the last tick), the data is exported as JSON
file.
Environment.java
public static Action<Environment> exportJSONOutput() {
return Action.create(Environment.class, environment -> {
try {
PrintWriter pw = new PrintWriter("Grid_History.json");
pw.write(environment.dataOutput.gridHistory.toString());
pw.flush();
pw.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
}
});
}
The data export enables to further analyze the simulation data in any data science tool of choice.
After having exported the simulation data as JSON file, this provides us with a great opportunity to exemplify how to conduct more detailed analysis of the simulation by pulling the data into an external analysis tool. In this example, we will use a Jupyter Notebook to analysis and visualize this data.
The JSON file contains information about the 2-d grid at every tick. In the Jupyter Notebook we load in the JSON file and animate the change in the position of agents over time.
import numpy as np
import pprint
import matplotlib.pyplot as plt
import matplotlib
from IPython.display import HTML
import matplotlib.patches as mpatches
import matplotlib.animation as animation
with open('GridHistory.json') as f:
data = json.load(f)
time_steps = [int(step) for step in data.keys()]
grid_idx = [int(idx) for idx in data[str(time_steps[0])].keys()]
grid_history = np.array([[[0 for k in range(len(grid_idx))] for j in range(len(grid_idx))] for i in range(len(time_steps))])
for t in time_steps:
for i in grid_idx:
for j in grid_idx:
grid_history[t, i, j] = int(data[str(t)][str(i)][str(j)])
im1=grid_history.shape
values = np.unique(grid_history.ravel())
fig=plt.figure(figsize=(8, 8))
cmap = matplotlib.colors.ListedColormap(['royalblue', 'lightgrey', 'red'])
schelling =plt.imshow(grid_history[0,:,:], cmap=cmap, interpolation='none')
colors = [schelling.cmap(schelling.norm(value)) for value in values]
patches = [mpatches.Patch(color=colors[0], label="blue agents"), mpatches.Patch(color=colors[1], label="empty cells") , mpatches.Patch(color=colors[2], label="red agents")]
plt.legend(handles=patches, bbox_to_anchor=(0.8, 0),
ncol=3, fancybox=True, shadow=True)
plt.grid(True)
plt.axis('off')
def animate_noise(i):
schelling.set_array(grid_history[i,:,:])
return [schelling]
anim = animation.FuncAnimation(fig, animate_noise, frames=dim1[0], interval=500, blit=True)
plt.close(fig)
HTML(anim.to_html5_video())
In this tutorial we have walked through an implementation of the classic Schelling model. This is an example of how a geospatial component (2-d grid) can be incorporated within the SDK. In this implementation, information about the 2-d grid is saved in the environment which communicates information about this grid to the Schelling agents. Based on this information, the Schelling agents decide whether to move cells or remain in their current cell. Through multiple iterations the model will show how people segregate based on their preferences for similarity. enter link description here