Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rebased Algorithm Documentation Update #938

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 44 additions & 11 deletions src/schedule-generator/README.md
Original file line number Diff line number Diff line change
@@ -1,37 +1,70 @@
# schedule-generator

This folder contains everything necessary to the functionality of the schedule-generation _algorithm_, for the new semesterly schedule-generation feature launching SP24.
This folder contains everything necessary to the functionality of the schedule-generation _algorithm_, for the new semesterly schedule-generation feature launching SP24. This is for use by the components in the `src/components/ScheduleGenerate` folder.

Below is a detailed file-by-file breakdown:

## `algorithm.ts`

This is essentially what is imported and used by the frontend. It takes in a request (type `GeneratorRequest`, see `generator-request.ts`) and outputs a meaningful value of type `GeneratedScheduleOutput` which can be used to see what courses fulfill what requirements and are at what times.

Currently the algorithm functions by randomly shuffling potential courses the user inputted, and then generating the first valid schedule that can be constructed through iteration through this randomly shuffled list.
Currently the algorithm functions by randomly shuffling potential courses the user inputted, and then generating the first valid schedule that can be constructed by iterating through this randomly shuffled list. A "valid schedule" is one that is under the credit limit and does not duplicate courses. Valid schedules are constructed by iterating through the shuffled list of courses and adding them to the schedule if:

- they do not fulfill a requirement that has already been fulfilled
- they are not some duplicate of a course already in the generated schedule
- they do not push the schedule over the credit limit
- the course is offered in the upcoming semester
- there is enough time to get to (one of) the course's offered timeslots given the current schedule and including a 15-minute gap between classes

If a course is added to the schedule, the requirement it fulfills is marked as fulfilled and the course is marked as taken, and the current number of credits is alos updated. The algorithm continues until the credit limit is reached or all courses have been iterated through.

Please note that the frontend generates five schedules at once, for paging through. This is done by calling the algorithm five times, but with a different random seed, as JavaScript's `Random` library uses system time. However, especially for smaller problems, this means that some generated schedules might be the same — a potential avenue for improvement down the line would be to have a somewhat more sophisticated algorithm that allows for the generation of `n` unique solutions, though `<n` might occur if `n` are not found (after some `>n` number of iterations).

There are a couple of specificities that one should be aware of in relation to the algorithm's functionality:

- a `Set` is used to guarantee that no course is duplicated
- if a user wants to take a course twice (e.g. CS 4999), they should make two separate requirement groups and add the course to each group
- another `Set` is used to guarantee that no _requirement_ is fulfilled multiple times
- previously there was no cap on this, but now we guarantee that an outputted schedule has <= 1 fulfillment of each requirement
- if a user wants to fulfill the same requirement multiple times (e.g. take multiple liberal studies, potentially), then they should follow the procedure above of duplicating requirement groups

Please note that GitHub Actions may complain about some `console.log` functions inside of `algorithm.ts`. This can be safely ignored — these `console.log`s are necessary and only called in the context of the `prettyPrintSchedule` function in a backend test.

## `course-unit.ts`

A `CourseUnit` is a class that represents a single course _in a single semester and "meta-timeslot"_. Example:
A `CourseUnit` is a class that represents a single course.

Say we have one lecture L and two discussions D1 and D2. Then there would be generated:
As shown in `testing.ts`, constructing a `CourseUnit` is somewhat involved due to all the associated frontend parameters, especially for the PDF generator. These include color, time, offered semesters, etc.

```typescript
Course(name=L, timeslots=[LectureTimeslot, D1Timeslot], ...)
Course(name=L, timeslots=[LectureTimeslot, D2Timeslot], ...)
new Course(
L,
'#FFFFF',
3,
[
{
daysOfTheWeek: ['Monday', 'Wednesday'],
start: '10:00 AM',
end: '11:30 AM'
}
],
['Fall', 'Spring'],
[coreClass, techElective]
);
```

This is done to aid in the random-shuffling algorithm as well as overlap-checking mechanism (in `algorithm.ts`).

A `Course` has associated with it 1-7 days of the week (hence why we need multiple `Course`s for a single class representation, as some may meet on different days — this is the easiest way to represent this).

## `generator-request.ts`

Just a narrow wrapper around the information you send to `algorithm.ts` for schedule generation. It stores the user's inputted courses to fulfill requirements, desired requirements to be fulfilled, the name of the semester, and a maximum amount of credits.

## `requirement.ts`

A super-simple class that just stores the "`name`" of a class, as well as the type of the requirement (e.g. "College") and its typeValue (e.g. "CS").
A simple class that just stores the "`name`" of a class, as well as the `type` of the requirement (e.g. `"College"`) and its `typeValue` (e.g. `"CS"`).

These parameters are required to:

- track which requirements are being fulfilled
- pass to the PDF downloader enough information to generate tables with requirement fulfillment information — see `src/tools/export-plan/pdf-schedule-generator.ts`

## `testing.ts`

Expand Down
64 changes: 58 additions & 6 deletions src/schedule-generator/algorithm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@ import Course, { Timeslot } from './course-unit';
import GeneratorRequest from './generator-request';
import Requirement from './requirement';

/**
* The output of the schedule generator.
*
* @param semester The semester for which the schedule is generated.
* @param schedule A map of courses to their respective timeslots.
* @param fulfilledRequirements A map of course codes to the requirements they fulfill.
* @param totalCredits The total number of credits in the schedule.
*/
export type GeneratedScheduleOutput = {
semester: string;
schedule: Map<Course, Timeslot[]>;
Expand All @@ -10,29 +18,47 @@ export type GeneratedScheduleOutput = {
totalCredits: number;
};

/**
* A class (based off of Java OOP architecture) that generates a valid semester schedule based on a
* list of courses and their respective timeslots, requirements, and other information.
*/
export default class ScheduleGenerator {
/**
* A directly-accessible method that generates a schedule for some desired semester given
* a list of courses, requirement info, and a credit limit.
*
* @param request An instance of a class containing the necessary information to generate a schedule.
* @returns The generated schedule.
*/
static generateSchedule(request: GeneratorRequest): GeneratedScheduleOutput {
const { classes, semester } = request;
let { creditLimit } = request;

const schedule: Map<Course, Timeslot[]> = new Map();
const fulfilledRequirements: Map<string, Requirement[]> = new Map(); // used for checking no course duplicates
const fulfilledRequirementsByCourse: Map<string, Requirement[]> = new Map(); // used for checking no course duplicates
const actualFulfilledRequirements: Set<string> = new Set(); // used for checking no requirement duplicates

// Randomly shuffle the list of available courses
classes.sort(() => Math.random() - 0.5);

// Randomly shuffle the course timeslots for more variability
classes.forEach(course => {
course.timeslots.sort(() => Math.random() - 0.5);
});

let totalCredits = 0;

classes.forEach(course => {
if (course.offeredSemesters.includes(semester)) {
let performAdditionFlag = true;
let performAdditionFlag = true; // whether we can use this course or not
// onlyCourseRequirement serves to doubly-ensure that only one requirement is being mapped
// to each course (because the courses are being dragged under single requirement groups)
const onlyCourseRequirement = course.requirements[0].name ?? 'nonsense-requirement';

// New logic: must be free for *all* time slots.
if (
actualFulfilledRequirements.has(onlyCourseRequirement) ||
fulfilledRequirements.has(course.code) ||
fulfilledRequirementsByCourse.has(course.code) ||
creditLimit - course.credits < 0
) {
performAdditionFlag = false;
Expand All @@ -54,17 +80,28 @@ export default class ScheduleGenerator {
ScheduleGenerator.addToSchedule(schedule, course, course.timeslots);
creditLimit -= course.credits;
totalCredits += course.credits;
fulfilledRequirements.set(course.code, course.requirements);
fulfilledRequirementsByCourse.set(course.code, course.requirements);
for (const requirement of course.requirements) {
actualFulfilledRequirements.add(requirement.name);
}
}
}
});

return { semester, schedule, fulfilledRequirements, totalCredits };
return {
semester,
schedule,
fulfilledRequirements: fulfilledRequirementsByCourse,
totalCredits
};
}

/**
* A helper static function that console.logs a pretty-printed text version of the schedule.
* Useful for debugging and testing.
*
* @param output The output of the schedule generator.
*/
static prettyPrintSchedule(output: GeneratedScheduleOutput): void {
console.log('************************');
console.log(`Generated Schedule for ${output.semester}:`);
Expand Down Expand Up @@ -92,12 +129,19 @@ export default class ScheduleGenerator {
console.log(`Total Credits in the Schedule: ${output.totalCredits}`);
}

/**
* A helper function to check if a timeslot is occupied in the schedule.
*
* @param schedule Information about the currently built-up schedule
* @param timeslot The timeslot to check for overlap
* @returns Whether the timeslot is occupied or not
*/
private static isTimeslotOccupied(
schedule: Map<Course, Timeslot[]>,
timeslot: Timeslot
): boolean {
// Check for overlap.
const gap = 15 * 60 * 1000; // 15 minutes in milliseconds
const gap = 15 * 60 * 1000; // 15 minutes in milliseconds; need a 15 min gap for walking
const timeslotCopy = { ...timeslot };

if (!timeslotCopy.start.includes(' ')) {
Expand Down Expand Up @@ -137,6 +181,14 @@ export default class ScheduleGenerator {
return false;
}

/**
* A helper function that adds to our map of courses to timeslots some new
* course and its respective timeslots.
*
* @param schedule The schedule to add the course to
* @param course The course to add
* @param timeslots The timeslots to add
*/
private static addToSchedule(
schedule: Map<Course, Timeslot[]>,
course: Course,
Expand Down
39 changes: 28 additions & 11 deletions src/schedule-generator/course-unit.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import Requirement from './requirement';

/**
* Represents a potential day (or days) in the week that a course might be offered.
*/
export type DayOfTheWeek =
| 'Monday'
| 'Tuesday'
Expand All @@ -9,15 +12,23 @@ export type DayOfTheWeek =
| 'Saturday'
| 'Sunday';

/**
* Represents a potential time slot for a course.
*/
export type Timeslot = {
daysOfTheWeek: DayOfTheWeek[]; // e.g. ['Monday', 'Wednesday', 'Friday']
start: string; // e.g. '10:00 AM'
end: string; // e.g. '11:30 AM'
};

/**
* Represents a course that can be taken by a student, as 'massaged' into a
* format that is more easily consumed by the frontend (and in particular, the
* PDF downloader).
*/
export type CourseForFrontend = {
title: string; // title
code: string; // math 2940
title: string; // e.g. Linear Algebra for Engineers
code: string; // e.g. MATH 2940
color: string;
courseCredits: number;
fulfilledReq: Requirement;
Expand All @@ -26,6 +37,16 @@ export type CourseForFrontend = {
timeEnd: string;
};

/**
* Represents a course that can be taken by a student.
*
* @param code The course code (e.g. MATH 2940).
* @param color The color of the course (for the PDF generator).
* @param credits The number of credits the course is worth.
* @param timeslots The timeslots that the course is offered.
* @param offeredSemesters The semesters in which the course is offered.
* @param requirements The requirements that the course fulfills.
*/
export default class Course {
code: string;

Expand All @@ -36,15 +57,6 @@ export default class Course {
/*
*all* of these have to be available for the course to be scheduled.
will usually just be one, but if there is e.g. a lab or a discussion then it could be two
for now, just going to have multiple copies of the course.

say we have one lecture L and two discussions D1 and D2.
then there would be generated:

Course(code=L, timeslots=[LectureTimeslot, D1Timeslot])
Course(code=L, timeslots=[LectureTimeslot, D2Timeslot])

then the algorithm will just choose one of these to schedule
*/
timeslots: Timeslot[];

Expand All @@ -68,6 +80,11 @@ export default class Course {
this.requirements = requirements;
}

/**
* A helper function that returns a string representation of the course.
*
* @returns A string representation of the course.
*/
toString(): string {
return `${this.code}:
-------------------
Expand Down
11 changes: 11 additions & 0 deletions src/schedule-generator/generator-request.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
import Course from './course-unit';
import Requirement from './requirement';

/**
* Represents a request to the schedule generator, containing all of the information
* necessary to construct a valid schedule according to user specification in the
* /build page.
*
* @param classes The list of courses that the student is willing to have fulfill requirements.
* @param requirements The list of requirements that the student wants to fulfill.
* @param creditLimit The maximum number of credits that the student is willing to take.
* @param semester The semester for which the schedule is being generated.
* @returns A new instance of the GeneratorRequest class that contains this data collated.
*/
export default class GeneratorRequest {
classes: Course[];

Expand Down
10 changes: 9 additions & 1 deletion src/schedule-generator/requirement.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
/**
* This class represents a requirement for a college, major, minor, grad school, or the university.
* It is one of the requirements that a student indicates they want to fulfill on the build page.
*
* @param name The name of the requirement (e.g. Mathematics).
* @param forType The type of requirement (e.g. College, Major, Minor, Grad, Uni).
* @param typeValue The specific 'subtype' of the requirement, for the PDF generator, e.g. 'CoE'.
*/
export default class Requirement {
name: string; // effectively name
name: string;

for: 'College' | 'Major' | 'Minor' | 'Grad' | 'Uni';

Expand Down
Loading
Loading