Skip to content

Quickstart Guide Part 8: Agent-Place Interaction

In the previous two parts of the Quickstart Guide, you encountered features of the FRED modeling language that allow agents to interact with each other during the course of a simulation. In Part 6, you used the ask and tell functions to have agents determine and change the values of each other's agent variables. And in Part 7, you saw how setting a condition to be transmissible enables agents to "expose" other agents when they are present at the same place at the same time.

In this lesson, you will explore how agents can interact with the places that comprise their environment. By the end of this lesson you will be able to: 1. Describe the concept of a group agent and their relation to a place. 2. Have regular agents make decisions based on the environment of the simulation by querying the group agents of places using the ask and tell functions. 3. Use variables to describe the current status of a place by having the group agent ask the members of their group questions using the ask function. 4. Move agents around in the environment by having them quit one place and join another. 5. Select a subset of agents from a population to include in a simulation and remove the other agents using the exit function.

Let's begin by loading the Python packages you will need to run this FRED simulation and examine the outputs:

# Epistemix package for managing simulations and simulation results in Python
from epx import Job, ModelConfig, SynthPop
import time

import pandas as pd
import json
import plotly.express as px
import plotly.io as pio
import plotly.graph_objects as go
import requests

# Use the Epistemix default plotly template
r = requests.get("https://gist.githubusercontent.com/daniel-epistemix/8009ad31ebfa96ac97b7be038c014c0d/raw/320c3b0ca3dfbf7946e49c97254fa65d4753aeac/epx_plotly_theme.json")
if r.status_code == 200:
    pio.templates["epistemix"] = go.layout.Template(r.json())
    pio.templates.default = "epistemix"

8.1 Schelling's model of segregation

The model you will run in this part of the Quickstart Guide is an implementation of Thomas Schelling's classic model of emergent racial segregation in cities. In the model, agents are assigned one of two colors: blue or red. At each time step in the simulation, agents assess the color distribution of the residents in their census block group and determine if they are happy with the fraction of agents that share their color.

If the agent is happy with the number of similar agents, they remain in their current household. If they are unhappy, they find a new place to live from a shared list of available, empty houses. This cycle repeats everyday.

In his original models (carried out with pen and paper!), Schelling looked at agents living on a grid of squares. At each time step, the agents assessed the color of those agents living on the 8 squares immediately adjacent to their own. Then, they made a decision about whether to stay in their current location or move to a new one based on the colors of those neighboring agents. Over time, interesting structure appears in the spatial distribution of agent color.

The Schelling model is often used as an example for demonstrating agent-based modeling capabilities. The image below shows an example from NetLogo, an agent-based modeling software originally developed to teach children about computer programming:

NetLogo_Schelling_example.png

Since the houses in the Epistemix Platform's synthetic population are not placed on a grid, we are able to explore more realistic spatial distributions of agents. The model you will explore here assesses the color property of all the agents living within a census block group (the smallest geographic unit in the Epistemix Platform's synthetic population). Schelling actually explored a similar model in his work, to investigate segregation in gated communities and in institutions like social clubs.

The Schelling models of segregation are some of the earliest examples of agent-based models, and they are famous because of the emergent behavior they reveal. By running the model below, you will quickly see this behavior for yourself. These models continue to be studied by sociologists, and they have many interesting use cases in research and business problems.

8.2 Group Agents and Places

The implementation of the Schelling model presented here makes use of a powerful feature of FRED that enables regular agents to interact with the places they inhabit in a unique way - the Group Agent.

Group agents represent an abstract agenct associated with a specific mixing group (i.e., place or network). For example, you can set up your model so that a group agent for a School represents the principal of that school, and this group agent might be responsible for making decisions such as when to close the school in an emergency. Or, you could define a group agent that represents the moderator of an online social network, who could decide to admit users to the network based on some selection criterion.

Every place that is defined in a FRED model - either by the synthetic population or by you, the modeler - can be assigned a group agent. To activate the group agent for a particular place (in this case the block group) within your simulation, use the following code:

place Block_Group {
    has_group_agent = 1
}
A group agents differs from a regular agent in some important ways: - A group agent has no demographic information - no age, race, or sex. - Their ID is the same as the ID of the place for which they are the group agent. - A group agent is not physically present in the place that they represent.

This last point is important. Group agents are not members of the group they "manage" - they do not physically mix with the members of that group. This means that they do not participate in the transmission process described in Part 7.

Like regular agents, though, they have their own instance of each agent variable, and this is one reason that they are a powerful tool in a simulation. Group agents can be used to store information about the place they represent, and that information can be queried by regular agents during a simulation.

In the model we explore here, the group agents of block groups are used to keep records of several important pieces of information. They are assigned several agent numeric variables that are used to track summary statistics about the population of agents living in their block group at every time step.

8.3 A Closer Look at the Schelling Model

Open the schelling_block.fred file to look at the model.

There is a lot going on in this model! Let's walk through it step by step:

The startup blocks declare the shared variables for the desired similarity fraction (the fraction of similar agents in a block group that will result in a happy agent who doesn't want to move) and the fraction of empty households in the simulation (these households will be available for unhappy agents to move to). It also opens a number of files that will be used to track the output from the simulation.

The agent_startup block then randomly assigns agents one of two colors, so that we have a roughly 50:50 split in the population.

The SETUP_AGENTS condition is used to narrow down the set of agents that will take part in the simulation. We want to work with just a single agent per household, so the model removes any agent that isn't a "householder" using the exit function. By using exit, rather than sending non-householder agents to the Excluded state, we completely remove the agent from the simulation. This saves on memory usage and speeds up the simulation. We also create empty households by then removing a fraction of the householder agents, again using the exit function.

In the DETERMINE_BLOCK_COLOR condition, the group agent of each block group asks all the remaining agents in their block group to report their color using this snippet of code:

        ...ask(members(Block_Group), my_color)...
This information is then used to calculate some summary values (block_blue and block_red, the number of blue and red agents in the block, and frac_blue and frac_red, which record the same information as fractions of the total block group population). The values of these variables are queried later by agents as they assess their happiness. This condition also instructs the group agents to record the block group color information in a file.

The group agents recalculate this information on each day that an agent has left or moved in to their corresponding block group.

The HAPPINESS condition keeps track of whether an agent is happy or not with the level of similarity among their neighbors in their block group. It includes two states where agents query the block group agent for the summary statistics described above to evaluate their happiness: EvaluateHappinessBlue and EvaluateHappinessRed. The query to their block group's group agent is achieved using the following code (and an analogous transition rule for the red color):

        if (ask(Block_Group, frac_blue) >= desired_similarity) then \
            next(Happy)
Agents only assess their happiness when instructed to do so by the group agent of their block group. The group agent only issues this instruction when a regular agent moves into or leaves the block group, and they do so using this line of code in the updateColorFrac state of the DETERMINE_BLOCK_COLOR condition:
        if (has_agent_moved) then \ 
            send(members(Block_Group), HAPPINESS, EvaluateHappiness)
The send function allows one agent to send another agent (or a list of agents) to a specific state in a given condition - in this case, the EvaluateHappiness state in the HAPPINESS condition.

The final condition, MOVE, is where the movement of agents takes place. If an agent is happy, they remain in the Stay state. If an agent is unhappy, they abandon their current household and current block group by quitting those places. They then select an available empty household uniformly at random, remove the selected household from the list of empty households, and "move" there by joining that household and its associated block group. Finally, they update the list of empty households by adding the now-empty household that they just left to the list.

This cycle repeats every day, as long as there is at least one unhappy agent. Let's run the model and take a look at the outputs.

8.4 Running the Model and Visualizing the Results

Execute the cell below to run the model. We have chosen a small county in Wisconsin (Kewaunee County) that borders Lake Michigan as the location for this tutorial simulation, because it runs quickly. (If you explore this model in a different location, be aware that it can take quite a long time to run as the number of agents increases.)

# create a ModelConfig object
schelling_config = ModelConfig(
                   synth_pop=SynthPop("US_2010.v5", ["Kewaunee_County_WI"]),
                   start_date="2022-08-01",
                   end_date="2022-09-01",
               )

# create a Job object using the ModelConfig
schelling_job = Job(
    "schelling_block.fred",
    config=[schelling_config],
    key="schelling_job",
    fred_version="11.0.1",
    results_dir="/home/epx/qsg-results"
)

# call the `Job.execute()` method
schelling_job.execute()

# the following loop idles while we wait for the simulation job to finish
start = time.time()
timeout   = 300 # timeout in seconds
idle_time = 3   # time to wait (in seconds) before checking status again
while str(schelling_job.status) != 'DONE':
    if time.time() > start + timeout:
        msg = f"Job did not finish within {timeout / 60} minutes."
        raise RuntimeError(msg)
    time.sleep(idle_time)

str(schelling_job.status)

The most important output files here are the agent_house_history.csv file, which tracks the household that each agent lives in on each day of the simulation, and the block_color_history.csv file, which keeps a record of the color fractions of each block group on each day.

Run the cell below to tidy up this data set and create a pandas DataFrame that can be used to make maps:

# prep the data for visualization

block_history = schelling_job.results.csv_output("block_color_history.csv")
house_info = schelling_job.results.csv_output("household_info.csv")
agent_info = schelling_job.results.csv_output("agent_info.csv")
agent_house_history = schelling_job.results.csv_output("agent_house_history.csv")
agent_house_history = pd.merge(
    agent_house_history,
    house_info[["household_id","house_lat","house_long"]],
    how="left",on="household_id"
)

If you take a look at the DataFrame produced by the cell above, you'll see all the information that we've gathered during the run of the simulation. Each line of this DataFrame represents the physical location of a given agent on a single day of the simulation:

agent_house_history

The cell below uses a feature of the plotly express visualization package called an "animation frame" to gather data points into timestamped sets and create an animated map. Here, the 'date' column is used to make an animation with one map per day.

Run the cell below to create the map, and then hit the 'play' button in the lower left to run the animation:

# calculate lat, long to pass to mapbox for map center
lat_cen = house_info['house_lat'].median()
long_cen = house_info['house_long'].median()

# set up Epistemix house map tiles
mapstyle="mapbox://styles/epxadmin/cm0ve9m13000501nq8q1zdf5p"
token="pk.eyJ1IjoiZXB4YWRtaW4iLCJhIjoiY20wcmV1azZ6MDhvcTJwcTY2YXpscWsxMSJ9._ROunfMS6hgVh1LPQZ4NGg"

# load the block group boundary file
f = open("Kewaunee_County_WI_blockgroups.geojson")
blocks_json = json.load(f)

# create the figure
fig1 = px.scatter_mapbox(
    agent_house_history, lat="house_lat", lon="house_long",
    color="agent_color",
    color_continuous_scale=[
                (0, "rgba(40, 95, 223, 1)"), # blue
                (1, "rgba(235, 90, 54, 1)") # red
    ],
    animation_frame='date', height=800, zoom=9.5,
    labels={'agent_color':'Agent color'}
)

fig1.update_layout(
    mapbox={'layers': [{
        'source': blocks_json,
        'type': "line", 'below': "traces", 'color': "#6383F3",
        'opacity': 1.0
    }]}
)

fig1.update_layout(mapbox_style=mapstyle, mapbox_accesstoken=token)
fig1.update(layout_coloraxis_showscale=False)
fig1.update_layout(margin={"r":0,"t":0,"l":0,"b":0})
fig1.show()

In the map above, each colored dot represents a householder agent who is either blue or red. Over time, the dots move around as agents who are unhappy seek out a new home in a block group that satisfies the similarity threshold contained in the variable desired_similarity. The solid blue lines on the map show the boundaries of the block groups that are included in this simulation (these boundaries were downloaded from the U.S. Census Bureau's website.)

This animated map shows that after about two simulated weeks, the population has reached a stable equilibrium configuration that is characterized by completely homogeneous block groups - all despite the fact that the population started with a roughly 50:50 color split in each block group and that people would be happy with as many as half of the other agents in their block group not having the same color as them.

8.5 An Alternative Visualization Strategy: Block Group Summary Statistics

The map above visualizes each of the 7393 Kewaunee county householder agents that take part in this simulation at each time step. For more populous places, we may encounter memory issues within this Epistemix IDE environment, if we try to plot all the householder agents.

The model here captures a second set of outputs that can be used to visualize the progress of the simulation using more course-grained information. In the DETERMINE_BLOCK_COLOR condition, the block group agent enters a state called updateColorFracs every 24 hours. In that state, the group agent updates the value of frac_blue and frac_red, which may have changed from the previous day. The group agent then writes out the values of these summary variables to a file.

The resulting file, which is much smaller, can be used to populate a different kind of map - one that visualizes the evolution of the geographical distribution of colors at the block group level, rather than the levelĀ of individual household locations. For that, we are making use of the choropleth_mapbox function in the plotly express package, which produces filled regions representing each block group. The color of these regions corresponds to frac_blue, the fraction of agents in that block group that are blue at the given time step:

# calculate lat, long to pass to mapbox for map center
lat_cen = house_info['house_lat'].median()
long_cen = house_info['house_long'].median()

# set up Epistemix house map tiles
mapstyle="mapbox://styles/epxadmin/cm0ve9m13000501nq8q1zdf5p"
token="pk.eyJ1IjoiZXB4YWRtaW4iLCJhIjoiY20wcmV1azZ6MDhvcTJwcTY2YXpscWsxMSJ9._ROunfMS6hgVh1LPQZ4NGg"

# load the block group boundary file
f = open("Kewaunee_County_WI_blockgroups.geojson")
blocks_json = json.load(f)

# create the figure
fig2 = px.choropleth_mapbox(
    block_history, geojson=blocks_json,
    locations='blockgroup_id', color='frac_blue',
    color_continuous_scale=[  
        # Although blue is represented by 0 in the simulation 
        # (and in the previous plot), in this map, 
        # the color represents the blue fraction of the block group, 
        # so the blue color corresponds to 1 on the scale for this plot.
        (0, "rgba(235, 90, 54, 1)"), # red (frac_blue == 0)
        (1, "rgba(40, 95, 223, 1)"), # blue (frac_blue == 1)
    ],
    range_color=(0, 1),featureidkey="properties.GEOID10",
    hover_data=block_history[["blockgroup_id", "frac_blue", "frac_red"]].columns,
    animation_frame='date',
    zoom=9.5, height=800,
    center = {"lat": lat_cen,"lon": long_cen},
    labels={
            'blockgroup_id': 'Block Group',
            'frac_blue':'Blue fraction', 
            'frac_red':'Red fraction', 
        }
)

fig2.update_layout(mapbox_style=mapstyle, mapbox_accesstoken=token)
fig2.layout.updatemenus[0].buttons[0].args[1]['frame']['duration'] = 300
fig2.update_layout(coloraxis_colorbar_orientation='h')
fig2.update_layout(coloraxis_colorbar_y=-0.1)
fig2.update_layout(margin={"r":0,"t":0,"l":0,"b":0.3})
fig2.show()

This second animation is useful for illustrating the overall outcome of the simulation without visualizing the status of every individual agent. As you can see, you have a great degree of flexibility in deciding what information to capture during a simulation, and group agents can play an important role in creating and recording summary information about a particular place or network.

8.6 Emergent Behavior

The result of this model is striking. Despite starting with an evenly mixed population, and having each agent be happy with as many as half of the other agents in their block group having a different color than them, the rules specified in this simulation lead to an almost completely segregated map in which each block group is almost completely homogenous.

This outcome is another example of emergent behavior. Nothing in the rules that we set up requires that agents only live with similarly-colored agents, but, nevertheless, this is the equilibrium condition that develops as the model unfolds. The ability to see emergent trends in a simulation that are not apparent from the rules followed by the individual agents is a true strength of agent-based modeling.

Furthermore, because of the realistic places and human agents defined in the Epistemix Platform's synthetic population, the output of simulations is immediately more useful for exploring real-world scenarios than the abstract "grid" environments of other agent-based modeling platforms.

Furthermore, the demographic information included in the synthetic population immediately allows you to develop models and tackle problems that depend on real characterisitics of the human population, like age, sex, and race.

All of this information is included in the synthetic population and available for you to use immediately in your models, saving you the effort of having to write code to develop these features yourself.

# deleting our job now that we are done with it
schelling_job.delete(interactive=False)

8.7 Lesson Recap

By inspecting and running the Schelling model, you have encountered features of the FRED modeling language that enable you to:

  1. Use group agents to represent and store information about physical places.
  2. Have regular agents learn information about a particular place by interacting with that place's group agent.
  3. Instruct agents to leave one place and move to another using the quit and join functions.
  4. Remove agents from the simulation using the exit function, which can save on memory and make your model run faster.

You have also seen another example of emergent behavior, in this case that fully-segregated block groups emerge in the simulation, despite the absence of any rule instructing agents to seek out this extreme level of homogeneity in their environments.

By this point in the Quickstart Guide you have seen many features of the FRED modeling language and simulation engine. You have also explored several types of interactions, including interactions between agents themselves and interactions between agents and their environment. In the next lesson, you'll see how you can use control statements to enable agents to perform complex computations as part of the action rules they carry out in a given state. Let's keep going!