# CSC148-Winter-2023-A1 Assignment 1 of CSC148 (Intro to Computer Science) University of Toronto 2023 Assignment 1: Forming Optimal Groups Introduction In the field of teaching university courses, a question that has received considerable attention is this: What is the best way to put students into groups? There are arguments for making groups heteregenous, and other arugments for making them homogeneous. What is best may depend on the kind of work, the size of the group, or what attributes we are basing the grouping on. For example, we might want groups with heterogeneous programs of study, so students bring different perspectives. Or we might want homogeneous neighbourhoods, so students from who live nearby can meet to work together in person. Or we may want a combination of these criteria. Of course, to apply criteria like these, we need the relevant information about the students (their program of study, or college, for instance). We can get that by surveying the students. Luckily, with your extensive programming knowledge that you have acquired from taking CSC148, you will be able to write a program that lets an instructor form student groups in the way that they think is best. Your task is to complete a program that analyzes an instructor’s criteria for good groups, plus data extracted from a student survey, to make groups that are optimal with respect to the criteria. We have broken down the assignment into several tasks that are outlined in detail in this handout. Learning Goals By the end of this assignment you should be able to: read complex code you didn’t write and understand its design and implementation, including: - understanding the class and method docstrings in detail (including attributes, representation invariants, preconditions, etc.) - determining relationships between classes, by applying your knowledge of composition and inheritance given a partial implementation of a class, complete it, including: - understanding the representation invariants and enforcing them - understanding the preconditions and appropriately factoring them in both when writing the body of the method and when writing client code - implementing the required methods according to their docstrings using inheritance to define a subclass - perform unit testing on a program with many interacting classes General Guidelines -You may complete this assignment individually or with a partner. -Please read this handout carefully and ask questions if there are any tasks you do not understand. -The tasks are not designed to be equally difficult, or even in increasing order of difficulty. They are just laid out in logical order. -Although implementing a complex application can be challenging at first, we will guide you through a progression of tasks, in order to gradually build pieces of your implementation. Your responsibility includes reading through this handout and the starter code we provide, carefully, and understanding how the classes work together in the context of the application. Coding Guidelines These guidelines are designed to help you write well-designed code that will adhere to the interfaces we have defined (and thus will be able to pass our test cases). You must: - write each method in such a way that the docstrings you have been given in the starter code accurately describe the body of the method. - write test methods where indicated. These will be marked, but we will not assess their thoroughness or their ability to fail on buggy code (as we have required on some of your Preps). We recommend that you go farther and write tests for all methods that you implement, even private ones. - You do NOT need to write doctests for the functions and methods in this assignment. The setup required to do the testing would create disruptively long docstrings. - incorporate inheritance into your code, as described in the Your Task section. - avoid writing duplicate code. You must NOT: - modify any code that you are given. In particular, you must not not: - change the parameters, parameter type annotations, or return types in any of the methods you have been given in the starter code. - add or remove any parameters in any of the methods you have been given in the starter code. - change the type annotations of any public or private attributes you have been given in the starter code. - create any new public attributes. - create any new public methods that do not override or extend a method of the same name in a parent class. - create any new top-level functions, that is, functions defined outside any class. - write code that mutates objects unnecessarily. - add any more import statements to your code. - create an alias between an attribute (that is of a mutable type) of a class and a parameter to a method; this could lead to nasty side effects. However, as discussed in Tasks 7 and 8, you will sometimes make a “shallow copy” that is not itself an alias but has aliasing within it. You may need to: - create new private methods for the classes you have been given. if you create new private methods you must provide type annotations for every parameter and return value. You must also write a full docstring for this method as described in the function design recipe Download function design recipe, except that you do not need to write any doctests, as noted above. - create new private attributes for the classes you have been given. if you create new private attributes you must give them a type annotation and include a description of them in the class’s docstring as described in the class design recipe Download class design recipe. - import more objects from the typing module While writing your code you can assume that all arguments passed to the methods you have been given in the starter code will respect the preconditions outlined in the methods’ docstrings. We have included a docstring for a __str__ method in many of the classes you will write. We have not specified what a string representation of these objects will look like; this is up to you to pick something that is useful to you in your debugging process. We will not be grading for any particular string representation. Your Task Complete the classes and methods given to you in the starter code to create a piece of software that keeps track of survey data and uses the results of that data to make optimal groups of students according to provided criteria. You are encouraged to complete this assignment in the order outlined in the tasks below but you may choose a different order if you wish. Make sure you complete each “Test your code!” section before moving on to the next task to make sure that your code works as expected. In each of the tasks below you are encouraged to read questions in the “Something to think about” sections. You are not required to answer these questions for this assignment but thinking about them might help you write better code! Task 1: Get the starter code and read the documentation Download the file a1.zip (to be provided shortly) that contains the starter code Unzip the file and place the contents in pycharm in your a1 folder (remember to set your a1 folder as a sources root) You should see the following files: course.py criterion.py grouper.py survey.py tests.py example_tests.py example_usage.py a folder called data containing several .json files (example_course.json, example_survey.json, generated_course_hetero.json, generated_course_lonely.json, generated_course.json, longer_survey_hetero.json, longer_survey_lonely.json, and longer_survey.json). For this assignment, you will be required to edit and submit the files that do not start with example_. If you look at these files you will notice that you have been given the headers and docstrings for many classes and methods, which describe how you are expected to implement them. You will complete them in the rest of this assignment’s tasks. Task 2: Complete the Student class in course.py The Student class represents a student who can be enrolled in a university course. What to do: - Implement the methods in the Student class as described in its method docstrings. - Write your own tests for each public method in the Student class in tests.py. You should have at least one test for each public method (other than __init__ and __str__). - None of the tests in example_tests.py will pass at this point. Remember: you may need to define additional private attributes or private helper methods! Note: The Student.has_answer method asks you to check if a student has a valid answer to a given question. We don’t yet have a way to determine if an answer is valid, and we won’t until we complete Task 5. You may need to come back and finish this method after completing Task 5. Task 3: Complete the Course class in course.py The Course class represents a university course. What to do: - Implement the methods in the Course class as described in its method docstrings. You may find the function sort_students helpful. - Write your own tests for each public method in the Course class in tests.py. You should have at least one test for each public method (other than __init__). - Run the tests in example_tests.py, and ensure that TestCourse.test_enroll_students and TestCourse.test_get_students have passed. None of the other tests will pass yet. Note: The Course.all_answered method asks you to check if all students have a valid answer for every question in a Survey. You may have to come back later to finish the Course.all_answered method after you have completed the Survey class. Task 4: Complete the question classes in survey.py The file survey.py contains an abstract Question class, and the following classes for representing different types of questions that you might find on a survey: MultipleChoiceQuestion NumericQuestion YesNoQuestion CheckboxQuestion Each of these classes defines the text of a question, and specifies what are valid answers to that particular type of question. We have not defined any inheritance hierarchy between these classes. You get to decide what it should be. What to do: -Define an inheritance hierarchy between these classes, following these rules: The abstract class Question must be a superclass of all the others. All other question classes must inherit from the abstract class Question either directly or indirectly. At least one non-abstract question class should inherit from another non-abstract question class. This means that your inheritance hierarchy will have more than two levels. There are many possible inheritance structures you could choose. Remember that one of the requirements for this assignment is to avoid writing duplicate code. Think about which sort of inheritance structure best lets you avoid duplicate code. - Implement the methods in each of the question classes, as described in their method docstrings. You may remove a method that we included in the starter code in a child class if you want to inherit the method directly from the parent class, rather than override/extend it. Note: You may find the Python set type to be useful. - In tests.py, write your own tests for each public method in the YesNoQuestion class. You should have at least one test for each public method (other than __init__ and __str__). You need to write tests for inherited methods, even if they are not overridden in the YesNoQuestion class. For example, even if you structure your code so that the YesNoQuestion class inherits its validate_answer method without modification from another class, you still need to write tests for the validate_answer method when called on an instance of YesNoQuestion. - Optional but recommended: Write your own tests in tests.py for each public method in the other question classes (except for the abstract class Question). Run the tests in example_tests.py, and ensure that TestStudent.test_set_answer and TestStudent.test_get_answer also now pass. Note: The validate_answer methods ask you to check if an answer is a valid answer for this question. You might not have enough information about the Answer class in order to complete this method now, and you may need to come back and finish this method after completing Task 5. Task 5: Complete the Answer class in survey.py The Answer class represents an answer to one of the questions you wrote classes for in Task 4. (By “answer” we mean a response someone might give to the question, not the correct answer. Notice that we don’t record a correct answer anywhere! This is because our surveys are meant to gather information about students, not as tests.) What to do: - Implement the methods in the Answer class as described in its method docstrings. - Write your own tests for the public method Answer.is_valid in tests.py. If you have not implemented the validate_answer methods in the question classes, or the Course.all_answered and the Student.has_answer methods yet, go back and finish them now. - Run the tests in example_tests.py, and ensure that all tests in the TestStudent, TestAnswer, TestMultipleChoiceQuestion, TestNumericQuestion, TestYesNoQuestion, and TestCheckboxQuestion classes now pass. Task 6: Complete the criterion classes in criterion.py A criterion is a way of judging the quality of a group based on the group members’ answers to a particular question. (The plural of criterion is criteria.) For example, one criterion could be to want groups with homogeneous (i.e. the same) answers to a question asking what year they are in. Another criterion could be to want groups to have heterogeneous (i.e. different) answers to another question. The criterion classes are the following: Criterion HomogeneousCriterion HeterogeneousCriterion LonelyMemberCriterion We have not defined any inheritance hierarchy between these classes. You get to decide what it should be. What to do: - Define an inheritance hierarchy between these classes, following these rules: The abstract class Criterion must be a superclass of all the others. All other criterion classes should inherit from the abstract class Criterion, either directly or indirectly. At least one non-abstract criterion class should inherit from another non-abstract criterion class. This means that your inheritance hierarchy will have more than two levels. There are many possible inheritance structures you could choose. Remember that one of the requirements for this assignment is to avoid writing duplicate code. Think about which sort of inheritance structure best lets you avoid duplicate code. - Implement the score_answers method in each of the criterion classes, as described in their method docstrings. You should NOT implement an initializer for these classes. Consequently, you shouldn’t define any additional public or private attributes for any of these classes. Remember: You may remove a method defined in a child class if you wish to simply inherit the parent’s method directly. - Write your own tests for the HomogeneousCriterion.score_answers method in tests.py. You need to write these tests even if you decide to inherit score_answers from its parent class without overriding it. - Optional but recommended: Write your own tests in tests.py for the score_answers method in the other criterion classes (except for the abstract class Criterion). - Run the tests in example_tests.py, and ensure the tests in the TestHomogeneousCriterion, TestHeterogeneousCriterion, and TestLonelyMemberCriterion classes now pass. Task 7: Complete the Group class in grouper.py The Group class represents a collection of one or more students. What to do: - Implement the methods in the Group class as described in its method docstrings. - Write your own tests for each public method in the Group class in tests.py, other than __init__ and __str__. - Run the tests in example_tests.py and ensure the tests in the TestGroup class now pass. Task 8: Complete the Grouping class in grouper.py The Grouping class represents a collection of Group instances. An instance of a Grouping class can be used to represent every student in a course, divided up into groups. What to do: - Implement the methods in the Grouping class as described in its method docstrings. - Write your own tests for the Grouping.add_group method in tests.py. - Optional but recommended: Write your own tests in tests.py for the __len__ and get_groups methods from the Grouping class. - Run the tests in example_tests.py and ensure the tests in the TestGrouping class now pass. Task 9: Complete the Survey class in survey.py The Survey class represents a collection of questions. It also associates each question with a criterion (indicating how to judge the quality of a group based on their answers to the question) and a weight (indicating the importance of this question in deciding how to group students). What to do - Implement the methods in the Survey class as described in its method docstrings. Hint: Read the documentation for Survey._get_criterion and Survey._get_weight carefully before you implement the initializer. Write at least one of your own tests for each public method in the Survey class in tests.py, other than the __init__, __str__, __len__, and __contains__ methods. - Run the tests in example_tests.py, and ensure that all tests in the TestSurvey and TestCourse classes now pass. Task 10: Complete the grouper classes in grouper.py The grouper classes represent different techniques for deciding how to split all the students in a course into groups. You will be implementing three different groupers, each using a different algorithm. The file Groupers.pdf Download Groupers.pdfwalks through a detailed example for each of the groupers. Make sure you understand the algorithms before you start writing code. Note that this task is intended to be the most algorithmically challenging part of the assignment. We encourage you to make sure you have completed the other tasks before investing significant time in this one. The grouper classes are the classes in grouper.py that have “Grouper” in their name: Grouper AlphaGrouper GreedyGrouper SimulatedAnnealingGrouper Unlike the criterion classes and question classes, the inheritance structure between the grouper classes has been given to you. You should NOT change the inheritance structure between the grouper classes. What to do: - Implement each of the methods in the grouper classes as described in the method docstrings, using the algorithms in the Grouper Explanation.pdf file (which provides more detail than the docstrings). Note that we have provided the abstract Grouper class for you, and that some of the other grouper classes use the inherited initializer from the parent class. You need to write the initializer only for SimulatedAnnealingGrouper, and you may find it helpful to add private attributes and/or methods to that class. You may find the function sort_students from the course.py file helpful. There are also several helper functions defined in grouper.py for you to use. Read their docstrings carefully, but you are not expected to understand all of the code in these helper functions. You may assume that there are more students in a course than the group size, however the group size may not evenly divide the number of students. That is, you can assume that there will be enough students to form at least two groups, and that you may end up with one group smaller than the “ideal” group size. - Write at least one test in tests.py for each of the AlphaGrouper.make_grouping and GreedyGrouper.make_grouping methods. - Optional but recommended: Write additional tests in tests.py for the AlphaGrouper.make_grouping and GreedyGrouper.make_grouping methods. Note that it will be more challenging to write tests for SimulatedAnnealing.make_grouping, since that method involves randomness. - Run the tests in example_tests.py and ensure that all tests in the TestAlphaGrouper, TestGreedyGrouper, and TestSimulatedAnnealingGrouper classes now pass. Task 11: Test your code again! Now that you have finished writing all the code, go back and run all the tests again. We strongly recommend you do much more testing than we have required above to convince yourself that your code is working correctly. The example_tests.py file might catch some bugs in your code but it will certainly not catch them all. Ideally you would write up thorough tests for every public method in the tasks above in pytest. We have only required a small number of the tests that you should be running on your code, in order to balance giving you some practice writing proper unit tests with letting you use other less formal testing methods if you choose. We hope it is clear by now that we think testing is important! Using example_usage.py At this point, you can also run the example_usage.py file again. This file should now run without errors (however this does not mean your code is bug-free). The example_usage.py file will create a course and a survey and will use that survey to group the students into groups using the three different grouper classes. It generates a visualization of the results as a HTML file that you can open with your web browser. To generate different groups, you can experiment with setting the values of the variables example and group_size on the first two lines of the if __name__ == '__main__' block. You can also try changing the number of iterations and initial temperature used to initialize the SimulatedAnnealingGrouper. The visualization HTML file contains a plot of the scores of the groups created by the different algorithms, as well as some simple statistics about how well they do. This visualization is just for your own exploration of the algorithms, you do not need to do anything with it. This project was completed on Pycharm. This project was given a grading of 90.81% (Class Average: 84.2%)