Skip to content

Quickstart Guide Part 7: Transmission

In the previous lesson, you explored a model that had agents interact with each other using the ask and tell functions. These functions enable direct interaction between agents, where one agent can determine and change the value of a variable for another agent and thus influence the course of the simulation.

FRED also includes infrastructure for indirect interaction between agents via a mechanism called transmission. In a transmissible condition, one agent can cause another agent to move to a certain state within the condition (with a certain probability) simply by being present in the same place or network. This interaction is indirect in the sense that you do not have to write any specific actions for agents to carry it out - the transmission is handled internally within the FRED simulation engine, as long as the condition is set up properly.

In this lesson, you'll explore the transmission feature of FRED by modeling an outbreak of an influenza-like disease in Jefferson County, PA. Don't worry if you don't have a background in epidemiology - the key details that you need to know will be explained below.

By the end of this lesson, you will be able to: 1. Define a transmissible condition within a FRED model using the transmission_mode keyword. 2. Describe how agents' transmissible and susceptible properties facilitate the spread of a transmissible condition in a FRED simulation. 3. Specify an exposed state that agents are moved to upon successful transmission within a transmissible condition. 4. Use the set_state command to move an agent to a specific state in a different condition. 5. Create plots of the number of agents in each state to visualize the spread of the transmissible condition.

Note that the idea of transmission is not limited to infectious disease. Transmission is useful for many models of social interaction where something - an idea, a belief, an advertisement, or some other piece of information - is spread between people.

Start by importing the packages required to run FRED and create the visualizations:

import epxexec
import epxexec.epxresults
from epxexec.fred_job import fred_job
from epxexec.visual.utils import default_plotly_template

import pandas as pd
import folium
import map_disease

import plotly.express as px
import plotly.io as pio
pio.templates["epistemix"] = default_plotly_template()
pio.templates.default = "epistemix"

7.2 Setting a Condition to be Transmissible

Start by opening the simpleflu.fred file in the left panel.

The model defines a condition named INFLUENZA. At the beginning of the block that defines this condition (before any states are defined), you'll notice some new lines of code:

    transmission_mode = proximity
    transmissibility = 1.0
    exposed_state = Exposed

These keywords are used to set the transmission properties of the condition. The first, transmission_mode, indicates that the condition will be transmissible and specifies how transmission should occur. The value proximity means that agents can transmit the condition by being in the same place - e.g., their household, workplace, or school - at the same time. The default value is none, meaning that the condition is not transmissible.

The transmissibility keyword sets the "contagiousness" of the condition - i.e., the ease with which it can spread between agents. The higher this number, the higher the expected number of contacts (i.e., potential transmission events) when agents are mixing at a given place and time.

The exposed_state indicates the state that agents are moved to upon an exposure, i.e., a "successful" transmission event. We'll discuss this in more detail below.

7.3 Transmissible and Susceptible Agents

Transmissible conditions must also set two new properties for agents - transmissibility and susceptibility. These two properties are linked: if an agent is transmissible for some condition, then they can expose a susceptible agent to whatever is being spread in that condition. In the case of the model you are exploring in this lesson, the thing being spread is the influenza virus.

In a condition named CONDITION, the transmissible and susceptible properties are set by the following keywords:

    CONDITION.trans = 1
    CONDITION.sus = 1

These properties exist for each condition separately - in other words, simulations can model more than one transmissible condition, if desired.

In the influenza model here, the virus is assumed to be novel and, thus, no one in the population has any immunity to it. This means that everyone is susceptible to infection at the start of the simulation. We achieve this in the agent_startup block with the following code:

agent_startup {
    INFLUENZA.sus = 1
}
This sets every regular agent to be susceptible within the INFLUENZA condition, meaning that, if they encounter a transmissible agent during the simulation, they may be moved to the Exposed state (upon "successful" transmission).

Agents can be made to be transmissible in a given condition by setting the trans keyword as follows:

    <some defined condition>.trans = 1
In the influenza model, a special agent called the meta agent introduces the first exposures, by moving a small number of agents to the "exposed" state (called Exposed) from which they subsequently become infectious. More detail on that in the next section.

To summarize, in a transmissible condition, a transmissible agent can expose a susceptible agent to whatever is being spread in the condition. An exposure results in the transition of the susceptible agent to the Exposed state defined for the condition.

7.4 The Exposed State and Introducing Exposures into the Simulation

Every transmissible condition must define an "exposed" state. This is the state that a susceptible agent transitions to upon a successful transmission event from a transmissible agent. The "exposed" state is set as follows at the start of the condition block:

    exposed_state = Exposed
Where do the initial transmissible agents come from? In the influenza model, they are imported by the meta agent. This is one important use of the meta agent in FRED simulations - to import the initial exposures in a transmissible condition.

The event that introduces the virus into this simulation happens in the Import state. Only the meta agent is sent to the Import state, as specified by the following line of code:

    meta_start_state = Import
Once the meta agent enters the Import state at the start of the simulation, it selects 10 agents at random, and moves them to the state Exposed.

    state Import {
        # Action rules
        import_exposures(INFLUENZA, 10)

        # Wait rules
        wait()

        # Transition rules
        default()
    }

In this model, an agent remains in the Exposed state for roughly two days, before moving to one of two infectious states. At that point, the exposed agent's INFLUENZA.trans property is set to be greater than 0 (the precise value depends on whether they are symptomatic), and they can go on to expose other agents to the virus.

state_diagrams.005.png

Note that there is no transition rule that specifies that any agent should transition from Susceptible to Exposed - only the an exposure, either from the meta agent or from contact with an already infectious agent, can cause an agent to enter the Exposed state. Similarly, agents will not enter the Exposed state of the RECORD_EXPOSURES condition except when as a result of the set_state action rule in the INFLUENZA.Exposed condition.

For More Information...

The three prior sections of this lesson introduced various aspects of transmissible conditions in FRED. For a more detailed look at each of these components, see Chapter 13 of the FRED Modeling Language Guide.

7.5 Run the Model and Visualize the State Count Data

Ok, thats a lot of preamble! Let's run the model and inspect the outputs to learn more about what is going on. Execute the cell below to run the model:

%%time
flu_job = fred_job(program="trans_demo.fred")

You just ran your first epidemic simulation! Here's a rundown of what just happened:

  • At the start of the simulation, the meta agent exposed 10 agents to the influenza virus, moving them to the Exposed state.
  • Over the course of the next week or so, those infected agents carried out their daily schedule of activities, spending time at home, at work or school, and interacting with other agents in their census block group - e.g., by visiting other households or businesses.
  • At each place they visited, they mixed with other agents and potentially exposed some of them to influenza.
  • Those new exposed agents reported information about themselves and their exposure to a file at the moment of their exposure.
  • Those newly infected agents then went on to expose other agents in the population as they went about their daily schedules.
  • The virus spread through the population, until it eventually ran out of people to infect, resulting in the end of the outbreak.

Model outputs

One important output that FRED produces is the number of agents in each state per day. You have encountered this output in earlier parts of the Quickstart Guide - for example, in Part 4, where agents slept and watched television.

Tracking the number of agents in each of the disease states is the foundation of epidemiological studies. It conveys information about the progression of the disease in the community, and it informs public health officials about which interventions may be required to bring an outbreak under control.

The code in the next cell retrieves the state count information for each state as a pandas DataFrame:

susceptible = flu_job.get_job_state_table(
    condition="INFLUENZA",
    state="Susceptible",
    count_type="new",
)

exposed = flu_job.get_job_state_table(
    condition="INFLUENZA",
    state="Exposed",
    count_type="new",
)

infectious_s = flu_job.get_job_state_table(
    condition="INFLUENZA",
    state="InfectiousSymptomatic",
    count_type="new",
)

infectious_a = flu_job.get_job_state_table(
    condition="INFLUENZA",
    state="InfectiousAsymptomatic",
    count_type="new",
)

recovered = flu_job.get_job_state_table(
    condition="INFLUENZA",
    state="Recovered",
    count_type="new",
)

Take a quick look at one of these DataFrames to see what data is included:

infectious_s

In the DataFrame above, you have the run number (equal to 1 in all cases, since we only ran a single simulation), the sim_day, which starts with the value 1 on the first day of the simulation, and a column named new. This gives the number of agents that entered that state on that simulation day.

The next cell does some merging and tidying up of these individual DataFrames to create a single DataFrame that contains the data for all of the states:

# rename the 'new' counts column to the name of the state
susceptible.rename(columns={'new':'Susceptible'},inplace=True)
exposed.rename(columns={'new':'Exposed'},inplace=True)
infectious_s.rename(columns={'new':'InfectiousS'},inplace=True)
infectious_a.rename(columns={'new':'InfectiousA'},inplace=True)
recovered.rename(columns={'new':'Recovered'},inplace=True)

# build up the merged DataFrame with counts for all states
all_states = pd.merge(susceptible,exposed)
all_states = pd.merge(all_states,infectious_s)
all_states = pd.merge(all_states,infectious_a)
all_states = pd.merge(all_states,recovered)

#retrieve the sim_date from the column and add to the all_states frame.
all_states['sim_date']=flu_job.get_job_date_table().sim_date
all_states

As you can see, the all_states DataFrame now contains the number of agents entering each state on each day of the simulation. We've also added the calendar date as a column, so we can plot real dates.

We can now use the plotly package to create a figure with the so-called "epi-curves" for this outbreak:

fig = px.line(
    all_states,
    x='sim_date',
    # pass a list to plot all states
    y=[
        'Exposed',
        'InfectiousS',
        'InfectiousA',
        'Recovered',
    ],
    title="The Evolution of an Influenza Epidemic"
)
# customize font and legend orientation & position
fig.update_layout(
    font_family="Epistemix Label",
    yaxis_title="Number of agents entering the state",
    xaxis_title="Date",
    legend_title="State",
    title_font_size=30
)
fig.show()

In this simple influenza model, agents recover from the disease and then have complete immunity, so they are not able to be reinfected if they encounter a transmissible agent later on. That results in this epidemic having a single peak, because the number of susceptible agents is constantly decreasing.

The blue curve represents the exposed agents. Red and green show the infectious agents who are either symptomatic or asymptomatic. Note that the relative scaling of these curves is indicative of the one third/two thirds split in the probability that exposed agents become asymptomatic or symptomatic: there are roughly twice as many symptomatic agents every day.

The purple curve shows the recovered population. Notice the lag in the timing of the peak, which occurs several days later than the peak of the exposed curve, it takes some time (but not forever) for an infected agent to recover.

7.5 Including Multiple Conditions via Separate Model Files

You may have noticed that when we ran the model above, we did not specify the simpleflu.fred file when we ran fred_job. We used the trans_demo.fred file to initiate the simulation instead. Open that file now to take a look.

You'll see the familiar simulation block, but, in this case, it is followed by two lines of code that use a new command - include:

include simpleflu.fred
include record_exposures.fred

In all the multi-condition models you have run up to this point of the Quickstart Guide, the different conditions have all resided in a single model file. Here, we have broken them out into two different files, simpleflu.fred, which defines the disease condition for this simulation, and record_exposures.fred, which instructs agents to record the location where they were exposed to influenza, along with some demographic information, in a file.

The include statements in trans_demo.fred tell FRED to load the conditions described in these two files and include them in the simulation. It can be useful to break conditions out into individual files like this. First, it helps keep model files from getting too long. Second, it makes it easy to turn a condition on or off, by removing or commenting out the include statement for that condition.

Cross-talk between conditions

A key feature in FRED is the ability for an action in one condition to transition an agent to a new state in another condition. You can see an example of this happening on line 45 of the simpleflu.fred file:

       set_state(RECORD_EXPOSURES, Exposed)
This command instructs the FRED simulation engine to move the agent that has just entered the Exposed state in the INFLUENZA condition into the Exposed state in the RECORD_EXPOSURES condition. Once in that new state, the agent then makes use of some additional functions related to transmission to determine where they were exposed.

Each of these functions queries records of the agent's exposure in the INFLUENZA condition, representing another case of cross-talk between conditions. The was_exposed_externally function determines whether or not the agent was exposed by the meta agent at the start of the simulation. If this function returns false, then the agent uses the was_exposed_in function to determine if they were exposed in one of the places that they regularly attend, like their household or workplace.

Once the agent determines where they were exposed, they obtain the latitude and longitude of that location (as we did in Part 5 of the Quickstart Guide) and write it to a CSV file, along with some demographic information.

7.6 Epi-Curves with Demographic Information

Let's start by reading the contents of the CSV file produced by the RECORD_EXPOSURES condition. The next cell executes some code to create a nicely formatted DataFrame from the CSV file. Then, it plots the number of agents that entered the Exposed state each day:

explocs = map_disease.reformat_expcsv(flu_job,"exposure_locs.csv")
fig = px.histogram(
    explocs, 
    x="expdate",
    labels={"expdate": "Date"},
    title="Exposures per Day"
)

fig.update_layout( # customize font and legend orientation & position
    yaxis_title="Number of exposures",
    title_font_size=30,
    bargap=0.1
)
fig.show()

This graph can be compared directly to the Exposed state counts we graphed earlier in this lesson, drawn from the all_states DataFrame that we created:

fig = px.bar(
    all_states,
    x='sim_date',
    y='Exposed',
    # changing date range to match histogram
    range_x=['2023-01-01','2023-04-15'],
    title="Exposures per Day",
)
fig.update_layout(
    yaxis_title="Number of exposures",
    xaxis_title="Date",
    legend_title="State",
    bargap=0.1,
    title_font_size=30,
)
fig.show()

As you can see, the curves look the exactly same over the date range of interest. However, we aren't making use of all the data available to us when making the first chart. Remember, every agent also recorded their demographic information when recording their exposure information. Thus, we can modify the code that made the first plot to dis-aggregate the exposures according to the race of the exposed agent:

fig = px.histogram(
    explocs, 
    x="expdate",
    # Added color keyword to group by agent race
    color='race',
    labels={"expdate": "Date"},
    title="Exposures per Day by Race",
)

# customize font and legend orientation & position
fig.update_layout(
    yaxis_title="Number of exposures",
    title_font_size=30,
    bargap=0.1,
)

fig.show()

Now, we have a stacked histogram, where the number of exposures for each race is indicated by a unique color. This information is not included in the base state counts in FRED. But, by defining an additional condition in which agents record information in a file when they are exposed, we can access additional dimensions of data to use in subsequent analyses.

7.7 Mapping Disease Spread

The last thing we'll do with the data set from this simulation is make a map to show how influenza spread in Jefferson County during this outbreak. This type of information is uniquely accessible in agent-based modeling - other strategies for modeling disease spread tend not capture this geographic detail.

Execute the code below to see the visualization. The first line of code downsamples the exposure location data to create a manageable sub-sample for this web-based map visualization. The second and third lines transform the latitude, longitude, and date into a format required to make the animated map. The last line runs some code that actually makes the map. You can examine these functions in the map_disease.py file in this directory, if you are interested in the details of how they work.

sample_df=map_disease.resample_dataframe(explocs,8000)
exp_geojson = map_disease.create_geojson(
    map_disease.create_geojson_features_basic(sample_df)
)
map_disease.fred_map_movie(exp_geojson)

Each time step of this animation respresents a day in the simulation, and each red dot represents the location of an agent exposed to influenza. These maps will look familiar if you've ever watched a movie with an epidemic, like Outbreak or Contagion. You can zoom in and out to see what is happening at different scales, from a single neighborhood to the whole county.

The spatial information captured here is incredibly useful for epidemiologists and public health policy makers. Maps like these can help indicate where clusters of disease might form, which can in turn help decision makers determine the best locations to roll out interventions like vaccines or stay-at-home orders.

7.8 Lesson Recap

You've covered a lot in this tutorial! The model you explored here gives a sense of the kind of complex social interactions that can be studied with FRED simulations. By running this model of influenza, you have encountered some important features of FRED and strategies for creating rich models with useful data outputs.

  1. You saw how a condition can be set to be transmissible, so that transmissible agents can expose susceptible agents to the property of interest being tracked in the simulation - in this case, the influenza virus.
  2. You encountered the idea of an "exposed" state, where susceptible agents are moved if a successful transmission takes place during the simulation.
  3. You plotted "epi-curves" to trace the progression of a flu epidemic. These plots display the number of agents entering each state of the flu model on each day.
  4. You included multiple conditions, which were written in separate files, in a single simulation using include statements.
  5. You used several functions that allow for cross-talk between states.
  6. You made use of the print_csv function in a separate state to record additional information about exposed agents in a file. Then, you used the resulting data to produce more complex visualizations of the evolution of this flu epidemic.

In the next lesson, you'll encounter another complex model that demonstrates how agents can interact with their environments. In doing so, you'll see another example of so-called emergent behavior, which agent-based modeling (with FRED) is uniquely suited to studying. Let's keep going!

7.9 Additional Exercises

  1. Try adjusting the wait times for different stages of the SEIR influenza model.
    How do these changes affect the epi-curves produces by the model?

  2. Now try adjusting the INFLUENZA.trans values for the symptomatic and asymptomatic states.
    How do the effects compare to changing the infection period?

  3. Adapt the model so that, after a certain period of time, agents lose their immunity and return to the Susceptible state. Is there a maximum time scale for immunity loss that shows up in the epi-curves?