CS 152: Lab 5

Title image Project 5
Spring 2017

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.


Tasks

If you have not already done so, mount your personal space, create a new project5 folder and bring up TextWrangler and a Terminal.

  1. If you haven't already set yourself up for working on the project, then do so now.
    1. Mount your directory on the Personal server.
    2. Open the Terminal and navigate to your project5 directory on the Personal server.
    3. Open TextWrangler. If you want to look at any of the files you have already created, then open those files.
  2. 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.

    control flow diagram

    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 Interval3.1
    Percent Darted0.0
    Maximum Juvenile Age12
    Maximum Adult Age60
    Probability of Calf Survival0.85
    Probability of Adult Survival0.996
    Probability of Senior Survival 0.20
    Carrying Capacity7000
    Number of Years200

    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[0] 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

    parameters[IDXCalvingInterval]
    
    instead of
    parameters[0]
    

    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.

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

    FieldType
    genderstring
    ageinteger
    months pregnantinteger
    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).

  4. 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 above.

    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.

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

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