Quickstart Guide Part 2: Introduction to Conditions and States¶
The last lesson demonstrated how to use Python tools to simulate a FRED model and examine some of the output, but it was very boring. Our simulation simply instantiated a specified synthetic population of agents and that was it. The agents didn't actually do anything!
In this lesson, you'll see how to use conditions and their related states to create the rules that govern how agents behave during a simulation.
By the end of this notebook, you should be able to:
1. Identify condition
and state
blocks within FRED models.
2. Know that agents can be described by one or more conditions at any time step in the simulation.
3. Describe the difference between action, wait, and transition rules within a state.
4. Use Python tools to examine the daily counts of agents in each state in each condition.
As you did last time, execute the cell below by clicking inside it and pressing the Shift
and Return
keys at the same time:
# import some required Python packages
import pandas as pd
import matplotlib.pyplot as plt
import time
# Epistemix package for managing simulations and simulation results in Python
from epx import Job, ModelConfig, SynthPop
2.1 What is a Condition?¶
A condition is a set of related characteristics, rules, and behaviors that agents are subject to during a FRED simulation. Each of these component parts are defined within states. Conditions track aspects of individuals and of the population that are of interest to your research question or business need.
Conditions can be simple or complex depending on the aspect of an agent you are trying to describe. Suppose you run a straight-to-consumer sales business, and you want to model the impact of a marketing campaign on your customers' happiness with your product. Your model could include a simple condition called SATISFACTION
that is described by two states, Satisfied
and Unsatisfied
, which represent the possible customer outcomes after your marketing campaign.
Medical conditions can also be described using conditions in FRED simulations. Your model can include states within that condition that can represent aspects of that medical condition, like spread or treatment. A model of COVID-19, for example, could define the familiar Susceptible
, Exposed
, Infectious
, and Recovered
states of an SEIR compartmental model to describe each agent's current status. A model of diabetes could define the treatment status of agents through a series of states called Untreated
, Treated_with_Diet
, and Treated_with_Insulin
.
Lastly, conditions can instruct agents to take an action, like make a decision, ask another agent a question, or record some information in an output file.
The FRED modeling language offers up a flexible framework for exploring problems through conditions and states. Defining the condition or set of conditions that you want to explore is the most important aspect of agent-based modeling work in the FRED modeling language!
More examples of these concepts will be introduced over the next few lessons. Here, you'll focus on how to define a condition and its component states.
Condition blocks¶
Begin by opening the model file you'll use in this lesson in a text editor tab - click the alive.fred
file in the panel to the left.
From Part 1, you will recognize the comment
block that documents the model. But this model has an additional block: a condition named ALIVE
.
condition ALIVE {
start_state = Asleep
state Asleep {
# Action rules
# Wait rules
wait()
# wait(until(7am))
# Transition rules
default()
}
}
A condition is described by one or more states. This condition currently has one state, Asleep
. The condition also defines a start_state
. This is the state within this condition that agents will be placed in when the simulation starts. Moreover, when a simulation contains more than one condition, agents will be placed into each condition's start_state
at the beginning of the simulation.
A full description of the condition
block and its properties can be found here.
State blocks¶
Take a closer look at the state
block:
Asleep
state defined here has no action rules - agents will take no action when they enter this state.
Wait rules tell agents how long to remain in a state before transitioning to another state within the condition. At the heart of these rules is the wait
instruction (documented here). If this state is a terminal state, wait()
with no argument inside the parentheses tells the agent to wait in the state indefinitely. A wait rule of wait(0)
tells the agent to immediately transition to the next state. Passing a positive value x
to wait
(i.e., wait(x)
) instructs the agent to wait x
hours (where x
is rounded to the nearest integer) before transitioning to the next state.
Lastly, transition rules specify what state agents should transition to next after the specified waiting period. There are two expressions for specifying transition rules: default()
and next()
. In states that transition to just one possible next state, we use the default(state_name)
transition. This rule dictates that all active agents will transition to the state called state_name
after any specified action rules have been carried out and the specified wait period has elapsed.
The next()
rule is used when multiple transitions are possible. It is generally accompanied by a predicate (a rule that selects agents according to some criterion). You'll see an example of next()
in action in a later lesson.
The default()
transition rule with no arguments can be paired with wait()
to indicate a terminal state - this pair of rules together instructs agents to wait forever in the current state. A word of caution, though - if an argument is passed to the wait()
command (including wait(0)
), use of default()
with no argument can lead to strange behavior, like infinite loops.
If the state is not a terminal state, a default transition state must be identified.
An important note about conditions and states¶
Agents can only be in one state per condition at any given time. In this model, we have only defined one condition. But, in models with multiple conditions, this concept will become more important. You'll explore this aspect of FRED in more detail in Part 4 of this guide.
2.2 Running the "Alive" Simulation¶
Now that we've explored the construction of conditions and their component states, let's run this model. As we did last time, we we will run the model in Loving County, TX for speed and for compactness of output.
As we saw in the previous lesson, to run a simulation, we use the tools from the epx package to:
1. Create a ModelConfig object.
2. Create a Job object using the ModelConfig.
3. Call the Job.execute()
method.
# create a ModelConfig object
alive_config = ModelConfig(
synth_pop=SynthPop("US_2010.v5", ["Loving_County_TX"]),
start_date="2022-05-10",
end_date="2022-05-17",
)
# create a Job object using the ModelConfig
alive_job = Job(
"alive.fred",
config=[alive_config],
key="alive_job",
fred_version="11.0.1",
results_dir="/home/epx/qsg-results"
)
# call the `Job.execute()` method
alive_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(alive_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(alive_job.status)
2.3 Examining the Simulation Output¶
Recall that in Part 1, we noted that the following code would return an error, since our minimal simulation model defined no conditions (and thus no states):
# Note: Calling this method would return an error.
minimal_job.results.state(
condition = "CONDITION",
state = "State",
count_type = "cumulative"
)
In a model with conditions and states, though - like the one in alive.fred
- we can use the JobResults.state()
method to examine the daily counts of agents in each state of a condition. Passing the argument count_type="cumulative"
means that the daily counts returned will be the number of times any agent has entered the state since the beginning of the simulation, reported at the end of each simulated day.
This view can be useful, for example, if you want to know the total number of people that were infected by a disease during a pandemic or the number of people who defaulted on a loan during a fiscal quarter.
We can also take a look at the number of agents who enter the state at any time during each simulated day (or, sim_day
), by setting count_type = "new"
. This view is useful for tracking the ongoing, changing behavior of a simulation.
You can see here that all 70 of our Loving County residents are instantiated in the simulation in the state Asleep
. Because we have set wait()
, the agents will remain in that state indefinitely; all 70 agents are in that state on each day of the simulation. Thus, no agents enter that state on any other simulated day.
JobResults objects also have a method, JobResults.dates()
, that maps each simulated day to a calendar date:
Note that all of the tables we have displayed here are pandas DataFrames:
As a result, these objects, can be manipulated using all of the tools available in the pandas library, which we loaded at the beginning of the lesson.
Run the next cell to create variables that contain the DataFrames returned by these methods:
asleep_new = alive_job.results.state(
condition = "ALIVE",
state = "Asleep",
count_type = "new"
)
asleep_dates = alive_job.results.dates()
We can add the sim_date
column from the asleep_dates
DataFrame to asleep_new
:
We can then use the built-in plot()
method of a pandas DataFrame to make a bar graph of the number of new agents entering the Asleep
state each day:
Of course, the resulting plot isn't very interesting. We have only one state defined and all agents remain in that state for the entirety of the simulation. Let's make our simulation a little more interesting by adding a second state to our ALIVE
condition that represents the agents being awake.
2.4 Adding a Second State to the ALIVE Condition¶
First, let's alter the wait rule in the Asleep
state so that agents don't remain asleep indefinitely. In the alive.fred
file, comment out the wait()
command on line 14 by adding a #
symbol to the start of the line.
Then uncomment the wait(until(7am))
command on line 15 by deleting the #
symbol at the start of the line.
Your "Wait rules" section should now look like this:
We also need to change our transition rule to specify that agents will move to the Awake
state at 7am.
In the alive.fred
file, modify the default()
command on line 18 to read default(Awake)
.
Your transition rule section should now look like this:
Save your changes by selecting 'Save File' in the File menu in the upper left of the JupyterLab browser tab (above the file browser).Now let's add our new Awake
state. Cut and paste the following text into the ALIVE
condition, below the Asleep
state:
Let's take a second to assess what these two states will do in tandem. Simulations of a FRED model begin at 12am on the start_date
specified in the model's configuration (i.e., in the ModelConfig object). Upon starting, all of the agents are immediately placed in the Asleep
state. They will stay in that state until the internal simulation clock reads 7am (each time step in a FRED simulation is one hour).
At 7am, all agents will transition into the Awake
state. They will then stay in that state until the internal clock reads 10pm, at which time they will transition to the Asleep
state. The cycle will then repeat until the clock reads 11pm on the specified end_date
.
Here is a diagram that describes the movement of agents through the updated ALIVE
condition. Condition/state diagrams can be a powerful tool for understanding (and explaining) agent-based models.
Let's verify that the model works as expected by looking at the state counts recorded when we run the simulation.
2.5 Running the 2-state model¶
We can run this new version of the model using the same steps we used back at the start of the lesson:
# create a ModelConfig object
alive_config = ModelConfig(
synth_pop=SynthPop("US_2010.v5", ["Loving_County_TX"]),
start_date="2022-05-10",
end_date="2022-05-17",
)
# create a Job object using the ModelConfig
alive_2s_job = Job(
"alive.fred",
config=[alive_config],
key="alive_2s_job",
fred_version="11.0.1",
results_dir="/home/epx/qsg-results"
)
# call the `Job.execute()` method
alive_2s_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(alive_2s_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(alive_2s_job.status)
First, take a look at the table of new agent counts for the Asleep
state using the state
method of the JobResults object associated with our Job object.
We can see here that all the agents now re-enter this state each simulated day. And on the first day, they enter the state twice: first at 12am, when the simulation starts, and then again at 10pm.
Now, let's look at the same information for the Awake
state:
Again, we see that all the agents enter the Awake
state each day.
2.6 Lesson Recap¶
In this lesson we introduced the concept of a condition, the basic building block of a FRED model.
1. A condition is a set of related rules, behaviors, and actions that agents are subject to during a FRED simulation.
2. Conditions are comprised of states that describe these rules, behaviors, and actions.
3. States are described by three kinds of rules: action rules, wait rules, and transition rules.
3. Agents inhabit exactly one state in each of a model's conditions at each time step of the simulation.
4. JobResults objects created by tools from the epx package (and accessed using the Job.results
attribute of a Job object) have several built-in methods that allow the user to examine the output from a simulation execution. This includes, for example, the ability to view different ways of counting the number of agents in a given state over the course of the simulation.
In the next lesson, we will write a condition that allow us to explore some properties of the agents that make up the Epistemix synthetic population. We'll also make our first interesting visualizations!
2.7 Additional Exercises¶
If you'd like to go ahead and start experimenting with the FRED code included in this lesson, try the exercises below!
-
How does changing the
start_state
toAwake
change the simulation output? -
What happens if you do not specify a transition rule in a state (e.g., write
default()
without an argument)? -
Rewrite the
Awake
state to use a duration, rather than anuntil
statement. -
Change the wait rule of the
Asleep
state, so that the duration of time that each agent spends in theAsleep
state is a random variable whose value is drawn from a statistical distribution function. You can read more about the statistical distribution functions available in the FRED modeling language in the language reference. For example, this page details how to draw random variables from a normal distribution and contains links to the other statistical distribution functions.
Exercise Solutions¶
Expand the following blocks to see solutions to each of the additional exercises.
start_state
It is important to remember that simulations of FRED models begin at 12am on the specified start_date
. In this example, having agents begin in the Awake
state means they only enter the Asleep
state once on Day 1.
default()
behavior
If the default()
function is invoked without an argument, agents will transition back to their current state and move through it once again.
until()
vs Duration inwait()
rules
Agents enter the Awake
state at 7am and wait until 10pm before transitioning back to the Asleep
state. An equivalent formulation of this wait rule would be as follows:
wait()
function is hours.
Note that if we used a duration for our Asleep
state, we would need to take into account the fact that our simulation begins at 12am, not 10pm! Often we will address issues like this with a start_state
that is separate from the core behavior loop. In this example, we could create a separate start_state
where agents wait 7 hours, transition to Awake
and then begin moving between Awake
and Asleep
as desired.
- Adding randomness to agent behavior
Here is a version of the ALIVE
condition that randomly selects the duration of each agent's sleep time every night.
condition ALIVE {
start_state = Start
state Start {
# Action rules
# Wait rules
wait(until(7am))
# Transition rules
default(Awake)
}
state Awake {
# Action rules
# Wait rules
wait(until(10pm))
# Transition rules
default(Asleep)
}
state Asleep {
# Action rules
# Wait rules
wait(normal(8, 1))
# Transition rules
default(Awake)
}
}
Here we assume that each agent goes to bed at 10pm and then draws the duration of their sleep from a normal distribution with mean of 8 hours and a standard deviation of 1 hour.