CS106 Lab 4: Population Simulation

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:

  1. How many individuals start out in the simulation?

  2. At what age does an individual die?

  3. What statistics are kept for all individuals for each time period?

  4. What does the function increment_age() do?

Find the code that follows the line # ---------------- main -----------------

  1. Describe to your partner how the first block of code creates individuals with (mostly) random properties.

  2. 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?

  3. Go back to the top of the file and inspect the comment that describes how to use the DEBUG constant. Make sure you understand this code.

Note these important features of the code:

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

  2. 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:

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:

Step 5: Change find_male()

Now we’re starting to work on the code for reproduction. The way this works is:

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:

Then, **submit your completed population.py on Moodle.

Grading Rubric (10 points total):