Objectives

This is the third project on elephant population simulation. In the first project, you developed the overall simulation and used it to figure out a single parameter: the probability with which adult female elephants should be darted each year. In the second project, you explored how to optimize one or more parameters of a simulation automatically. In this project, you are using the same content/concept but redesigning the code to use classes for the Elephant and Simulation parts of the project. Using classes can make the coding process simpler, to avoid some of the challenges of the prior two weeks.

Tasks

Project 6

Projects 5-7 are cumulative. If your Project 6 generated reasonable results, you're ready to go! But if your results were off for Project 6, the same bug will throw your results in Project 7 as well. Please stop by office hours if you're stuck. We're here to help!

Simulation Class

Create a new file, simulation.py. At the top, import sys, random, and elephant. Then begin a new class called Simulation. The first thing to write is the __init__() method. Using the following def statement for it. Note that numYears will no longer be part of the Simulation fields. It will be provided as a parameter when runSimulation() is called.

def __init__(self,
             probDart = 0.43,
             cullStrategy = 0,
             probCalfSurv = 0.85,
             probAdultSurv = 0.996,
             probSeniorSurv = 0.2,
             calvingInterval = 3.1,
             carryingCapacity = 7000):

In the body, assign each of these values to a corresponding field of of the object (self). For example, the following assigns the probDart parameter to a corresponding field of the object, which is referred to by the variable self.

self.probDart = probDart

In addition, create fields to hold the population and the results and assign an empty list to each of them. That completes the creation of the Simulation object and it has all of the variables it needs to hold. All of your code in the __init__() method will be assignments to fields.

Getters & Setters

The next step is to create the "getter" and "setter" functions, one for each of the fields except for population. For example, the following returns the value of the probDart field.

def getProbDart(self):
    return self.probDart

Create similar functions for all of the other simulation parameters. Then create functions that allow you to set each parameter. Each of these functions should take self and the new value of the field as arguments. For example, the following sets the probDart field to a new value.

def setProbDart(self, val):
    self.probDart = val

When you have both getter and setter functions, test them with the following test program, which should give you this output. Note that it doesn't matter what you call your internal fields. You could store carrying capacity in self.cc, for example. It does matter what you call the getter and setter functions: they have to match what is in the test code.

Simulation Implementation

The next task is to recreate your simulation from project 5 using the same breakdown into methods. The differences will be in how you access the information. Your simulation parameters will always be contained in the fields of the object that you created in the init method (e.g., self.carryingCapacity). Likewise, to call a method of the object, such as initPopulation(), you will use self.initPopulation().

initPopulation( )

Start with the initPopulation() method, which should have the following definition.

def initPopulation(self):

Make sure the def is inside the Simulation class definition (tabbed in by one level). The initPopulation() method should assign the empty list to self.population, then it should loop for the number of times specified by the carryingCapacity parameter (self.carryingCapacity) and, each time through the loop, append a new elephant to the population (self.population).

Creating New Elephants

In order to create a new elephant, you need to create a new Elephant object. As with all objects, use the name of the class as a function to create a new instance of the object. Since the Elephant.__init__() method uses calvingInterval, pass that parameter as the sole argument to Elephant. The following expression is how you create a new Elephant object. This is what you should be appending to self.population.

elephant.Elephant( self.calvingInterval )

showPopulation( )

Write a new method, showPopulation() that prints out a header line (something that might print a separator, or the string "Showing population"), then loops through the population and, for each elephant, prints it. Because we created a __str__() method in the Elephant class, if the variable e is a reference to an Elephant object, you can use print(e) to print it to the terminal. The header for showPopulation should be:

def showPopulation(self):

incrementAge( )

Write the incrementAge() method that loops over the population list and, for each Elephant object, calls its incrementAge() method. Yes, this is a two line function.

Test Function

At this point, you may want to add a simple test function, as below, to the bottom of your simulation.py file. This function should not be part of the Simulation class.

def test_simple():
    sim = Simulation()
    sim.setCarryingCapacity(20)
    sim.initPopulation()
    sim.showPopulation()
    sim.incrementAge()
    sim.showPopulation()

if __name__ == "__main__":
    test_simple()

Run your simulation.py file and make sure it is printing out a population of 20 elephants and that it is correctly incrementing their ages.

dartPopulation( )

Write the dartPopulation() method that loops over the population list and, for each Elephant object, if it is an adult female (use the isAdult() and isFemale() methods), and if random.random() is less than self.probDart, call the dart method for the elephant object. Update your test function so that it calls dartPopulation() and then shows the population again.

cullElephants_0( )

Write a method cullElephants_0() that implements the same culling method as the prior project. It should test if there are more elephants than the carrying capacity. If so, it should reduce the population list to the carrying capacity. Finally, it should return the number of elephants culled. Note that it does not need to return the population. But you do need to make sure that self.population holds the new, modified population list. You can test this by adding the following to your simple test function.

sim.setCarryingCapacity(15)
print( "numCulled:", sim.cullElephant_0() )
sim.showPopulation()

Make sure there are only 15 elephants in the last step.

controlPopulation( )

Write the controlPopulation() method. It should call either cullElephants_0() or dartPopulation(), depending on the value of self.probDart. Then it needs to return the number of elephants culled, which is either 0, if dartPopulation() was called, or the return value of cullElephants_0().

simulateMonth( )

Write the simulateMonth() method. This will be much simpler than the prior week. It should loop over the population. For each elephant, if it is female and an adult (use isFemale and isAdult), then if the return value of the elephant's progressMonth() method is True, append a new Elephant of age 1 to the population.

calcSurvival( )

Write the calcSurvival() method. Assign the empty list to a temporary population variable. Then loop over the population (self.population). Test each calf, juvenile/adult, or senior and, if they survive, append them to the new population. The last step should be to assign to self.population the new population. The method does not return anything.

simulateYear( )

Write the simulateYear() method. This will be four lines of code. Call the calcSurvival() method, call the incrementAge() method, then loop 12 times and inside the loop call the simulateMonth() method.

calcResults( )

Write the calcResults() method. It should do pretty much the same thing as it did in the prior two labs. The calcResults() method should have self and numCull as its parameters. It should return a list with total population, the number of calves, the number of juveniles, the number of adult males, the number of adult females, the number of seniors, and the number culled.

runSimulation( )

Finally, write the runSimulation() method. It should have self and numYears as parameters. It should do the same thing as in the prior labs: call initPopulation(), call controlPopulation(), assign an empty list to self.results, then loop for the number of years (numYears). Each time through the loop it should call simulateYear(), then controlPopulation(), then append to the results list the return value of calcResults(). Make sure to use self. whenever you are referencing a field of the object (e.g., results) and whenever you are calling a method of the Simulation class (e.g., calcResults()). The method should return self.results.

You can use this test function to test your runSimulation() method. When you run it with a value like 0.43, you should get around 1000 total elephants at the end. If you run it with larger or smaller probDart values, make sure you get smaller/larger total population results.

writeDemographics( )

Write a writeDemographics function, that takes in a filename (in addition to self) and writes the results, stored in self.results, to a proper CSV file. The CSV file should have a header line that begins with a hash, #, and then has a header for each column. Make Year the first column, then total population, number of calves, number of juveniles, number of male adults, number of female adults, number of seniors, and number culled.

You can use the following test function to run your whole simulation. The test file creates the file demographics.csv. Note, it will overwrite any existing file with that name.

Make a plot that shows year, number of calves, number of juveniles, and number of adult females for a darting probability of 0.43. Then make a second plot that shows the same data for a darting probability of 0.0. Discuss these results in your writeup.

cullElephants_1( )

Implement a second cull strategy that culls only adult females. Create a new function cullElephants_1() that implements the strategy. The modify controlPopulation() so that the new strategy is called instead of cullElephants_0() when the cullStrategy is equal to 1.

Run the two different culling strategies, then examine the demographics and number culled, in comparison to random culling. You can present this as a simple table showing the average number of each category (calf, juvenile, adult female, etc) for each case.

Simulating the Effects of Catastrophic Events

Update the code so that you can simulate the population under normal conditions, then simulate a catastrophic event that kills off a percentage of the population, then continue with the population simulation.

First, write a method to simulate a catastrophic event: decimate(). The decimate() method reduces the population by the given percent. The surviving elephants should be a random sampling of the population.

Second, add a parameter to runSimulation() that makes it possible to skip the initialization steps. i.e. change the def statement to def runSimulation(self, numYears, startFresh = True): and adjust your code so that it does the initialization (calling initPopulation(), calling controlPopulation(), and assigning an empty list to self.results) only if startFresh is True.

Use these methods to run the simulation for 100 years (starting fresh), then decimate the population by 30%, then continue the simulation for another 100 years. How do the culling v. darting strategies respond?

Be sure to turn in the test code you wrote to find your answer.

Plot the total population over each 200-year trial (one per culling strategy) in one graph, and include this graph in your writeup.

Example Extensions

Each assignment will have a set of suggested extensions. The required tasks constitute about 83% of the assignment, and if you do only the required tasks and do them well you will earn a B. To earn a higher grade, you need to undertake one or more extensions. The difficulty and quality of the extension or extensions will determine your final grade for the assignment. One complex extension, done well, or 2-3 simple extensions are typical.

The following are a few examples of potential extensions. Please do not feel that your extensions must be drawn from this list. Get creative, design extensions that interest you, and explain why they are awesome when you present the results in your writeup. Your interest in your own extensions really does make a difference.

Writeup

Make a new wiki page for your assignment. Put the label cs152f18project7 on the page. Make sure that it is there.

In addition to making the wiki page writeup, put the python files you wrote on the Courses server in your private directory in a folder named project7.

Colby Wiki

In general, your writeup should follow the outline below.


© 2018 Eric Aaron (with contributions from Colby CS colleagues).