# Making a Class Schedule Using a Genetic Algorithm

Wednesday Feb 20th 2008 by Mladen Jankovic
Share:

Learn how to make a class schedule by using a genetic algorithm.

### Introduction

Making a class schedule is one of those NP hard problems. The problem can be solved using heuristic search algorithm to find optimal solution, but it works only for simple cases. For more complex inputs and requirements, finding a considerably good solution can take a while or it may be impossible. This is where genetic algorithms come into the game. In this article, I assume that you are familiar with the basic concepts of genetic algorithms, and won't describe them in details because it has been done so many times before.

### Background

When you make a class schedule, you must take into consideration many requirements (number of professors, students, classes and classrooms, size of classroom, laboratory equipment in classroom and many other). These requirements can be divided into several groups by their importance.

Hard requirements (if you break one of these, the schedule is infeasible):

• The class can be placed only in a spare classroom.
• No professor or student group can have more then on class at one time.
• The classroom must have enough seats to accommodate all students.
• To place a class in a classroom, the classroom must have laboratory equipment (computers in your case) if the class requires it.

Some soft requirements (can be broken but schedule is still feasible):

• Preferred time of class by professors.
• Preferred classroom by professors.
• Distribution (in time or space) of classes for student groups or professors.

Hard and soft requirements, of course, depend on the situation. In this example, only hard requirements are implemented. Let me start by explaining the objects on the class schedule.

### Objects of the Class Schedule

#### Professor

The Professor class has the ID and name of professor. It also contains a list of classes that the professor teaches.

#### Student Group

The StudentsGroup class has the ID and name of the student group, as well as the number of students (size of group). It also contains a list of classes that the group attends.

#### Classroom

The Room class has the ID and name the of classroom, as well as the number of seats and information about equipment (computers). If the classroom has computers, it is expected that there is a computer for each seat. IDs are generated internally and automatically.

#### Course

The Course class has the ID and name of each course.

#### Class

CourseClass holds a reference to the course to which the class belongs, reference to the professor who teaches the class, and a list of student groups that attend the class. It also stores how many seats (sum of student groups' sizes) are needed in the classroom, if the class requires computers in classroom and the duration of class (in hours).

#### Chromosome

The first thing you should consider when you deal with genetic algorithms is how to represent your solution in such a way that is feasible for genetic operations such as crossover and mutation. Also, you should know how to specify how good your solution is. In other words, you should be able to calculate the fitness value of your solution.

#### Representation

How do you represent chromosome for the class schedule? You need a slot (time-space slot) for each hour (assume that time is in one hour granules), for every room of every day. Also, assume that classes cannot begin before 9am and should finish before or at 9pm (12 hours total) and working days are from Monday to Friday (5 days total). You can use std::vector with size 12*5*number_of_rooms. Slot should be std::list because, during execution of your algorithm, you allow multiple classes at the same time-space slot.

There is an additional hash map that is used to obtain the first time-space slot at which class begins (its position in vector) from the address of a class's object. Each hour of a class has a separate entry in the vector, but there is only one entry per class in the hash map. For instance, if a class starts at 1pm and lasts for three hours, it has entries in the 1pm, 2pm, and 3pm slots.

Figure 1: Chromosome Representation

Chromosomes are represented by the Schedule class. It stores representation of the class schedule in these two attributes:

```// Time-space slots, one entry represent one hour in one classroom
vector<list<CourseClass*>> _slots;

// Class table for chromosome
// Used to determine first time-space slot used by class
hash_map<CourseClass*, int> _classes;
```

Additionally, the chromosome should store its fitness value and parameters that are used by genetic operations.

The fitness value is stored here:

```// Fitness value of chromosome
float _fitness;

// Flags of class requiroments satisfaction
vector<bool> _criteria;
```

Chromosome parameters:

```// Number of crossover points of parent's class tables
int _numberOfCrossoverPoints;

// Number of classes that is moved randomly by single mutation
// operation
int _mutationSize;

// Probability that crossover will occur
int _crossoverProbability;

// Probability that mutation will occur
int _mutationProbability;
```

#### Fitness

Now, you need to assign a fitness value to the chromosome. As I previously said, only hard requirements are used to calculate the fitness of the class schedule. This is how you do it:

• Each class can have from 0 to 5 points.
• If the class uses a spare classroom, you increment its score.
• If the class requires computers and they are located in the classroom, or it doesn't require them, you increment the score of the class.
• If the class is located in a classroom with enough available seats, guess what, you increment its score.
• If the professor has no other classes at the time, you increment the class's score once again.
• The last thing that you check is whether any of the student groups that attend class has no other classes at same time and, if they don't, you increment the score of the class.
• If the class braeks a rule at any time-space slot that it occupies, its score is not incremented for that rule.
• The total score of the class schedule is the sum of points of all classes.
• The fitness value is calculated as schedule_score/maximum_score; the maximum_score is number_of_classes*5.

Fitness values are represented by a single-precision floating point number (float) in a range from 0 to 1.

#### Crossover

A crossover operation combines data in hash maps of two parents, and then it creates a vector of slots according to the content of the new hash map. A crossover 'splits' the hash maps of both parents into parts of random size. The number of parts is defined by the number of crossover points (plus one) in the chromosome's parameters. Then, it alternately copies the parts from the parents to the new chromosome, and forms a new vector of slots.

[cscross.png]

Figure 2: Crossover operation

```// Performs crossover operation using chromosomes and returns
// pointer to offspring
Schedule* Crossover(const Schedule& parent2) const;
```

### Mutation

The mutation operation is very simple. It just takes a class randomly and moves it to another randomly chosen slot. The number of classes that are going to be moved in a single operation is defined by the mutation size in the chromosome's parameters.

```// Performs mutation on chromosome
void Mutation();
```

### Algorithm

Genetic algorithm is fairly simple. For each generation, it performs two basic operations:

1. Randomly selects N pairs of parents from the current population and produces N new chromosomes by performing a crossover operation on each pair of parents.
2. Randomly selects N chromosomes from the current population and replaces them with new ones. The algorithm doesn't select chromosomes for the replacement if it is among the best chromosomes in the population.

These two operations are repeated until the best chromosome reaches a fitness value equal to 1 (meaning that all classes in the schedule meet the requirement). As mentioned before, genetic algorithm keeps track of the M best chromosomes in the population and guarantees that they are not going to be replaced while they are among the best chromosomes.

```// Genetic algorithm
class Algorithm
{

private:

// Population of chromosomes
vector<Schedule*> _chromosomes;

// Inidicates whether chromosome belongs to
// best chromosome group
vector<bool> _bestFlags;

// Indices of best chromosomes
vector<int> _bestChromosomes;

// Number of best chromosomes currently saved in
// best chromosome group
int _currentBestSize;

// Number of chromosomes which are replaced in
// each generation by offspring
int _replaceByGeneration;

// Pointer to algorithm observer
ScheduleObserver* _observer;

// Prototype of chromosomes in population
Schedule* _prototype;

// Current generation
int _currentGeneration;

// State of execution of algorithm
AlgorithmState _state;

// Synchronization of algorithm's state
CCriticalSection _stateSect;

// Pointer to global instance of algorithm
static Algorithm* _instance;

// Synchronization of creation and destruction
// of global instance
static CCriticalSection _instanceSect;

public:

// Returns reference to global instance of algorithm
static Algorithm& GetInstance();

// Frees memory used by global instance
static void FreeInstance();

// Initializes genetic algorithm
Algorithm(int numberOfChromosomes,
int replaceByGeneration,
int trackBest,
Schedule* prototype,
ScheduleObserver* observer);

// Frees used resources
~Algorithm();

// Starts and executes algorithm
void Start();

// Stops execution of algoruthm
void Stop();

// Returns pointer to best chromosomes in population
Schedule* GetBestChromosome() const;

// Returns current generation
inline int GetCurrentGeneration() const
{ return _currentGeneration; }

// Returns pointe to algorithm's observer
inline ScheduleObserver* GetObserver() const
{ return _observer; }

private:

// Tries to add chromosomes in best chromosome group

// Returns TRUE if chromosome belongs to best chromosome group
bool IsInBest(int chromosomeIndex);

// Clears best chromosome group
void ClearBest();

};
```

### Observing

The ScheduleObserver class handles events that are triggered by the genetic algorithm. This class you send messages to view the application's window. Also, you can block the caller's thread until execution of algorithm is not finished or stopped by calling the WaitEvent() method.

```// Handles event that is raised
// when algorithm finds new best chromosome
void NewBestChromosome(const Schedule& newChromosome);

// Handles event that is raised when state
// of execution of algorithm is changed
void EvolutionStateChanged(AlgorithmState newState);

// Block caller's thread until algorithm finishes execution
inline void WaitEvent()    //...
```

If you plan to change the NewBestChromosome method, keep in mind that if you want to keep the best chromosome to display it, you must make a hard copy (by using the MakeCopy method of the Schedule class), because the algorithm can delete that chromosome in the next generation.

### Configuration

#### Configuration file

Types of objects:

1. professor (#prof tag): Describes professor
2. course (#course tag): Describes course
3. room (#room tag): Describes room
4. group (#group tag): Describes student group
5. course class (#class tag): Describes class; binds professor, course, and student groups

Each object begins with its tag and finishes with an #end tag; all tags must be on separate lines. In the body of an object, each line contains only one key and value pair (attribute) separated by a = character. Each attribute should be specified just one time except for the group attribute in #group object; this can have multiple group attributes. The tag and key names are case sensitive. Here is a list of the objects' attributes:

1. #prof
• id (number, required): ID of the professor.
• name (string, required): Name of the professor.
2. #course
• id (number, required): ID of the course.
• name (string, required): Name of the course.
3. #room
• name (string, required): Name of the room.
• size (number, required): Number of seats in the room.
• lab (boolean, optional): Indicates whether the room is a lab (has computers). If not specified, the default value is false.
4. #group
• id (number, required): ID of the student groups.
• name (string, required): Name of the student groups.
• size (number, required): Number of students in the group.
5. #class
• professor (number, required): ID of a professor. Binds professor to class.
• course (number, required): ID of a course. Binds course to class.
• group (number, required): ID of a student group. Binds student group to class. Each class can be bound to multiple student groups.
• duration (number, optional): Duration of class (in hours). If not specified, the default value is 1.
• lab (boolean, optional): Whether the class requires computers in a room. If not specified, the default value is false.

Note that the professor, student group, and course objects must be defined before they are bound to the course class object.

#### Example of configuration file

```#prof
id = 1
name = John Smith
#end

#course
id = 1
name = Introduction to Programming
#end

#room
name = R1
lab = true
size = 24
#end

#group
id = 1
name = 1O1
size = 19
#end

#class
professor = 1
course = 1
duration = 2
group = 1
group = 2
#end

#class
professor = 1
course = 1
duration = 3
group = 1
lab = true
#end

#class
professor = 1
course = 1
duration = 3
group = 2
lab = true
#end
```

#### Parsing configuration

Parsing of the configuration file is done by the Configuration class. The ParseFile method opens and parses the configuration file. It searches for object tags and calls the appropriate method for each parsing object. The ParseFile method also clears a previously parsed object.

```public:

void ParseFile(char* fileName);

private:

Professor* ParseProfessor(ifstream& file);
StudentsGroup* ParseStudentsGroup(ifstream& file);
Course* ParseCourse(ifstream& file);
Room* ParseRoom(ifstream& file);
CourseClass* ParseCourseClass(ifstream& file);
```

To parse a file:

`Configuration::GetInstance().ParseFile( "GaSchedule.cfg" );`

Parsed objects, except course classes, are kept in a hash map so they can be accessed easily and quickly.

```private:

hash_map<int, Professor*> _professors;
hash_map<int, StudentsGroup*> _studentGroups;
hash_map<int, Course*> _courses;
hash_map<int, Room*> _rooms;

list<CourseClass*> _courseClasses;
```

The Configuration class also contains methods for retrieving parsed information and objects.

```public:
inline Professor* GetProfessorById(int id)    //...
inline int GetNumberOfProfessors() const      //...

inline StudentsGroup* GetStudentsGroupById(int id)    //...
inline int GetNumberOfStudentGroups() const           //...

inline Course* GetCourseById(int id)     //...
inline int GetNumberOfCourses() const    //...

inline Room* GetRoomById(int id)         //...
inline int GetNumberOfRooms() const      //...

inline const list<CourseClass*>& GetCourseClasses()
const    //...
inline int GetNumberOfCourseClasses() const    //...
```