Quickstart Guide Part 9: Control Structures¶
The FRED modeling language defines a set of control structures that can be used to define complex rules for the agents to follow. You have already seen the if
statement in action, as we have used alongside predicate statements to influence the way agents transition between states.
The FRED modeling language also includes both while
and for
loop structures. These allow for more complex actions to be taken by agents within a given state. They also facilitate more efficient interactions with list, table, and list_table variables.
In this tutorial, you will explore a model of a lottery taking place in Jefferson County, PA. This model makes use of all three kinds of control structures, sometimes in concert, to have agents pick numbers and to announce any winners.
By the end of this lesson, you should be able to:
- Describe the concept of the meta agent, and use it to perform actions within a simulation.
- Use
if
statements andfor
andwhile
loops within your model to control agent actions within a given state. - Have agents report information to the standard output of a running FRED model using the
print()
function, retrieve that print output using theJobResults.print_output()
method, and format it to render in an easy-to-read layout directly in the Jupyter notebook. - Pass the
n_reps
andseed
parameters when instantiating aModelConfig
object to execute multiple simulations of the same model with different random seed numbers.
As always, let's start by loading the packages we need to run the model.
import time
# Epistemix package for managing simulations and simulation results in Python
from epx import Job, ModelConfig, SynthPop
9.1 The Meta Agent¶
You have actually encountered the meta agent in several earlier parts of this Quickstart Guide, but without much explanation. There is a single meta agent for each simulation. The meta agent can take actions that affect the overall simulation, and, just like ordinary agents and group agents, the behavior of the meta agent is controlled by defining conditions, states, and rules.
The meta agent can execute several special actions that other agents cannot. For example, the meta agent can start disease outbreaks by infecting a given number of individuals. This may represent, for example, transmission to agents from a source that is not explicitly included in the simulation model. You saw this in action in Part 7, when the meta agent imported the first exposures to the influenza virus into the population of ordinary agents.
And, in any model you have encountered so far that has included a startup
block to open a file or set the value of a shared variable, it was the meta agent that carried out these actions. (Actions to be carried out by group and regular agents at startup time are specified using alternative blocks: group_startup
and agent_startup
, respectively.
By default, the meta agent is sent to the Excluded state of each condition after any actions specified in a startup
block are completed. (Similarly, if there are no startup
blocks, the meta agent is sent to the Excluded state of each condition by default at the start of the simulation.) The meta_start_state
command can be used to specify a different start state for the meta agent. You will see an example of this in this lesson's lottery model.
9.2 The Steps of the Lottery Model¶
Open the lottery.fred
file by clicking on the file in the browser to the left. Here is a walkthrough of what happens in the states defined in the model's LOTTERY
condition:
- The meta agent specifies the number of draws that each agent will make to play the lottery in the
startup
block and sets up a list_table that winners will use to record their IDs. - Ordinary agents start the simulation by choosing their lottery numbers. They then wait for the meta agent to choose and announce the winning numbers.
- Once the meta agent chooses and announces the winning numbers, the ordinary agents check their picks against the winning set. Agents with 4 or 5 matches are considered winners.
- Winning agents record their IDs in a list corresponding to their number of matches. These lists are stored in a list_table called
matches
, so that any winner can easily access the list corresponding to their number of matches. - The meta agent then announces the IDs of the winning agents.
Throughout this model, agents are instructed to carry out complex tasks using the FRED modeling language's built-in control structures. Sometimes, these tasks could actually be accomplished in a single line of code using a built-in function. But, by exploring how to use control structures to build up these behaviors from scratch, you'll see how you can use these language features in your own models to more easily define custom agent behaviors.
9.3 Control Structures in the FRED Modeling Language¶
FRED defines three types of control structures: for
loops, while
loops, andif
statements.
Loops are intended to be used as part of the action rules within a given state. They enable agents to repeat a series of action rules, perhaps with different behavior in certain iterations.
For example, agents can make use of for
loops to read a list and process the entries one at a time, possibly making different decisions depending on the values that they encounter. And while
loops can be used to specify that an agent continue doing something inside a state until a specified condition is met, like creating a list of 5 different numbers or selecting an agent of the opposite sex from among the members of a place where they themself are a member.
The if
statements in the FRED modeling language can be used with both action and transition rules. You have already encountered the use of if
within predicates that determine which agents move to which states. You'll see here that if
statements can also be used to tell agents to take different actions, depending on the outcome of evaluating a predicate.
Altogether, these control structures enable quite complex agent behavior during a simulation. Here is some more detail about each structure:
for
¶
for
loops are used in several places in the lottery model to read from or write to list variables. The general structure of a for
loop in the FRED modeling language is as follows:
variables {
# declare the iterator variable to use in the for loop
shared numeric item
...
}
# define the action rules in the for loop
for (item, list-expression) do {
action1
action2
...
}
for
loop, the list expression specified by the second argument in the parentheses is traversed one step at a time. In each step, the entry in the list corresponding to the current step (i.e., the first entry for the first step, the second entry for the second step, and so on) is made available as the variable called item
(or whatever name you choose for the first argument variable) and the action rules specified in the curly braces after the keyword do
are carried out. Note that the variable item
must be declared as a shared numeric
in the variables
block of the model, or else the for
loop cannot execute properly.
You can see an example of a for
loop in the ChooseWinningNumbers
state in the lottery model. This is executed by the meta agent after the regular agents have each picked a set of numbers.
print("The winning numbers are:")
for (item, winning_lottery_numbers) do {
print("Number: ", item)
}
winning_lottery_numbers
list. For each entry in the list, the value of that entry is printed after the prefix "Number: ". There are other examples of for
loops in the LOTTERY
condition that are used in conjunction with other control structures.
As you use for
loops, don't forget that you must declare the variable that you use to access the value of the item retrieved from the list in the variables
block. We recommend adding a comment after the variable declaration to denote that the variable will be used as an "iterator" variable. When an iterator variable will be used in one loop, you can give it an informative name that makes sense in the context of that loop to improve the readability of your model code. For example, if you are traversing a list called prices, you may want to call the varible price
so the loop can be read as "for price
in prices
do..."
When your iterator variable will be re-used in many loops, it may be better to give it a generic name like "item" - this can make it easy to remember (for you or for someone else reading your code) that a particular variable is an iterator variable.
while
¶
A while
loop has a similar structure to a for
loop. But, rather than repeating a series of actions until the end of a list is reached, the actions specified in a while
loop will continue to take place while a certain predicate statement remains true:
# declare the variable to use in the while loop condition
variables {
shared numeric x
...
}
# set up the while loop
while (predicate involving x) do {
action1
action2
...
}
x
, which may be modified in the loop.
The LOTTERY
condition makes use of a while
loop in the ChooseWinningNumbers
state that the meta agent enters 24 hours after the start of the simulation. In fact, the while
loop is executed inside a for
loop, as follows:
for (item, range(number_of_picks)) do {
pick = floor(uniform(0, 40))
while (is_in_list(winning_lottery_numbers, pick)) do {
pick = floor(uniform(0, 40))
}
winning_lottery_numbers[item] = pick
}
winning_lottery_numbers
list with 5 different numbers. The range(number_of_picks)
instruction in the outer for
loop invokes a list of sequential integers of length specified by the number_of_picks
variable.
During each step of the for
loop, the meta agent then chooses a number between 0 and 39 using the uniform(0, 40)
function. The uniform
function draws a single real number uniformly at random from the range between the specifed lower and upper values, in this case 0 and 40. Using the floor
function to convert from a real number to an integer ensures that each number has an equal probability to be chosen.
The while
loop is then used to check whether the number has already been chosen or not. If the number is already in the list, the agent will draw again (and will keep doing so until a new number is found). If the number is not in the list, the agent records it in the winning_lottery_numbers
list at the index specified by the outer for
loop. The result of these nested loop actions is a list of winning numbers of length number_of_picks
, where every element is a unique value between 0 and 39.
You might have noticed that when the regular agents in the model pick their numbers in the ChooseNumbers
state, they don't use a while
statement. That's because the FRED modeling language includes a function to carry out the exact same action: sample_without_replacement
. This function can be used to select a specified number of items from a list and ensure that the same number isn't drawn twice. It also has an advantage over the while
loop method, in that it can be written and executed as a single line of code. Here is how regular agents pick their numbers in the ChooseNumbers
state:
while
loop. These loops, which often cannot be readily replaced by built-in functions, are useful in all sorts of situations when building models.
if
¶
You have encountered if
statements in the context of transition rules in previous lessons, where you made use of predicates to direct agents to move to a new state based on some property. For example, the following lines of code tell female agents to transition to a state called Pregnant
and male agents to transition to the Excluded state.
if
statements can also be used with action rules, either by including a single action rule after then
, or by opening a pair of curly braces after and placing a set of action rules inside. Here are two examples:
if (age() == 18) then print("Can't wait to vote in the next election!")
if (age() == 18) then {
print("Can't wait to vote in the next election!")
number_new_voters = number_new_voters + 1
}
The second if
statement instructs the agent to print the statement and then incremement a counter variable named number_new_voters
by 1. The curly braces are necessary in the second version to group the action rules together and ensure that both statements are carried out by agents if and only if they meet the age requirement defined by the predicate.
You can see several examples of if
statements in the LOTTERY
condition. For example, they are used in the final AnnounceWinners
state to allow the meta agent to determine which (grammatically correct!) statements to output when announcing the winners at the end of the simulation.
9.4 The print
Function¶
This model makes use of the print
function built into the FRED modeling language to create unstructured output that can be formatted and displayed directly beneath a Jupyter cell. Creating output in this way can be very useful during the model development process, as it allows you to test whether model is doing what you expect in a particular step.
For example, you could include print
statements that are executed before and after an agent alters a variable, to make sure that the variable was modified in the way you expect. Or, you can have agents report their ID and other agent variables using a print
statement to make sure that predicates are selecting agents in the way that you want during the course of a simulation.
Another helpful test is to have agents print
their ID when they enter a particular state. This can help you troubleshoot your wait and transition rules, if you get an unexpectedly low or high number of agents in a particular state.
A word of caution, though: Printing large amounts of information will use up memory in your notebook, so be judicious in your use of the print
function!
9.5 Run the Model and Examine the Outputs¶
Execute the cell below to run the lottery model. Then, execute the subsequent cell to retrieve and format the simulation output and see exactly which agents in Jefferson County, PA won the lottery!
# create a ModelConfig object
lottery_config = ModelConfig(
synth_pop=SynthPop("US_2010.v5", ["Jefferson_County_PA"]),
start_date="2022-09-20",
end_date="2022-09-23",
seed=6887
)
# create a Job object using the ModelConfig
lottery_job = Job(
"lottery.fred",
config=[lottery_config],
key="lottery_job",
fred_version="11.1.1",
results_dir="/home/epx/qsg-results"
)
# call the `Job.execute()` method
lottery_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(lottery_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(lottery_job.status)
The output from the model that is created by the meta agent is shown between the lines of equal signs. In this simulated lottery, nine agents won by matching 4 numbers, but no agents matched 5. Furthermore, you should find that no matter how many times you run the cell above, the outcome is always the same.
Why is this? The FRED simulation engine is set up to enable reproducible results. Every simulation - via a random seed - fixes the (pseudo)random numbers that determine the outcome of any probabilistic rules encountered during a simulation run. Given the seed, the simulation engine creates identical initial conditions for the model. In other words, if you run the same model with the same seed multiple times, you should always get the same output.
This is where the concept of a "run" becomes important. Recall that previously in this guide, we've mentioned that a simulation job can be comprised of one of more runs. Conceptually, a run corresponds to a unique set of initial conditions for, and thus a unique execution of, a given model. So far, we have only ever executed each model a single time.
We can explore the effects of randomness in the modeling process by executing multiple runs of the same model. We do this using the n_reps
argument when instantiating a ModelConfig object. In order to guarantee reproducible results, we also pass the seed
argument, providing one seed for each "rep" (repetition) that we declared with the n_reps
argument.
Execute the cell below to see how this works:
# create a ModelConfig object
lottery_n6_config = ModelConfig(
synth_pop=SynthPop("US_2010.v5", ["Jefferson_County_PA"]),
start_date="2022-09-20",
end_date="2022-09-23",
n_reps=6,
seed=[
8839,
3610,
5998,
1972,
1186,
6191
]
)
# create a Job object using the ModelConfig
lottery_job = Job(
"lottery.fred",
config=[lottery_n6_config],
key="lottery_job",
fred_version="11.1.1",
results_dir="/home/epx/qsg-results"
)
# call the `Job.execute()` method
lottery_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(lottery_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(lottery_job.status)
As you can see, our ModelConfig instructed the FRED simulation engine to run the simulation 6 times, and each run produced a different outcome - the number of winning agents (and their IDs) was different each time. Also, the output of each run was displayed separately.
Every one of these runs used a different random seed to set up the conditions of the simulation, resulting in different outcomes. The ability to easily set up and execute multiple simulations of the same model is an important strength of the Epistemix Modeling Platform.
The exploration of the distribution of stochastic (i.e., random) outcomes that follows from the same model configuration (but different random seeds) is another key reason that modelers use agent-based modeling in their work. For example, the outcomes from multiple runs can be compared to estimate the rarity (or commonness) of an emergent behavior of the model. Or, the outcomes from multiple runs can be aggregated to estimate uncertainty about the value of any output parameter of interest.
9.6 Lesson Recap¶
In this lesson, you saw how the control structures built into the FRED modeling language can be used to create complex agent behaviors.
- You explored the concept of the meta agent and learned about some of the very useful actions it can carry out in a simulation.
- You used
for
loops to iterate through lists of numbers andwhile
loops to instruct an agent to keep doing something until a condition was met. - You saw how the
if
statement can be used with action rules to create different behaviors when different conditions are met. - You used the
print()
function to write unstructured output from the model and retrieved and formatted that output to render directly in the notebook. - You encountered the idea of randomness in simulations and saw how to instruct the FRED simulation engine to run multiple versions of the same model by passing the
n_reps
argument when instantiating a ModelConfig object.
You are almost at the end of this introductory sequence of Epistemix lessons! You have encountered many features of the FRED modeling language and explored some of the central concepts that power FRED simulations.
In the final lesson in this sequence, you'll encounter some functions that can be used to read data into the simulation for use by agents in your model. You'll also see a summary of all the techniques you've encountered in this guide for getting data out of your simulation. See you in Part 10!