Lab Exercise 5: Representing Elephants as Lists
The purpose of this project is to practice modular design of code with a larger, slightly more complex simulation than last week. In addition, we'll be making use of nested lists--lists of lists--in order to manage more complex data.
This is the first of a three-part project where we'll be simulating the elephant population in Kruger National Park, South Africa. The carrying capacity of the park is approximately 7000 elephants (1 elephant per square mile of park). Previous efforts to manage the population involved culling approximately 400 animals per year. After the development of an elephant contraceptive, the current effort to manage the population involves using a contraceptive dart on adult female elephants to limit the birth rate.
The elephant population simulation will be more detailed than the penguin simulation, because the characteristics of each animal will be relevant to the birth and death of individual animals.
The CS focus of this week is to gain more practice writing code, working with lists, working with lists of lists, and using modular code design.
If you have not already done so, mount your personal space, create a new project5 folder and bring up TextWrangler and a Terminal.
- If you haven't already set yourself up for working
on the project, then do so now.
- Mount your directory on the Personal server.
- Open the Terminal and navigate to your project5 directory on the Personal server.
- Open TextWrangler. If you want to look at any of the files you have already created, then open those files.
Create a new file called elephant.py. The design of the
simulation this week will be similar to the penguin simulation in the
last project. The main function (the function at the top of the
hierarchy) will handle parameter setting, call the function that runs
the simulation, and then collate and print/write the results.
The diagram below shows the hierarchical relationship of all of the functions you will be writing.
The simulation this week will be a little more complicated, because we are going to model the sex, age, and breeding status of each individual elephant in order to more accurately model the number of births each year and the effects of using the contraceptive darts.
Here are a few assumptions we'll make about the elephant population.
First we divide the elephants into age groups and outline out assumptions about how likely elephants in each age groups are to survive each year.
- Calves - calves are 1 year old. The survival rate of a calf is between 80-90%, so we're going to assume it is 85%.
- Juveniles - juveniles are between 2 and 12 years old (i.e. a 2-year-old is a juvenile and a 12-year-old is a juvenile). The survival rate of juveniles is very high, 99.6%.
- Adults - adults are between 13 and 60 years old (i.e. a 13-year-old is an adult and a 60-year-old is an adult). Adults have the same survival rate as juveniles, 99.6%.
- Seniors - a senior is more than 60 years old. The lifespan of an elephant is about 60 years, so we assume that the survival rate of seniors is 20%.
Next, we outline how we handle reproduction:
- Adult female elephants can get pregnant, and the gestation periods if 22 months.
- The average period between giving birth for an adult female is between 3.1 and 3.3 years, we're going to use 3.1 years as our average.
- Since the gestation period is 22 months, that means that when a female is not already pregnant, the chance per month of her getting pregnant is 1.0 / (3.1*12 - 22), if she is not on contraceptive.
- A contraceptive dart ends any existing pregnancy and prohibits the elephant from getting pregnant for the next 22 months.
For this simulation, we're going to model gestation and contraception on a per month basis. However, we'll model survival and darting on a per year basis. The mixture is because the gestation and contraception don't follow simple yearly intervals, but survival rates are easier to compute per year.
As with last week, you'll start with the lower level functions, testing them as you write them, and work up to the entire simulation.
Unlike last week, where we gave each function many, many parameters, we're going to collect all of the parameters for the simulation in a list and pass around the list. This has the benefit of making the function definitions simpler and more uniform, but it means we have to define the position of each simulation parameter in the parameter list. It is important that we set up the list of parameters carefully and use the same location in the list for the same parameter throughout the code.
The following table shows all of the parameters of the simulation and their initial default values. As you will need to discover the percent of elephants to dart to make the population stable, that value is given as zero.
Calving Interval 3.1 Percent Darted 0.0 Maximum Juvenile Age 12 Maximum Adult Age 60 Probability of Calf Survival 0.85 Probability of Adult Survival 0.996 Probability of Senior Survival 0.20 Carrying Capacity 7000 Number of Years 200
In order to pass around all of these parameters, we're going to use a list. One method of setting up the list is to just make a list of numbers. However, that makes it difficult to remember which number represents which parameter. Instead, what if we assign the numbers to individual variables and then create a list out of the variables? This has the benefit that we can look at the variable names in the list to tell us the meaning of each position in the list.
Using the template below, make a test function in your elephant.py file that creates the parameter list and then prints it to the Terminal.
def test(): # assign each parameter from the table above to a variable with an informative name # make the parameter list out of the variables # print the parameter list if __name__ == "__main__": test()
The one problem with using a list is that when we need to access a parameter, we have to remember that parameter's index in the list. For example, the calving interval parameter will be parameter if you set up the parameter list with that value in the first position.
Using this setup means it is easy to make mistakes when you have to remember whether a given value is in position 4 or position 5. It also makes it hard to modify your code. For example, if you feel you need to add a new parameter to the list of parameters, what happens if you put it first in the list? That would mean you would have to go through all of your code and change the indexes everywhere you were accessing a parameter value. That's a lot of code and a lot of opportunities to make mistakes.
Instead, how could we use software engineering principles to avoid having to remember the index? What about using a module-level (top-level) variable whose value remains constant throughout the simulation? We can use it to index into the list. For example, we could create a variable IDXCalvingInterval that is set to 0 -- the index of the calving interval in the list of parameters. Then, when we need to access the calving interval, we use code like
In your elephant.py file, assign appropriate values to module-level variables for each of the parameters in the simulation (like we did for IDXCalvingInterval). When naming your variables, please begin each name with IDX to indicate the value is an index, then use a name that indicates the field. Add code to your test function to make sure the parameters are correctly assigned.
- Because we will be modeling individual elephants
with such detail, we will be modeling each elephant as a list with four features.
We have to keep track of an elephant's gender and
age. For females, we also have to keep track of whether an adult
female is pregnant, and whether she has been hit with a contraceptive
dart, and, if so, how long is left on the contraception. The following
table gives the attributes and their types.
Field Type gender string age integer months pregnant integer months contraceptive remaining integer
We will use the same design principle as above, and use top-level variables with constant values to keep track of which entry in the elephant list is storing information about which field in the table. Write the four assignment statements necessary to keep track of elephant features. Use the same naming convention as above (Note: we used IDXGender, IDXAge, IDXMonthsPregnant, and IDXMonthsContraceptiveRemaining in our code and will refer to these by name in the instructions).
The first function you will write is newElephant, which
should create and return a list with all of the necessary features of
an individual elephant. The newElephant function should have
two parameters: the list of simulation parameters, and the age of the
elephant to create. It should return a list with the four items
def newElephant( parameters, age ): # code goes here return elephant
The newElephant function will need to use the calving interval, maximum juvenile age, and maximum adult age parameters, so use the IDX names to access the parameter list and assign those to well-named local variables.
Next, create a list (e.g. elephant) with four zeros in it. Then use the random package to assign to elephant[IDXGender] either 'm' or 'f'. Then assign to elephant[IDXAge] the age parameter (not a random number, but the value contained in the age parameter).
If the elephant is female, then, if the elephant is of breeding age (older than the maximum juvenile age and less than or equal to the maximum adult age), test if the elephant is pregnant (the probability the elephant is pregnant is 1.0 / calvingInterval). If the elephant is pregnant, pick a random number between 1 and 22 and assign that to the IDXMonthsPregnant position in the elephant list. This will be three nested if-statements.
No elephant starts out on contraceptive, so the IDXMonthsContraceptiveRemaining field of the elephant list will always be zero.
Return the elephant list.
Finally, test the newElephant function by downloading test_newElephant.py, reading the code to make sure you understand it. Read the comments to make sure you know what output to expect, run it, examine the output, and make any necessary changes to your code.
The next step is to use the newElephant function in the initPopulation
function. The initPopulation function takes in the parameter list and
returns a list of elephant lists. The number of elephants to create
is the carrying capacity parameter.
The initPopulation function should initialize a population list to the empty list, loop for the number of elephants to create and append a new elephant list (call newElephant with a randomly chosen age between 1 and the maximum adult age) to the population list each time through the loop. It should then return the population list.
Add some code to your test function that calls initPopulation and then prints out the population list. You may want to temporarily reduce the carrying capacity parameter to 20 instead of 7000 for testing.
The next function to create is incrementAge. This function should
take in a population list and return a population list. Inside the
function, it should increment each elephant's age by 1 (year).
When you loop over the population list, you can choose to loop by an index (e.g. for i in range(len(pop)):) or you can choose to loop over the contents of the population list (e.g. for e in pop:). In either case, you want to modify the second element each elephant list. In the first case, the elephant list will be pop[i]. In the second case, the elephant list will be e.
Add code to your test function that calls incrementAge. You will be passing in the population list created by initPopulation and then assigning the result of the incrementAge function back to the same variable. Print out the new population list and double-check that each age was incremented by one. Again, you probably want to keep the population small (like 20).
When you are done with the lab exercises, you may begin the project.