Skip to content

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 simulation can specify multiple conditions. For now, this model contains just one condition - we will add a second condition in a later lesson.

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:

    state Asleep {
        # Action rules

        # Wait rules
        wait()
        # wait(until(7am))

        # Transition rules
        default()
    }
States contain three kinds of rules. Action rules specify actions that agents will carry out when they enter the state. Examples include updating the value of a variable that is being tracked in the simulation, asking another agent a question, or writing to an output file. These are just a few examples - you'll explore many kinds of action rules in the coming lessons. The 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.

alive_job.results.state(
    condition  = "ALIVE", 
    state      = "Asleep", 
    count_type = "cumulative"
)

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.

alive_job.results.state(
    condition  = "ALIVE", 
    state      = "Asleep", 
    count_type = "new"
)

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:

alive_job.results.dates()

Note that all of the tables we have displayed here are pandas DataFrames:

type(alive_job.results.dates())

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:

asleep_new['sim_date'] = asleep_dates['sim_date']
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:

bar_graph = asleep_new.plot(x='sim_date',y='new',kind='bar')

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.

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

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:

# Wait rules
# wait()
wait(until(7am))

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:

# Transition rules
default(Awake)
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:

    state Awake {
        # Action rules

        # Wait rules
        wait(until(10pm))

        # Transition rules
        default(Asleep)
    }

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.

alive.png

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.

alive_2s_job.results.state(
                 condition  = "ALIVE", 
                 state      = "Asleep", 
                 count_type = "new"
             )

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:

alive_2s_job.results.state(
                 condition  = "ALIVE", 
                 state      = "Awake", 
                 count_type = "new"
             )

Again, we see that all the agents enter the Awake state each day.

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

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!

  1. How does changing the start_state to Awake change the simulation output?

  2. What happens if you do not specify a transition rule in a state (e.g., write default() without an argument)?

  3. Rewrite the Awake state to use a duration, rather than an until statement.

  4. Change the wait rule of the Asleep state, so that the duration of time that each agent spends in the Asleep 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.

  1. 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.

  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.

  1. until() vs Duration in wait() rules

Agents enter the Awakestate at 7am and wait until 10pm before transitioning back to the Asleep state. An equivalent formulation of this wait rule would be as follows:

    state Awake {
        # Action rules

        # Wait rules
        wait(15)

        # Transition rules
        default(Asleep)
    }
The FRED simulation engine assumes that the unit of the numerical input to the 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.

  1. 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.