Due: , 11:59 pm
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 some 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 haven't already set yourself up for working on the project, then do so now:
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 for the project--the full project, not just the lab part. (You'll see the same diagram on the project assignment.)
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, elephants live about 60 years. Females over age 12 and less than or equal to age 60 can get pregnant, and the gestation period is 22 months. For elephants over age 60, we're going to assume that the survival rate is 20% per year (they are unlikely to live long beyond 60). From ages 2 through 60, we're going to assume that the survival rate is very high, 99.6%. The survival rate of a calf, age 1, is between 80-90%, so we're going to assume it is 85%. 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 probability with which elephants need to be darted to make the population stable, that value is given here as zero (you'll change it to different values later in the project assignment).
|Probability of Darting||0.0|
|Probability of Calf Survival||0.85|
|Probability of Adult Survival||0.996|
|Probability of Senior Survival||0.20|
|Number of Years||200|
(Recall that the "Maximum Age" above refers to the maximum age for an elephant to be categorized as Adult rather than Senior. Elephants can live beyond that Maximum Age value, but they are not likely to live long past it.)
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 at index 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
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.
|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 (that is, a list that represents an elephant) 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 an elephant list, having the four items above.
def newElephant( parameters, age ): # code goes here return elephant
The newElephant() function will need to use the calving interval, juvenile age, and maximum age parameters, so use the IDX constants 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', with a probability of 0.5 that the elephant will be female. 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 juvenile age and less than or equal to the maximum 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 adding the following code to your test function. It should create 15 elephants and then print out each elephant list. At least a few of the adult females should have a non-zero number in the third column. This code assumes you have a variable named parameters that contains the parameters for a simulation.
pop =  for i in range(15): pop.append( newElephant( parameters, random.randint(1, parameters[IDXMaxAge]) ) ) for e in pop: print(e)
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 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
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 in 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 start on the rest of the project.
© 2019 Eric Aaron (with contributions from Colby CS colleagues).