The purpose of today's lab is to practice using if statements.
Your program will simulate a population of individual animals -- rabbits or Blast-Ended Skrewts or some such. The program will consider the individual’s health, age, and gender when deciding if the individual should live or die, and reproduce or not. The program keeps track of the number of individuals each time unit (day, month, whatever) that are alive, have died, or have been born.
You will be given a large amount of skeleton code, with missing pieces
to fill in. The code to fill in will only involve if statements,
assignment statements, and manipulating lists and dicts.
Population data structure (individuals) ¶
The population is represented by a list of individuals.
Each individual is represented as a dictionary. A population might look like:
[{'age': 7, 'gender': 'F', 'health': 9.06, 'is_alive': True},
{'age': 6, 'gender': 'M', 'health': 1.39, 'is_alive': True},
{'age': 5, 'gender': 'F', 'health': 7.31, 'is_alive': True},
{'age': 1, 'gender': 'F', 'health': 6.32, 'is_alive': True}]
Then individual 0 is a Female, age 7, with health 9.06, and is alive.
If we’d wanted to access their data, we could write:
# get out the dictionary for just one individual
individual = individuals[0]
print(individual) # prints the first line of "population looks like" above
print(individual['age'])
print(individual['gender'])
...
and we could change them with
individual = individuals[0]
# make the individual a year older
individual['age'] = individual['age'] + 1
# a shorter way to do the same thing:
individual['age'] += 1
A note about is_alive: When an individual dies, it isn’t immediately removed from the individuals list. Instead, the is_alive flag (boolean value) for that animal is set to False instead of True. Dead animals are removed from the population (by vultures perhaps?) once per time step by code that is already provided.
Counters (counters) dictionary ¶
The code needs to keep track of how many individuals were born and died each time step.
It keeps these counts in a dict named counters, which looks like:
{'num_died_of_old_age': 1, 'num_died_of_poor_health': 2, 'num_born': 5}
See display_population_stats for an example of how to get data out of the counters.
At the beginning of each step the counters are reset to 0. Every time an individual dies or is born, we increment the corresponding counter. For example (and forgive me the Oregon Trail reference):
counters['num_died_of_snakebite'] += 1
Step 1: Download the skeleton file ¶
Begin by creating a new folder for today's lab called lab4.
Download this file. Put it in your lab 4 folder and rename it to population.py.
Step 2: Tour the Code ¶
With your partner, answer these questions by inspecting the code:
-
How many individuals start out in the simulation?
-
At what age does an individual die?
-
What statistics are kept for all individuals for each time period?
-
What does the function
increment_age()do?
Find the code that follows the line
# ---------------- main -----------------
-
Describe to your partner how the first block of code creates individuals with (mostly) random properties.
-
Describe to your partner what the “Main loop” code does for each “day”. I.e., what function calls are made each day? Does the order matter?
-
Go back to the top of the file and inspect the comment that describes how to use the
DEBUGconstant. Make sure you understand this code.
Note these important features of the code:
-
Find the places where the word TODO exists in the code. These are where you will need to write your own code, or make changes to the provided code. You do not need to change any other code besides what’s marked with TODOs.
-
The code makes extensive use of functions, which you have experience calling, but have not experienced writing yourself. You will have to make changes to these functions. Make sure your code is indented properly within the functions.
Step 3: Run the existing code ¶
Execute the code and note what you see. You should see no individuals being born or dying.
Make sure you understand the data structures:
- Predict what
counters.keys()will give you, then try it on the Thonny shell. - Try to extract the age of the first individual in the population using code on the Thonny shell.
- Now try to increase the health of the first individual by one unit by running code in the Thonny shell.
- Finally, try to increment one of the counters.
Now you’re ready to go!
Step 4: Change update_health() ¶
Replace the TODO comment in update_health() with real code. Note that you
will need to use random.uniform(a, b) to get a random floating point
number here.
Refer to increment_age as an example.
Since it’s tricky to see the effect of changes to individual functions, we’ve provided ZyBooks tests for individual functions.
Check your solution by copying the entire update_health function (starting from def update_heath) into the corresponding ZyBooks test.
Step 5: Change update_alive() ¶
Replace the TODO comment in update_alive() with real code.
Let me help you get started. You see this in the file:
for i in range(len(individuals)):
individual = individuals[i]
# TODO: check if this individual should die due to
# old age. If so,
# - print a debugging message that the individual i died of old age
# - change is_alive to False for that individual
# - increment the correct counter
# otherwise, check if their health is 0 or less. If so:
# - print a debugging message that the individual i died of poor health
# - change is_alive to False for that individual
# - increment the correct counter
Start by creating the if statement: leave the comments intact so they
guide you. The if statement is going to have to check the age of
individual i against the DIE_AGE constant. I would write this code:
for i in range(len(individuals)):
individual = individuals[i]
# Check if this individual should die due to
# old age.
if individual['age'] >= DIE_AGE:
# print a debugging message that the individual i died of old age
if DEBUG:
print("Individual", i, "died of old age")
# change is_alive to False for that individual
# increment the correct counter
Notice how I leave the comments (which describe the algorithm) in place, and add code below each comment line. I also removed the TODO.
Continue to add code in the rest of this function to implement the algorithm. Tips:
- I recommend putting multiple debugging print statements (“wrapped” in
if DEBUG:) to make sure you get the code correct. - Watch out for how much each line is indented. For each line, ask yourself: under what condition should this line run?
- Try running the code. Fix any typos that Thonny finds.
- Once you think it’s generally right, submit it to the corresponding ZyBooks test.
Step 5: Change find_male() ¶
Now we’re starting to work on the code for reproduction. The way this works is:
reproducecallsget_reproducing_pairsto match up pairs of animals.get_reproducing_pairsgoes through all of the eligible females in the population; for each female, it callsfind_maleto find a male that hasn’t yet been matched up.find_malegoes through all of the eligible males to find one that’s not yet matched
Notice that find_males takes a parameter named males_already_paired. This is a list of indices (list of ints). Each index is the index into the individuals list of the males that have already been paired.
Your task is to replace the False in find_male() with the correct condition. See the tips in the previous step.
As usual, try your function in ZyBooks to check it out. Note that ZyBooks won’t catch every possible mistake.
Step 6: Change get_reproducing_pairs() ¶
Now that find_male() works, you can work on the analogous condition in get_reproducing_pairs(). Try it in ZyBooks.
Step 7: Change reproduce() ¶
Replace the TODOs in reproduce(). Refer to the code just below it, commented Initialize first population of individuals, for an example of how to make new individuals. Try the result in ZyBooks.
Step 8: Observe the results ¶
Run your code multiple times and observe the results. Does your code
often make it through NUM_ITERATIONS or do all individuals die before
that? Does your code ever get through all iterations without showing a
population explosion?
Try tweaking some of the CONSTANTS at the top of the file to see if you can produce some runs where the population survives but there is not a population explosion.
Submit ¶
Before you submit:
- Make sure your header documentation includes your userid, your partner’s userid, and the other usual documentation.
- Test every function in ZyBooks. Note that ZyBooks won’t catch every possible mistake.
- Set
DEBUGtoFalsebefore submitting your code. Check that everything still works! - Make sure your comments are cleaned up (no TODOs) and line up with the corresponding code. See the example in Step 4.
Then, **submit your completed population.py on Moodle.
Grading Rubric (10 points total):
- 1 point for setting
DEBUGtoFalsebefore submitting - 1 point for code in
update_health() - 2 points for code in
update_alive() - 2 points for code in
find_male() - 2 points for code in
get_reproducing_pairs() - 2 points for code in
reproduce()