Due date: February 9, 2024 at 11:59pm
Introduction
Some tricksters in the biology department decided to increase school spirit by genetically engineering a virus to turn everyone’s hair blue. However, they need your help! They can’t figure out exactly what parameters to use to affect the largest number of students possible. They’ve described the virus behaviour, but it’s up to you to simulate it and find the maximum number of students that can have their hair turned blue.
Objectives
- Implement your first medium-scale C++ program
- Review the use of loops, conditionals, and functions
- Practice using data types in C++
- Practice using pass by reference parameters in C++
Note: This assignment is not to be completed using arrays, vectors, or any other data structure. The goal is to practice with the basic data types and control structures in C++.
The Virus
The virus is a simple one, but it has a flaw: infected persons will either have their hair turned blue (“afflicted”) or they will be able to infect others (“carriers”). The chance of any individual being afflicted is represented as $p$, while the number of other people that a carrier can infect is represented as $R_0$ . Your task is to find the $p$ and $R_0$ values that will afflict the largest number of people.
The range of $p$ values must be between 0 (no one afflicted) and 1 (everyone afflicted), while the range of $R_0$ values is restricted to between 0 (not at all contagious) and 20 (approximately as contagious as measles).
The Program
Your program should allow the user to input the start and end values for a range of $p$ and $R_0$, then simulate the virus for each combination, with a fixed number of 10 steps between the start and end values (inclusive). It should then print out the maximum number of people afflicted and the $p$ and $R_0$ values that produced that maximum.
Sample run, with user input in bold:
$ ./a1 $ Enter the range of R0 values (0 - 20): 0.5 12 $ Enter the range of p values (0 - 1): 0.1 0.95 $ Running scenarios, this may take some time $ Maximum of 5088 afflicted at an R0 of 10.7 and p value of 0.5
Note: due to the randomness of the simulation, your results are likely to differ from the sample output.
In addition, your program should perform some basic error checking on the user input. If the user enters a range that is out of bounds (e.g. $R_0 > 20$), set the value to the maximum/minimum allowed value and inform the user, e.g.:
$ ./a1 $ Enter the range of R0 values (0 - 20): 0.5 25 $ 25 is too high, setting to 20 ... etc
Finally, if the user enters the range backwards (e.g. start value is greater than end value), swap them. No need to inform the user in this case. For example:
$ ./a1 $ Enter the range of R0 values (0 - 20): 12 0.5 $ Enter the range of p values (0 - 1): 0.95 0.1 $ Running scenarios, this may take some time $ Maximum of 4893 afflicted at an R0 of 8.2 and p value of 0.5
(Note that the numbers are somewhat different from the first sample run due to the randomness of the simulation.)
The Starter Code
cd
to your 1633
directory (or wherever you keep your files for this course), then clone the starter code repository from /library/students/comp1633/a1.git
using the following command:
$ git clone /library/students/comp1633/a1.git
This will create a directory called a1
in your current directory. cd
into a1
and configure your assignment dropbox using the git-asg-config
command as described in lab 2
. This time, the menu items have been updated to be shown in alphabetical order, so select option 1
to configure for assignment 1.
Inside this directory you will find the following structure:
$ tree
.
├── main.cpp
├── makefile
├── testing
│ ├── makefile
│ └── test_a1.cpp
├── virus.cpp
└── virus.h
It seems like a lot of files, but there’s a method to the madness! The bulk of your code should go in virus.cpp
, while you should not need to modify virus.h
, test_a1.cpp
, or the two makefiles.
main.cpp
This file contains the main()
function (your program entry point) and a get_range
function declaration. You will need to:
- Implement the
get_range
function at the bottom ofmain.cpp
. This function should allow the user to input a start/end range and perform the error checking described above.get_range
does not need to prompt for input - we haven’t discussed string variables yet so you can prompt inmain
before callingget_range
.Note: we will be discussing C-strings towards the assignment due date, so if you’d like to modify the function to accept a C-string and prompt in
get_range
you may do so. However, this is not required. - Implement the
main
function with the following logic:
Your output should match the sample output shown above, with the number of afflicted people rounded to the nearest integer and the R0 and p values displayed to one decimal place.Prompt for and get the R0 range Prompt for and get the p range Call run_scenarios with the R0 and p ranges Print the resulting maximum number of afflicted people and the corresponding R0 and p values
virus.h
This file contains the declarations for the functions you will need to implement in virus.cpp
. You should not modify this file.
In addition, virus.h
defines the following constants:
POPULATION
: the total number of people in the simulationSTEPS
: the number of steps to divide the $R_0$ and $p$ ranges intoN_TRIALS
: the number of trials to run and average for each combination of $R_0$ and $p$
You may modify these constants during development if you wish, but make sure to set them back to their original values before submitting. These numbers were chosen for a reasonable balance between runtime and accuracy.
virus.cpp
Here’s where most of the magic happens! You will need to implement the three functions declared in virus.h
.
single_trial
: Run a single trial for the given $R_0$ and $p$ values. Pseudocode for this function is as follows:Initialize the number of infected people to 1 Initialize the number of susceptible people to POPULATION - 1 Initialize a counter to keep track of total afflicted while there are still infected people: Initialize counters for current afflicted and carriers for each infected person: call rand_float to get a random float between 0 and 1 if the random float is less than p: increment the number of afflicted people else: increment the number of carriers update the total number of afflicted update the number of infected to carriers * R0 and cast the result to an integer if the total number of infected is greater than susceptible: set infected to susceptible subtract the number of afflicted from susceptible subtract the number of carriers from susceptible return the total number of afflicted
The function
rand_float()
is implemented for you in this file. Do not modify the various random number functions in this program - C++98 has fairly janky random number generation, and the current configuration ensures that the tests are repeatable.mean_afflicted
should callsingle_trial
N_TRIALS
times and return the average number of afflicted people. No psuedocode for this one, but it should be a fairly straightforward and small function.run_scenarios
should take the $R_0$ and $p$ ranges as parameters and simulate the virus for each combination of $R_0$ and $p$. It should then update the reference parameters to the maximum number of afflicted people and the corresponding R0 and p values. Pseudocode for this function is as follows:Print the message that the scenarios are running (see sample output above) Initialize the reference parameters to keep track of: - the maximum number of afflicted people - the corresponding R0 and p values Divide the R0 and p ranges into STEPS steps each, inclusive of start and end values For each R0 value For each p value Call mean_afflicted with the current R0 and p values If the number of afflicted people is greater than the current maximum Update the maximum number of afflicted people Update the corresponding R0 and p values
Note that
run_scenarios
is avoid
function - it “returns” information by updating the reference parameters.Hint: Be careful when dividing the ranges into
STEPS
steps: you want to be certain to include both ends. For example, to evenly divide the range [1, 3] into 3 steps (1, 2, and 3), you can’t simply divide (3 - 1) by 3. After finding your step size, you can calculate the R0 and p values at each iteration, for examplestart + step_size * counter
. Technically you can also use use a floating point loop control variable, but this can be risky as your condition (e.g.R0 <= R0_end
) is not guaranteed to be met due to the nature of floating point operations.
Testing and development tips
Test early, test often! Do not try to write the entire program at once - start with each piece and test as you go. For example, you can hard-code some $R_0$ and $p$ values in main
and call single_trial
to make sure it works. Then you can test mean_afflicted
by calling it with a few different values. Finally, you can test run_scenarios
by calling it with a few different ranges and checking the output. I don’t recommend adding the user input until the very end, as it’s much faster to just re-run a program when there are no prompts for user input.
As with labs, the starter code includes a test program that you can use to test the single_trial
function. To run the tests, cd
into the testing
directory and run make
. This will compile the test suite, at which point it can be run with ./test_a1
. If all goes well, you should see the following output:
Running main() from gtest_main.cc [==========] Running 8 tests from 1 test case. [----------] Global test environment set-up. [----------] 8 tests from SingleTrialTest [ RUN ] SingleTrialTest.ZeroP [ OK ] SingleTrialTest.ZeroP (0 ms) [ RUN ] SingleTrialTest.ZeroR0 [ OK ] SingleTrialTest.ZeroR0 (0 ms) [ RUN ] SingleTrialTest.HighR0MediumP [ OK ] SingleTrialTest.HighR0MediumP (0 ms) [ RUN ] SingleTrialTest.HighR0LowP [ OK ] SingleTrialTest.HighR0LowP (1 ms) [ RUN ] SingleTrialTest.MediumR0MediumP [ OK ] SingleTrialTest.MediumR0MediumP (0 ms) [ RUN ] SingleTrialTest.MediumR0LowP [ OK ] SingleTrialTest.MediumR0LowP (0 ms) [ RUN ] SingleTrialTest.LowR0MediumP [ OK ] SingleTrialTest.LowR0MediumP (0 ms) [ RUN ] SingleTrialTest.LowR0LowP [ OK ] SingleTrialTest.LowR0LowP (0 ms) [----------] 8 tests from SingleTrialTest (1 ms total) [----------] Global test environment tear-down [==========] 8 tests from 1 test case ran. (1 ms total) [ PASSED ] 8 tests.
If you see any FAILED
tests, you should fix your code before submitting. Note that this test only checks the single_trial
function - you will need to test the other functions yourself, but as they all depend on single_trial
I thought it appropriate to provide some expected values for that function.
Repeatable randomness
Feb 7 update: I’m getting some questions about how to know whether the solution is correct. It’s tough with random numbers! The tests define a random number “seed” that makes it produce a repeatable sequence of values, but your main program sets the seed based on the current time, whatever that happens to be.
If you’d like to verify your results in a repeatable way, open main.cpp
and comment out the following line:
// Random number stuff - do not modify
srand(static_cast<unsigned int>(time(NULL)));
Instead, set the seed to a fixed value, e.g.:
// Random number stuff - do not modify
// srand(static_cast<unsigned int>(time(NULL)));
srand(42);
With this change my program always produces the following result:
$ ./a1 Enter the range of R0 values (0 - 20): 0.5 12 Enter the range of p values (0 - 1): 0.1 0.95 Running scenarios, this may take some time Maximum of 4822 afflicted at an R0 of 12.0 and p value of 0.5
(or 4821 if you are truncating instead of rounding the final number - I’m fine with either approach).
This is a good way to verify that your program is working as expected. Just remember to change the seed back to the original value before submitting!
Incremental development
A portion of the marks for this assignment is for “evidence of incremental development”. This means I am looking for several meaningful commits to your git repository rather than just adding your solution all in one go. This is ultimately a good habit to get into! I recommend committing your changes whenever you:
- Add a new function
- Solve a problem you’ve been trying to fix
- Stop working on your code to go do something else
- Or any other time it feels right
You may add, commit, and push your code as many times as you like up until the assignment deadline. I will only mark the final version, but I will look at your commit history to see how you developed your solution.
Marking scheme
This assignment is worth 8% of your final grade and roughly divided as:
- 40% for program functionality (does it behave as specified?)
- 40% for program implementation (is it written as specified?)
- 10% for style and documentation (is it readable and appropriately commented/cited?)
- 10% for incremental development (did you commit your changes regularly with descriptive commit messages?)
Refer to the style guide for a reference on style and documentation. If you use external resources such as Stack Overflow, ChatGPT, or a friend in the class, make sure to cite them in your comments. Failure to cite external resources will be considered plagiarism. An example of a citation is as follows:
// Jordan Pratt helped me with the logic for this loop
for (int i = 0; i < 10; i++) {
// ...
}
If your solution uses arrays, vectors, or other data structures or techniques not covered in class, you will receive a reduced grade, possibly as low as 0. If you have previous experience, try to challenge yourself to solve this problem using only the basics.
In addition, there will be an automatic 20% deduction if your code fails to compile or run. If the problem is extreme and I cannot fix it with a small change, a grade of 0 may be assigned. Make sure your code compiles and runs on INS with the following compiler flags:
$ g++ -ansi -pedantic-errors -Wall -Wconversion