Files
ortools-clone/examples/cpp/course_scheduling.cc
Corentin Le Molgat c34026b101 Bump copyright to 2025
note: done using
```sh
git grep -l "2010-2024 Google" | xargs sed -i 's/2010-2024 Google/2010-2025 Google/'
```
2025-01-10 11:33:35 +01:00

849 lines
35 KiB
C++

// Copyright 2010-2025 Google LLC
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "examples/cpp/course_scheduling.h"
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <limits>
#include <utility>
#include <vector>
#include "absl/container/flat_hash_set.h"
#include "absl/status/status.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_format.h"
#include "absl/strings/str_split.h"
#include "absl/types/span.h"
#include "ortools/base/logging.h"
#include "ortools/base/mathutil.h"
#include "ortools/base/numbers.h"
#include "ortools/base/status_macros.h"
#include "ortools/linear_solver/linear_solver.h"
#include "ortools/scheduling/course_scheduling.pb.h"
namespace operations_research {
absl::Status CourseSchedulingSolver::ValidateModelAndLoadClasses(
const CourseSchedulingModel& model) {
time_slot_count_ = model.days_count() * model.daily_time_slot_count();
room_count_ = model.rooms_size();
solve_for_rooms_ = room_count_ > 0;
// If there are no rooms given, we have to give room_count at least one in
// order for the loops creating the solver variables and constraints to work.
if (!solve_for_rooms_) {
room_count_ = 1;
}
// Validate the information given for each course.
for (const Course& course : model.courses()) {
if (course.consecutive_slots_count() != 1 &&
course.consecutive_slots_count() != 2) {
return absl::InvalidArgumentError(absl::StrFormat(
"The course titled %s has %d consecutive time slots specified when "
"it can only have 1 or 2.",
course.display_name(), course.consecutive_slots_count()));
}
if (course.teacher_section_counts_size() != course.teacher_indices_size()) {
return absl::InvalidArgumentError(
absl::StrFormat("The course titled %s should have the same number of "
"teacher indices and section numbers.",
course.display_name()));
}
for (const int teacher_index : course.teacher_indices()) {
if (teacher_index >= model.teachers_size()) {
return absl::InvalidArgumentError(absl::StrFormat(
"The course titled %s has teacher %d assigned to it but there are "
"only %d teachers.",
course.display_name(), teacher_index, model.teachers_size()));
}
}
for (const int room_index : course.room_indices()) {
if (room_index >= model.rooms_size()) {
return absl::InvalidArgumentError(
absl::StrFormat("The course titled %s is slotted for room index %d "
"but there are only %d rooms.",
course.display_name(), room_index, room_count_));
}
}
}
// Validate the information given for each teacher and create hash sets of the
// restricted indices for each teacher.
teacher_to_restricted_slots_ =
std::vector<absl::flat_hash_set<int>>(model.teachers_size());
for (int teacher_index = 0; teacher_index < model.teachers_size();
++teacher_index) {
for (const int restricted_slot :
model.teachers(teacher_index).restricted_time_slots()) {
if (restricted_slot >= time_slot_count_) {
return absl::InvalidArgumentError(
absl::StrFormat("Teacher with name %s has restricted time slot %d "
"but there are only %d time slots.",
model.teachers(teacher_index).display_name(),
restricted_slot, time_slot_count_));
}
teacher_to_restricted_slots_[teacher_index].insert(restricted_slot);
}
}
// Since each course can have multiple sections (classes), we need to
// "flatten" out each course so that each of its sections gets a unique index.
// This vector stores the information for calculating each unique index. The
// first value is the unique index the course's sections begin at. The second
// value is the total number of sections of that course.
course_to_classes_ = std::vector<std::vector<int>>(model.courses_size());
// For each teacher, store the class unique indices that they teach.
teacher_to_classes_ =
std::vector<absl::flat_hash_set<int>>(model.teachers_size());
// Store course indices of courses that have a single section.
absl::flat_hash_set<int> singleton_courses;
int flattened_course_index = 0;
for (int course_index = 0; course_index < model.courses_size();
++course_index) {
const Course& course = model.courses(course_index);
int total_section_count = 0;
for (int teacher = 0; teacher < course.teacher_indices_size(); ++teacher) {
for (int section = 0; section < course.teacher_section_counts(teacher);
++section) {
teacher_to_classes_[course.teacher_indices(teacher)].insert(
flattened_course_index);
course_to_classes_[course_index].push_back(flattened_course_index);
++flattened_course_index;
}
total_section_count += course.teacher_section_counts(teacher);
}
if (total_section_count == 1) {
singleton_courses.insert(course_index);
}
}
class_count_ = flattened_course_index;
// Validate the information given for each student. Store each student's
// course pairs.
for (const Student& student : model.students()) {
for (const int course_index : student.course_indices()) {
if (course_index >= model.courses_size()) {
return absl::InvalidArgumentError(absl::StrFormat(
"Student with name %s has course index %d but there are only %d "
"courses.",
student.display_name(), course_index, model.courses_size()));
}
}
InsertSortedPairs(std::vector<int>(student.course_indices().begin(),
student.course_indices().end()),
&course_conflicts_);
}
LOG(INFO) << "Number of days: " << model.days_count();
LOG(INFO) << "Number of daily time slots: " << model.daily_time_slot_count();
LOG(INFO) << "Total number of time slots: " << time_slot_count_;
LOG(INFO) << "Number of courses: " << model.courses_size();
LOG(INFO) << "Total number of classes: " << class_count_;
LOG(INFO) << "Number of teachers: " << model.teachers_size();
LOG(INFO) << "Number of students: " << model.students_size();
if (solve_for_rooms_) {
LOG(INFO) << "Number of rooms: " << model.rooms_size();
}
return absl::OkStatus();
}
CourseSchedulingResult CourseSchedulingSolver::Solve(
const CourseSchedulingModel& model) {
CourseSchedulingResult result;
const auto validation_status = ValidateModelAndLoadClasses(model);
if (!validation_status.ok()) {
result.set_solver_status(
CourseSchedulingResultStatus::SOLVER_MODEL_INVALID);
result.set_message(validation_status.message());
return result;
}
ConflictPairs class_conflicts;
result = SolveModel(model, class_conflicts);
if (result.solver_status() != SOLVER_FEASIBLE &&
result.solver_status() != SOLVER_OPTIMAL) {
return result;
}
const auto verifier_status = VerifyCourseSchedulingResult(model, result);
if (!verifier_status.ok()) {
result.set_solver_status(CourseSchedulingResultStatus::ABNORMAL);
result.set_message(verifier_status.message());
}
return result;
}
CourseSchedulingResult CourseSchedulingSolver::SolveModel(
const CourseSchedulingModel& model, const ConflictPairs& class_conflicts) {
CourseSchedulingResult result;
result = ScheduleCourses(class_conflicts, model);
if (result.solver_status() != SOLVER_FEASIBLE &&
result.solver_status() != SOLVER_OPTIMAL) {
if (result.solver_status() == SOLVER_INFEASIBLE) {
result.set_message("The problem is infeasible with the given courses.");
}
return result;
}
ConflictPairs class_conflicts_to_try = AssignStudents(model, &result);
if (class_conflicts_to_try.empty()) return result;
std::vector<std::pair<int, int>> conflicts(class_conflicts_to_try.begin(),
class_conflicts_to_try.end());
const int conflicts_count = conflicts.size();
const int conflicts_log = conflicts_count == 1 ? 1 : log2(conflicts_count);
for (int i = 0; i < conflicts_log; ++i) {
const int divisions = MathUtil::IPow<double>(2, i);
for (int j = 0; j < divisions; ++j) {
const int start = std::floor(conflicts_count * j / divisions);
const int end = std::floor(conflicts_count * (j + 1) / divisions);
ConflictPairs new_class_conflicts = class_conflicts;
if (end >= conflicts_count) {
new_class_conflicts.insert(conflicts.begin() + start, conflicts.end());
} else {
new_class_conflicts.insert(conflicts.begin() + start,
conflicts.begin() + end);
}
result = SolveModel(model, new_class_conflicts);
if (result.solver_status() == SOLVER_FEASIBLE ||
result.solver_status() == SOLVER_OPTIMAL) {
return result;
}
}
}
return result;
}
std::vector<int> CourseSchedulingSolver::GetRoomIndices(const Course& course) {
if (solve_for_rooms_) {
return std::vector<int>(course.room_indices().begin(),
course.room_indices().end());
}
return {0};
}
void CourseSchedulingSolver::InsertSortedPairs(absl::Span<const int> list,
ConflictPairs* pairs) {
for (int first = 1; first < list.size(); ++first) {
for (int second = first; second < list.size(); ++second) {
pairs->insert(std::minmax(list[first - 1], list[second]));
}
}
}
std::vector<absl::flat_hash_set<int>>
CourseSchedulingSolver::GetClassesByTimeSlot(
const CourseSchedulingResult* result) {
std::vector<absl::flat_hash_set<int>> time_slot_to_classes(time_slot_count_);
for (const ClassAssignment& class_assignment : result->class_assignments()) {
const int course_index = class_assignment.course_index();
const int section_number = class_assignment.section_number();
for (const int time_slot : class_assignment.time_slots()) {
time_slot_to_classes[time_slot].insert(
course_to_classes_[course_index][section_number]);
}
}
return time_slot_to_classes;
}
void CourseSchedulingSolver::AddVariableIfNonNull(double coeff,
const MPVariable* var,
MPConstraint* ct) {
if (var == nullptr) return;
ct->SetCoefficient(var, coeff);
}
CourseSchedulingResult CourseSchedulingSolver::ScheduleCourses(
const ConflictPairs& class_conflicts, const CourseSchedulingModel& model) {
LOG(INFO) << "Starting schedule courses solver with "
<< class_conflicts.size() << " class conflicts.";
MPSolver mip_solver("CourseSchedulingMIP",
MPSolver::SCIP_MIXED_INTEGER_PROGRAMMING);
const double kInfinity = std::numeric_limits<double>::infinity();
// Create binary variables x(n,t,r) where x(n,t,r) = 1 indicates that course n
// is scheduled for time slot t in course r. Variables are only created if
// attendees of class n are available for time slot t and if the course can be
// placed into room r.
std::vector<std::vector<std::vector<MPVariable*>>> variables(
class_count_,
std::vector<std::vector<MPVariable*>>(
time_slot_count_, std::vector<MPVariable*>(room_count_, nullptr)));
for (int proto_index = 0; proto_index < model.courses_size(); ++proto_index) {
const Course& course = model.courses(proto_index);
const int course_index = course_to_classes_[proto_index][0];
int total_section_index = 0;
for (int i = 0; i < course.teacher_section_counts_size(); ++i) {
const int teacher = course.teacher_indices(i);
const int section_count = course.teacher_section_counts(i);
for (int section = 0; section < section_count; ++section) {
for (int time_slot = 0; time_slot < time_slot_count_; ++time_slot) {
if (teacher_to_restricted_slots_[teacher].contains(time_slot)) {
continue;
}
for (const int room : GetRoomIndices(course)) {
variables[course_index + total_section_index][time_slot][room] =
mip_solver.MakeBoolVar(absl::StrFormat(
"x_%d_%d_%d", course_index + total_section_index, time_slot,
room));
}
}
++total_section_index;
}
}
}
std::vector<std::vector<MPVariable*>> intermediate_vars(
class_count_, std::vector<MPVariable*>(time_slot_count_));
for (int class_index = 0; class_index < class_count_; ++class_index) {
for (int time_slot = 0; time_slot < time_slot_count_; ++time_slot) {
MPConstraint* const ct = mip_solver.MakeRowConstraint(0, 0);
for (int room = 0; room < room_count_; ++room) {
if (variables[class_index][time_slot][room] == nullptr) continue;
ct->SetCoefficient(variables[class_index][time_slot][room], 1);
}
if (!ct->terms().empty()) {
intermediate_vars[class_index][time_slot] = mip_solver.MakeBoolVar(
absl::StrFormat("intermediate_%d_%d", class_index, time_slot));
ct->SetCoefficient(intermediate_vars[class_index][time_slot], -1);
}
}
}
// 1. Each course meets no more than its number of consecutive time slots a
// day.
for (int day = 0; day < model.days_count(); ++day) {
for (int course = 0; course < model.courses_size(); ++course) {
const int consecutive_slot_count =
model.courses(course).consecutive_slots_count();
for (const int class_index : course_to_classes_[course]) {
MPConstraint* const ct =
mip_solver.MakeRowConstraint(0, consecutive_slot_count);
for (int daily_time_slot = 0;
daily_time_slot < model.daily_time_slot_count();
++daily_time_slot) {
AddVariableIfNonNull(
1,
intermediate_vars[class_index]
[(day * model.daily_time_slot_count()) +
daily_time_slot],
ct);
}
}
}
}
// 2. Each course must meet the given number of times * its number of
// consecutive time slots.
for (int course = 0; course < model.courses_size(); ++course) {
const int meeting_count = model.courses(course).meetings_count();
const int consecutive_slot_count =
model.courses(course).consecutive_slots_count();
for (const int class_index : course_to_classes_[course]) {
MPConstraint* const ct =
mip_solver.MakeRowConstraint(meeting_count * consecutive_slot_count,
meeting_count * consecutive_slot_count);
for (int time_slot = 0; time_slot < time_slot_count_; ++time_slot) {
AddVariableIfNonNull(1, intermediate_vars[class_index][time_slot], ct);
}
}
}
// 3. Teachers are scheduled for no more than one course per time slot.
for (int time_slot = 0; time_slot < time_slot_count_; ++time_slot) {
for (int teacher = 0; teacher < model.teachers_size(); ++teacher) {
MPConstraint* const ct = mip_solver.MakeRowConstraint(0, 1);
for (const int class_index : teacher_to_classes_[teacher]) {
AddVariableIfNonNull(1, intermediate_vars[class_index][time_slot], ct);
}
}
}
// 4. Each room can only be occupied by one course for each time slot.
if (solve_for_rooms_) {
for (int time_slot = 0; time_slot < time_slot_count_; ++time_slot) {
for (int room = 0; room < room_count_; ++room) {
MPConstraint* const ct = mip_solver.MakeRowConstraint(0, 1);
for (int class_index = 0; class_index < class_count_; ++class_index) {
AddVariableIfNonNull(1, variables[class_index][time_slot][room], ct);
}
}
}
}
// 5. Ensure each class is scheduled for the correct number of consecutive
// time slots.
for (int course = 0; course < model.courses_size(); ++course) {
const int consecutive_slot_count =
model.courses(course).consecutive_slots_count();
if (consecutive_slot_count == 1) continue;
for (const int class_index : course_to_classes_[course]) {
for (int day = 0; day < model.days_count(); ++day) {
for (int room = 0; room < room_count_; ++room) {
// If only the previous time slot is chosen, force the current time
// slot to be chosen.
for (int daily_time_slot = 0;
daily_time_slot < model.daily_time_slot_count();
++daily_time_slot) {
MPConstraint* const ct = mip_solver.MakeRowConstraint(0, kInfinity);
const int time_slot_offset =
day * model.daily_time_slot_count() + daily_time_slot;
if (daily_time_slot > 0) {
AddVariableIfNonNull(
1, variables[class_index][time_slot_offset - 1][room], ct);
}
AddVariableIfNonNull(
-0.5, variables[class_index][time_slot_offset][room], ct);
if (daily_time_slot < model.daily_time_slot_count() - 1) {
AddVariableIfNonNull(
0.5, variables[class_index][time_slot_offset + 1][room], ct);
}
}
}
}
}
}
// 6. Ensure that there are at least two sections of each course_conflicts_
// pair that are scheduled for different time slots.
for (const auto& conflict_pair : course_conflicts_) {
const int course_1 = conflict_pair.first;
const int course_2 = conflict_pair.second;
for (int time_slot = 0; time_slot < time_slot_count_; ++time_slot) {
const int bound = course_to_classes_[course_1].size() +
course_to_classes_[course_2].size() - 1;
MPConstraint* const ct = mip_solver.MakeRowConstraint(0, bound);
for (const int class_1 : course_to_classes_[course_1]) {
AddVariableIfNonNull(1, intermediate_vars[class_1][time_slot], ct);
}
for (const int class_2 : course_to_classes_[course_2]) {
AddVariableIfNonNull(1, intermediate_vars[class_2][time_slot], ct);
}
}
}
// 7. Ensure that conflicting class pairs are not scheduled for the same time
// slot.
for (const auto& conflict_pair : class_conflicts) {
for (int time_slot = 0; time_slot < time_slot_count_; ++time_slot) {
MPConstraint* const ct = mip_solver.MakeRowConstraint(0, 1);
AddVariableIfNonNull(1, intermediate_vars[conflict_pair.first][time_slot],
ct);
AddVariableIfNonNull(
1, intermediate_vars[conflict_pair.second][time_slot], ct);
}
}
MPSolver::ResultStatus status = mip_solver.Solve();
CourseSchedulingResult result;
result.set_solver_status(MipStatusToCourseSchedulingResultStatus(status));
if (status != MPSolver::OPTIMAL && status != MPSolver::FEASIBLE) {
if (status == MPSolver::UNBOUNDED) {
result.set_message(
"MIP solver returned UNBOUNDED: the model is solved but the solution "
"is infinity");
} else if (status == MPSolver::ABNORMAL) {
result.set_message(
"MIP solver returned ABNORMAL: some error occurred while solving");
}
return result;
}
for (int course = 0; course < model.courses_size(); ++course) {
for (int section = 0; section < course_to_classes_[course].size();
++section) {
ClassAssignment class_assignment;
class_assignment.set_course_index(course);
class_assignment.set_section_number(section);
const int class_index = course_to_classes_[course][section];
for (int time_slot = 0; time_slot < time_slot_count_; ++time_slot) {
for (int room = 0; room < room_count_; ++room) {
MPVariable* x_i = variables[class_index][time_slot][room];
if (x_i != nullptr && x_i->solution_value() == 1) {
if (solve_for_rooms_) {
class_assignment.add_room_indices(room);
}
class_assignment.add_time_slots(time_slot);
}
}
}
*result.add_class_assignments() = class_assignment;
}
}
return result;
}
CourseSchedulingSolver::ConflictPairs CourseSchedulingSolver::AssignStudents(
const CourseSchedulingModel& model, CourseSchedulingResult* result) {
LOG(INFO) << "Starting assign students solver.";
MPSolver mip_solver("AssignStudentsMIP",
MPSolver::SCIP_MIXED_INTEGER_PROGRAMMING);
// Create binary variables y(s,n) where y(s,n) = 1 indicates that student s is
// enrolled in class n. Variables are created for a student and each section
// of a course that they are signed up to take.
std::vector<std::vector<MPVariable*>> variables(
model.students_size(), std::vector<MPVariable*>(class_count_, nullptr));
for (int student_index = 0; student_index < model.students_size();
++student_index) {
const Student& student = model.students(student_index);
for (const int course_index : student.course_indices()) {
for (const int class_index : course_to_classes_[course_index]) {
variables[student_index][class_index] = mip_solver.MakeBoolVar(
absl::StrFormat("y_%d_%d", student_index, class_index));
}
}
}
// 1. Each student must be assigned to exactly one section for each course
// they are signed up to take.
for (int student_index = 0; student_index < model.students_size();
++student_index) {
const Student& student = model.students(student_index);
for (const int course_index : student.course_indices()) {
MPConstraint* const ct = mip_solver.MakeRowConstraint(1, 1);
for (const int class_index : course_to_classes_[course_index]) {
AddVariableIfNonNull(1, variables[student_index][class_index], ct);
}
}
}
// 2. Ensure that the minimum and maximum capacities for each class are met.
for (int course_index = 0; course_index < model.courses_size();
++course_index) {
const Course& course = model.courses(course_index);
const int min_cap = course.min_capacity();
const int max_cap = course.max_capacity();
for (const int class_index : course_to_classes_[course_index]) {
MPConstraint* const ct = mip_solver.MakeRowConstraint(min_cap, max_cap);
for (int student = 0; student < model.students_size(); ++student) {
AddVariableIfNonNull(1, variables[student][class_index], ct);
}
}
}
// 3. Each student should be assigned to one class per time slot. This is a
// soft constraint -- if violated, then the variable infeasibility_var(s,t)
// will be greater than 0 for that student s and time slot t.
std::vector<std::vector<MPVariable*>> infeasibility_vars(
model.students_size(),
std::vector<MPVariable*>(time_slot_count_, nullptr));
std::vector<absl::flat_hash_set<int>> time_slot_to_classes =
GetClassesByTimeSlot(result);
for (int time_slot = 0; time_slot < time_slot_count_; ++time_slot) {
for (int student_index = 0; student_index < model.students_size();
++student_index) {
infeasibility_vars[student_index][time_slot] = mip_solver.MakeIntVar(
0, class_count_,
absl::StrFormat("f_%d_%d", student_index, time_slot));
const Student& student = model.students(student_index);
MPConstraint* const ct = mip_solver.MakeRowConstraint(0, 1);
ct->SetCoefficient(infeasibility_vars[student_index][time_slot], -1);
for (const int course_index : student.course_indices()) {
for (const int class_index : course_to_classes_[course_index]) {
if (!time_slot_to_classes[time_slot].contains(class_index)) continue;
AddVariableIfNonNull(1, variables[student_index][class_index], ct);
}
}
}
}
// Minimize the infeasibility vars. If the objective is 0, then we have found
// a feasible solution for the course schedule. If the objective is greater
// than 0, then some students were assigned to multiple courses for the same
// time slot and we need to find a new schedule for the courses.
MPObjective* const objective = mip_solver.MutableObjective();
for (int student_index = 0; student_index < model.students_size();
++student_index) {
for (int time_slot = 0; time_slot < time_slot_count_; ++time_slot) {
objective->SetCoefficient(infeasibility_vars[student_index][time_slot],
1);
}
}
mip_solver.SetSolverSpecificParametersAsString("limits/gap=0.01");
MPSolver::ResultStatus status = mip_solver.Solve();
ConflictPairs class_conflict_pairs;
// This model will only be infeasible if the minimum or maximum capacities
// are violated.
if (status != MPSolver::OPTIMAL && status != MPSolver::FEASIBLE) {
result->set_solver_status(MipStatusToCourseSchedulingResultStatus(status));
result->clear_class_assignments();
if (status == MPSolver::INFEASIBLE) {
result->set_message(
"Check the minimum or maximum capacity constraints for your "
"classes.");
}
return class_conflict_pairs;
}
LOG(INFO) << "Finished assign students solver with " << objective->Value()
<< " schedule violations.";
if (objective->Value() > 0) {
for (int time_slot = 0; time_slot < time_slot_count_; ++time_slot) {
for (int student_index = 0; student_index < model.students_size();
++student_index) {
std::vector<int> conflicting_classes;
MPVariable* const f_i = infeasibility_vars[student_index][time_slot];
if (f_i != nullptr && f_i->solution_value() == 0) continue;
for (const int class_index : time_slot_to_classes[time_slot]) {
MPVariable* const y_i = variables[student_index][class_index];
if (y_i != nullptr && y_i->solution_value() == 1) {
conflicting_classes.push_back(class_index);
}
}
InsertSortedPairs(conflicting_classes, &class_conflict_pairs);
}
}
return class_conflict_pairs;
}
for (int student_index = 0; student_index < model.students_size();
++student_index) {
StudentAssignment student_assignment;
student_assignment.set_student_index(student_index);
const Student& student = model.students(student_index);
for (const int course_index : student.course_indices()) {
for (int section_index = 0;
section_index < course_to_classes_[course_index].size();
++section_index) {
int class_index = course_to_classes_[course_index][section_index];
MPVariable* const y_i = variables[student_index][class_index];
if (y_i->solution_value() == 1) {
student_assignment.add_course_indices(course_index);
student_assignment.add_section_indices(section_index);
}
}
}
*result->add_student_assignments() = student_assignment;
}
return class_conflict_pairs;
}
CourseSchedulingResultStatus
CourseSchedulingSolver::MipStatusToCourseSchedulingResultStatus(
MPSolver::ResultStatus mip_status) {
switch (mip_status) {
case MPSolver::ResultStatus::OPTIMAL:
return SOLVER_OPTIMAL;
case MPSolver::ResultStatus::FEASIBLE:
return SOLVER_FEASIBLE;
case MPSolver::ResultStatus::INFEASIBLE:
return SOLVER_INFEASIBLE;
case MPSolver::ResultStatus::UNBOUNDED:
case MPSolver::ResultStatus::MODEL_INVALID:
return SOLVER_MODEL_INVALID;
case MPSolver::ResultStatus::NOT_SOLVED:
return SOLVER_NOT_SOLVED;
case MPSolver::ResultStatus::ABNORMAL:
return ABNORMAL;
default:
return COURSE_SCHEDULING_RESULT_STATUS_UNSPECIFIED;
}
}
absl::Status CourseSchedulingSolver::VerifyCourseSchedulingResult(
const CourseSchedulingModel& model, const CourseSchedulingResult& result) {
std::vector<absl::flat_hash_set<int>> class_to_time_slots(class_count_);
std::vector<absl::flat_hash_set<int>> room_to_time_slots(model.rooms_size());
for (const ClassAssignment& class_assignment : result.class_assignments()) {
const int course_index = class_assignment.course_index();
const int meetings_count = model.courses(course_index).meetings_count();
const int consecutive_time_slots =
model.courses(course_index).consecutive_slots_count();
// Verify that each class meets the correct number of times.
if (class_assignment.time_slots_size() !=
meetings_count * consecutive_time_slots) {
return absl::InvalidArgumentError(absl::StrFormat(
"Verification failed: The course titled %s and section number %d "
"meets %d times when it should meet %d times.",
model.courses(course_index).display_name(),
class_assignment.section_number(), class_assignment.time_slots_size(),
meetings_count * consecutive_time_slots));
}
const int class_index =
course_to_classes_[course_index][class_assignment.section_number()];
std::vector<std::vector<int>> day_to_time_slots(model.days_count());
for (const int time_slot : class_assignment.time_slots()) {
class_to_time_slots[class_index].insert(time_slot);
day_to_time_slots[std::floor(time_slot / model.daily_time_slot_count())]
.push_back(time_slot);
}
// Verify that a class meets no more than its consecutive time slot count
// per day. If a class needs 2 consecutive time slots, verify that it is
// scheduled accordingly.
for (int day = 0; day < model.days_count(); ++day) {
const int day_count = day_to_time_slots[day].size();
if (day_count != 0 && day_count != consecutive_time_slots) {
return absl::InvalidArgumentError(
absl::StrFormat("Verification failed: The course titled %s does "
"not meet the correct number of times in "
"day %d.",
model.courses(course_index).display_name(), day));
}
if (day_count != 2) continue;
const int first_slot = day_to_time_slots[day][0];
const int second_slot = day_to_time_slots[day][1];
if (std::abs(first_slot - second_slot) != 1) {
return absl::InvalidArgumentError(
absl::StrFormat("Verification failed: The course titled %s is not "
"scheduled for consecutive time slots "
"in day %d.",
model.courses(course_index).display_name(), day));
}
}
// Verify that their is no more than 1 class per room for each time slot.
if (solve_for_rooms_) {
for (int i = 0; i < class_assignment.room_indices_size(); ++i) {
const int room = class_assignment.room_indices(i);
const int time_slot = class_assignment.time_slots(i);
if (room_to_time_slots[room].contains(time_slot)) {
return absl::InvalidArgumentError(
absl::StrFormat("Verification failed: Multiple classes have "
"been assigned to room %s during time slot %d.",
model.rooms(room).display_name(), time_slot));
}
room_to_time_slots[room].insert(time_slot);
}
}
}
// Verify that each teacher is assigned to no more than one class per time
// slot and that each teacher is not assigned to their restricted time slots.
for (int teacher = 0; teacher < model.teachers_size(); ++teacher) {
const auto& class_list = teacher_to_classes_[teacher];
absl::flat_hash_set<int> teacher_time_slots;
for (const int class_index : class_list) {
for (const int time_slot : class_to_time_slots[class_index]) {
if (teacher_to_restricted_slots_[teacher].contains(time_slot)) {
return absl::InvalidArgumentError(absl::StrFormat(
"Verification failed: Teacher with name %s has been assigned to "
"restricted time slot %d.",
model.teachers(teacher).display_name(), time_slot));
}
if (teacher_time_slots.contains(time_slot)) {
return absl::InvalidArgumentError(absl::StrFormat(
"Verification failed: Teacher with name %s has been assigned to "
"multiple classes at time slot %d.",
model.teachers(teacher).display_name(), time_slot));
}
teacher_time_slots.insert(time_slot);
}
}
}
std::vector<int> class_student_count(class_count_);
for (const StudentAssignment& student_assignment :
result.student_assignments()) {
const int student_index = student_assignment.student_index();
// Verify that each student is assigned to the correct courses.
std::vector<int> enrolled_courses =
std::vector<int>(model.students(student_index).course_indices().begin(),
model.students(student_index).course_indices().end());
std::vector<int> assigned_courses =
std::vector<int>(student_assignment.course_indices().begin(),
student_assignment.course_indices().end());
std::sort(enrolled_courses.begin(), enrolled_courses.end());
std::sort(assigned_courses.begin(), assigned_courses.end());
if (enrolled_courses != assigned_courses) {
return absl::InvalidArgumentError(
absl::StrFormat("Verification failed: Student with name %s has not "
"been assigned the correct courses.",
model.students(student_index).display_name()));
}
// Verify that each student is assigned to no more than one class per time
// slot.
absl::flat_hash_set<int> student_time_slots;
for (int i = 0; i < student_assignment.course_indices_size(); ++i) {
const int course_index = student_assignment.course_indices(i);
const int section = student_assignment.section_indices(i);
const int class_index = course_to_classes_[course_index][section];
++class_student_count[class_index];
for (const int time_slot : class_to_time_slots[class_index]) {
if (student_time_slots.contains(time_slot)) {
return absl::InvalidArgumentError(absl::StrFormat(
"Verification failed: Student with name %s has been assigned to "
"multiple classes at time slot %d.",
model.students(student_index).display_name(), time_slot));
}
student_time_slots.insert(time_slot);
}
}
}
// Verify size of each class is within the minimum and maximum capacities.
for (int course = 0; course < model.courses_size(); ++course) {
const int min_cap = model.courses(course).min_capacity();
const int max_cap = model.courses(course).max_capacity();
for (const int class_index : course_to_classes_[course]) {
const int class_size = class_student_count[class_index];
if (class_size < min_cap) {
return absl::InvalidArgumentError(absl::StrFormat(
"Verification failed: The course titled %s has %d students when it "
"should have at least %d students.",
model.courses(course).display_name(), class_size, min_cap));
}
if (class_size > max_cap) {
return absl::InvalidArgumentError(absl::StrFormat(
"Verification failed: The course titled %s has %d students when it "
"should have no more than %d students.",
model.courses(course).display_name(), class_size, max_cap));
}
}
}
return absl::OkStatus();
}
} // namespace operations_research