Project 7: Object-Oriented Simulation Design
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 percentage of female elephants to dart 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. You should find that using classes makes the coding process simpler and avoids some of the challenges of the prior two weeks.
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
def __init__(self, percDart = 0.425, 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 percDart parameter to a corresponding field of the object, which is referred to by the variable self.
self.percDart = percDart
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.
The next step is to create the "get" and "set"
functions, one for each of the fields except for population.
For example, the following returns the value of the percDart field.
def getPercDart(self): return self.percDart
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 percDart field to a new value.
def setPercDart(self, val): self.percDart = val
When you have both get and set 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 get and set functions: they have to match what is in the test code.
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
Start with the initPopulation method, which should have the following definition.
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).
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 )
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:>
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.
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.
Write the dartPopulation method that loops over the population list and, for each Elephant object, if it is female (use the isFemale method), and if random.random() is less than self.percDart, call the dart method for the elephant object. Update your test function so that it calls dartPopulation and then shows the population again.
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.
Write the controlPopulation method. It should call either cullElephants_0 or dartPopulation, depending on the value of self.percDart. 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.
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.
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.
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.
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.
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.42, you should get around 1000 total elephants at the end. If you run it with larger or smaller percdart values, make sure you get smaller/larger total population results.
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 dart percentage of 0.425. Then make a second plot that shows the same data for a dart percentage of 0.0.
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, junvenile, adult female, etc) for each case.
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
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 inititialization 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?
Each assignment will have a set of suggested extensions. The required tasks constitute about 85% 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.
- Figure out how to automate the graphiing process using gnuplot.
- Develop another management strategy, which is to adjust the percent darted based on whether the population is above or below the target. You have to make very small adjustments to the percent darted in order to avoid large oscillations. See how this method responds to an event that decimates the population.
- Explore how the population responds to decimation events that are selective in their effect. For example, what if only calves and juveniles are affected? What if only pregnant females are affected?
- Develop other culling/darting strategies and discuss their effects and trade-offs. How easy would they be to implement?
- Enable the user to control your top level program with optional flags. For example, -par CarryingCapacity would specify that the program should evaluate carrying capacity, and -min 3500 would specify that it should start the evaluation at 3500.
- Check out the os package (import os). What could you do with the os.system function to automate your simulations?
Write-up and Hand-in
Turn in your code by putting it into your private hand-in directory on the Courses server. All files should be organized in a folder titled "Project 7" and you should include only those files necessary to run the program. We will grade all files turned in, so please do not turn in old, non-working, versions of files.
Make a new wiki page for your assignment. Put the label cs152f16project7 in the label field on the bottom of the page. But give the page a meaningful title (e.g. Milo's Project 7).
In general, your intended audience for your write-up is your peers not in the class. Your goal should be to be able to use it to explain to friends what you accomplished in this project and to give them a sense of how you did it. Follow the outline below.
- A brief summary of the task, in your own words. This should be no more than a few sentences. Give the reader context and identify the key purpose of the assignment.
- A description of your solution to the tasks, including any text output or images you created. This should be a description of the form and functionality of your final code. Note any unique computational solutions you developed or any insights you gained from your code's output. You may want to incorporate code snippets in your description to point out relevant features. Code snippets should be small segments of code--usually less than a whole function--that demonstrate a particular concept. If you find yourself including more than 5-10 lines of code, it's probably not a snippet.
- A summary of your findings in the simulations. What did you discover by using the simulation? Do your results make sense?
- A description of any extensions you undertook, including text output or images demonstrating those extensions. If you added any modules, functions, or other design components, note their structure and the algorithms you used.
- A brief description (1-3 sentences) of what you learned. Think about the answer to this question in terms of the stated purpose of the project. What are some specific things you had to learn or discover in order to complete the project?
- A list of people you worked with, including TAs and professors. Include in that list anyone whose code you may have seen, such as those of friends who have taken the course in a previous semester.
- Double-check the label. When you created the page, you should have added a the label cs152f16project7. Make sure it is there.