Skip to content

Algorithms

JamesyJi edited this page Dec 20, 2021 · 5 revisions

Note to future teams: These algorithms will require a lot of tweaking to adapt to constantly changing requirements in the future. It is possible that some information is out of date so make sure to check the page history and constantly update this wiki.

Contents

Prior Reading

In order to understand this, you should ensure you have read:

  • Tokenising Requirements
  • Manual Fixes to Course Prerequisites

Overview

The goal of the algorithms is to determine if a specific course has been unlocked for the user based on their taken program, specialisation, taken courses, uoc, wam, and much more. It has been implemented using 3 main classes - Condition, Category, User. We will go into detail on this later.

Have a look at the following example:

user = User(some_user_data)
comp2521_tokens = ["(", "COMP1511", "||", "DPST1091", "||", "COMP1917", "||", "COMP1921", ")"]
comp2521_cond = create_condition(comp2521_tokens, "COMP2521")

res = comp2521_cond.is_unlocked(user)
res["result"] # True/False if this course can be taken by the user
res["warnings"] # List of warnings (for now, it only applies to GRADE and WAM requirements)

Firstly, a User is created from some given user data (including things like what courses they have already taken). The Condition object is created via create_condition() which takes in the condition tokens and the course the condition applies to. We then pass in the User to is_unlocked() to determine if the user can take this course or not.

Classes

Inside conditions.py and categories.py, you can find implementations for many different Condition and Category classes. The exact details are best learnt via reading through the code and playing around with it on a separate branch.

Conditions

There are many types of conditions in UNSW involving courses, co-requisites, UOC, WAM and more... Each condition has a validate() method which checks if that condition is met. For example, "12UOC" would generate a UOCCondition with the uoc field set to 12. Calling validate() will check if the given user has taken at least 12 units of courses.

For complex conditions, we have a CompositeCondition class. This class can contain multiple Conditions (including other CompositeConditions) and be of "AND" or "OR" logic. If "AND" logic, then it will check if all the Conditions validate() as true. Similarly, "OR" will check if any of the Conditions validate() as true.

At the time of writing, there is a FirstCompositeCondition class which is the highest level CompositeCondition. It contains an is_unlocked() method which is crucial for the warning checking implementation. Please read the code.

Categories

Many courses have requirements with the "in" keyword. E.g. "12UOC in L2 MATH". For this reason, the Category class was developed. Some Conditions might contain a Category. Again, it is best to read the code but a brief overview of this logic for the above example is:

  1. Create UOCCondition with 12UOC
  2. Attach a LevelCourseCategory with L2 and MATH
  3. The Category Class has several useful methods. In this case, it can get the UOC the user has taken belonging to its specific category, i.e. the total UOC in courses of format MATH2XXX.
  4. The UOCCondition will use this Category to check the applicable UOC of the user when in order to validate() itself.

User

There is a User class. The frontend will pass some user data to our API which then creates a User from that data. This User will be passed to our Condition object's is_unlocked() method to determine if the User can take this course or not.

unselect_course() and problems loading condition objects

This is a tricky method in User which I thought I would elaborate on. When someone unselects a course on the frontend, we need to return a list of courses that could potentially be affected. E.g. unselecting COMP1511 would "affect" COMP2521 since COMP1511 is a prerequisite of COMP2521. It makes sense that the user should know that unselecting COMP1511 should unselect COMP2521.

NOTE: One method people often jump to is to think of the courses as some sort of dependency graph. Think about why this is difficult (hint, what if a course has a UOC condition such as 48UOC? Which courses does this depend on?).

Firstly, have a read of the unselect_course() method. You will notice that we need access to the Condition objects for the courses. But where do we get these Condition objects? There is a conditions.pkl file which stores all the Conditions (so we don't have to recreate them each time...) but due to problems with pickle and importing* this failed. A workaround I've done is to load all the condition tokens, then have unselect_course() create the necessary Conditions each time. Please read the code.

*Read up on this if you're interested, I think the solution requires separating the User from conditions.py or with absolute path imports or something but then you'd have to make sure this works with docker and etc my brain is too small and there is no time someone in the future pls fix this.

Creating a Condition from Tokens

Have a read through the create_condition function. Tokens are read from left to right and then Conditions and Categories are created recursively. We can attach an optional parameter that is the course this condition belongs to. This is needed to check for exclusions since we must know the course in order to know what it excludes.