Google OR-Tools v9.15
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
course_scheduling.cc
Go to the documentation of this file.
1// Copyright 2010-2025 Google LLC
2// Licensed under the Apache License, Version 2.0 (the "License");
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5//
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
15
16#include <algorithm>
17#include <cmath>
18#include <cstdlib>
19#include <limits>
20#include <utility>
21#include <vector>
22
23#include "absl/container/flat_hash_set.h"
24#include "absl/log/log.h"
25#include "absl/status/status.h"
26#include "absl/strings/str_format.h"
27#include "absl/types/span.h"
31
32namespace operations_research {
33
35 const CourseSchedulingModel& model) {
36 time_slot_count_ = model.days_count() * model.daily_time_slot_count();
37 room_count_ = model.rooms_size();
38 solve_for_rooms_ = room_count_ > 0;
39 // If there are no rooms given, we have to give room_count at least one in
40 // order for the loops creating the solver variables and constraints to work.
41 if (!solve_for_rooms_) {
42 room_count_ = 1;
43 }
44
45 // Validate the information given for each course.
46 for (const Course& course : model.courses()) {
47 if (course.consecutive_slots_count() != 1 &&
48 course.consecutive_slots_count() != 2) {
49 return absl::InvalidArgumentError(absl::StrFormat(
50 "The course titled %s has %d consecutive time slots specified when "
51 "it can only have 1 or 2.",
52 course.display_name(), course.consecutive_slots_count()));
53 }
54
55 if (course.teacher_section_counts_size() != course.teacher_indices_size()) {
56 return absl::InvalidArgumentError(
57 absl::StrFormat("The course titled %s should have the same number of "
58 "teacher indices and section numbers.",
59 course.display_name()));
60 }
61
62 for (const int teacher_index : course.teacher_indices()) {
63 if (teacher_index >= model.teachers_size()) {
64 return absl::InvalidArgumentError(absl::StrFormat(
65 "The course titled %s has teacher %d assigned to it but there are "
66 "only %d teachers.",
67 course.display_name(), teacher_index, model.teachers_size()));
68 }
69 }
70
71 for (const int room_index : course.room_indices()) {
72 if (room_index >= model.rooms_size()) {
73 return absl::InvalidArgumentError(
74 absl::StrFormat("The course titled %s is slotted for room index %d "
75 "but there are only %d rooms.",
76 course.display_name(), room_index, room_count_));
77 }
78 }
79 }
80
81 // Validate the information given for each teacher and create hash sets of the
82 // restricted indices for each teacher.
83 teacher_to_restricted_slots_ =
84 std::vector<absl::flat_hash_set<int>>(model.teachers_size());
85 for (int teacher_index = 0; teacher_index < model.teachers_size();
86 ++teacher_index) {
87 for (const int restricted_slot :
88 model.teachers(teacher_index).restricted_time_slots()) {
89 if (restricted_slot >= time_slot_count_) {
90 return absl::InvalidArgumentError(
91 absl::StrFormat("Teacher with name %s has restricted time slot %d "
92 "but there are only %d time slots.",
93 model.teachers(teacher_index).display_name(),
94 restricted_slot, time_slot_count_));
95 }
96 teacher_to_restricted_slots_[teacher_index].insert(restricted_slot);
97 }
98 }
99
100 // Since each course can have multiple sections (classes), we need to
101 // "flatten" out each course so that each of its sections gets a unique index.
102 // This vector stores the information for calculating each unique index. The
103 // first value is the unique index the course's sections begin at. The second
104 // value is the total number of sections of that course.
105 course_to_classes_ = std::vector<std::vector<int>>(model.courses_size());
106 // For each teacher, store the class unique indices that they teach.
107 teacher_to_classes_ =
108 std::vector<absl::flat_hash_set<int>>(model.teachers_size());
109 // Store course indices of courses that have a single section.
110 absl::flat_hash_set<int> singleton_courses;
111 int flattened_course_index = 0;
112 for (int course_index = 0; course_index < model.courses_size();
113 ++course_index) {
114 const Course& course = model.courses(course_index);
115 int total_section_count = 0;
116 for (int teacher = 0; teacher < course.teacher_indices_size(); ++teacher) {
117 for (int section = 0; section < course.teacher_section_counts(teacher);
118 ++section) {
119 teacher_to_classes_[course.teacher_indices(teacher)].insert(
120 flattened_course_index);
121 course_to_classes_[course_index].push_back(flattened_course_index);
122 ++flattened_course_index;
123 }
124 total_section_count += course.teacher_section_counts(teacher);
125 }
126 if (total_section_count == 1) {
127 singleton_courses.insert(course_index);
128 }
129 }
130 class_count_ = flattened_course_index;
131
132 // Validate the information given for each student. Store each student's
133 // course pairs.
134 for (const Student& student : model.students()) {
135 for (const int course_index : student.course_indices()) {
136 if (course_index >= model.courses_size()) {
137 return absl::InvalidArgumentError(absl::StrFormat(
138 "Student with name %s has course index %d but there are only %d "
139 "courses.",
140 student.display_name(), course_index, model.courses_size()));
141 }
142 }
143 InsertSortedPairs(std::vector<int>(student.course_indices().begin(),
144 student.course_indices().end()),
145 &course_conflicts_);
146 }
147
148 LOG(INFO) << "Number of days: " << model.days_count();
149 LOG(INFO) << "Number of daily time slots: " << model.daily_time_slot_count();
150 LOG(INFO) << "Total number of time slots: " << time_slot_count_;
151 LOG(INFO) << "Number of courses: " << model.courses_size();
152 LOG(INFO) << "Total number of classes: " << class_count_;
153 LOG(INFO) << "Number of teachers: " << model.teachers_size();
154 LOG(INFO) << "Number of students: " << model.students_size();
155 if (solve_for_rooms_) {
156 LOG(INFO) << "Number of rooms: " << model.rooms_size();
157 }
158
159 return absl::OkStatus();
160}
161
163 const CourseSchedulingModel& model) {
165
166 const auto validation_status = ValidateModelAndLoadClasses(model);
167 if (!validation_status.ok()) {
168 result.set_solver_status(
170 result.set_message(validation_status.message());
171 return result;
172 }
173
174 ConflictPairs class_conflicts;
175 result = SolveModel(model, class_conflicts);
176
177 if (result.solver_status() != SOLVER_FEASIBLE &&
178 result.solver_status() != SOLVER_OPTIMAL) {
179 return result;
180 }
181
182 const auto verifier_status = VerifyCourseSchedulingResult(model, result);
183 if (!verifier_status.ok()) {
185 result.set_message(verifier_status.message());
186 }
187
188 return result;
189}
190
192 const CourseSchedulingModel& model, const ConflictPairs& class_conflicts) {
194
195 result = ScheduleCourses(class_conflicts, model);
196 if (result.solver_status() != SOLVER_FEASIBLE &&
197 result.solver_status() != SOLVER_OPTIMAL) {
198 if (result.solver_status() == SOLVER_INFEASIBLE) {
199 result.set_message("The problem is infeasible with the given courses.");
200 }
201 return result;
202 }
203
204 ConflictPairs class_conflicts_to_try = AssignStudents(model, &result);
205
206 if (class_conflicts_to_try.empty()) return result;
207
208 std::vector<std::pair<int, int>> conflicts(class_conflicts_to_try.begin(),
209 class_conflicts_to_try.end());
210
211 const int conflicts_count = conflicts.size();
212 const int conflicts_log = conflicts_count == 1 ? 1 : log2(conflicts_count);
213 for (int i = 0; i < conflicts_log; ++i) {
214 const int divisions = MathUtil::IPow<double>(2, i);
215 for (int j = 0; j < divisions; ++j) {
216 const int start = std::floor(conflicts_count * j / divisions);
217 const int end = std::floor(conflicts_count * (j + 1) / divisions);
218
219 ConflictPairs new_class_conflicts = class_conflicts;
220 if (end >= conflicts_count) {
221 new_class_conflicts.insert(conflicts.begin() + start, conflicts.end());
222 } else {
223 new_class_conflicts.insert(conflicts.begin() + start,
224 conflicts.begin() + end);
225 }
226
227 result = SolveModel(model, new_class_conflicts);
228 if (result.solver_status() == SOLVER_FEASIBLE ||
229 result.solver_status() == SOLVER_OPTIMAL) {
230 return result;
231 }
232 }
233 }
234
235 return result;
236}
237
238std::vector<int> CourseSchedulingSolver::GetRoomIndices(const Course& course) {
239 if (solve_for_rooms_) {
240 return std::vector<int>(course.room_indices().begin(),
241 course.room_indices().end());
242 }
243
244 return {0};
245}
246
247void CourseSchedulingSolver::InsertSortedPairs(absl::Span<const int> list,
248 ConflictPairs* pairs) {
249 for (int first = 1; first < list.size(); ++first) {
250 for (int second = first; second < list.size(); ++second) {
251 pairs->insert(std::minmax(list[first - 1], list[second]));
252 }
253 }
254}
255
256std::vector<absl::flat_hash_set<int>>
257CourseSchedulingSolver::GetClassesByTimeSlot(
258 const CourseSchedulingResult* result) {
259 std::vector<absl::flat_hash_set<int>> time_slot_to_classes(time_slot_count_);
260
261 for (const ClassAssignment& class_assignment : result->class_assignments()) {
262 const int course_index = class_assignment.course_index();
263 const int section_number = class_assignment.section_number();
264 for (const int time_slot : class_assignment.time_slots()) {
265 time_slot_to_classes[time_slot].insert(
266 course_to_classes_[course_index][section_number]);
267 }
268 }
269
270 return time_slot_to_classes;
271}
272
273void CourseSchedulingSolver::AddVariableIfNonNull(double coeff,
274 const MPVariable* var,
275 MPConstraint* ct) {
276 if (var == nullptr) return;
277
278 ct->SetCoefficient(var, coeff);
279}
280
281CourseSchedulingResult CourseSchedulingSolver::ScheduleCourses(
282 const ConflictPairs& class_conflicts, const CourseSchedulingModel& model) {
283 LOG(INFO) << "Starting schedule courses solver with "
284 << class_conflicts.size() << " class conflicts.";
285 MPSolver mip_solver("CourseSchedulingMIP",
287 const double kInfinity = std::numeric_limits<double>::infinity();
288
289 // Create binary variables x(n,t,r) where x(n,t,r) = 1 indicates that course n
290 // is scheduled for time slot t in course r. Variables are only created if
291 // attendees of class n are available for time slot t and if the course can be
292 // placed into room r.
293 std::vector<std::vector<std::vector<MPVariable*>>> variables(
294 class_count_,
295 std::vector<std::vector<MPVariable*>>(
296 time_slot_count_, std::vector<MPVariable*>(room_count_, nullptr)));
297 for (int proto_index = 0; proto_index < model.courses_size(); ++proto_index) {
298 const Course& course = model.courses(proto_index);
299 const int course_index = course_to_classes_[proto_index][0];
300 int total_section_index = 0;
301 for (int i = 0; i < course.teacher_section_counts_size(); ++i) {
302 const int teacher = course.teacher_indices(i);
303 const int section_count = course.teacher_section_counts(i);
304 for (int section = 0; section < section_count; ++section) {
305 for (int time_slot = 0; time_slot < time_slot_count_; ++time_slot) {
306 if (teacher_to_restricted_slots_[teacher].contains(time_slot)) {
307 continue;
308 }
309
310 for (const int room : GetRoomIndices(course)) {
311 variables[course_index + total_section_index][time_slot][room] =
312 mip_solver.MakeBoolVar(absl::StrFormat(
313 "x_%d_%d_%d", course_index + total_section_index, time_slot,
314 room));
315 }
316 }
317 ++total_section_index;
318 }
319 }
320 }
321
322 std::vector<std::vector<MPVariable*>> intermediate_vars(
323 class_count_, std::vector<MPVariable*>(time_slot_count_));
324 for (int class_index = 0; class_index < class_count_; ++class_index) {
325 for (int time_slot = 0; time_slot < time_slot_count_; ++time_slot) {
326 MPConstraint* const ct = mip_solver.MakeRowConstraint(0, 0);
327 for (int room = 0; room < room_count_; ++room) {
328 if (variables[class_index][time_slot][room] == nullptr) continue;
329
330 ct->SetCoefficient(variables[class_index][time_slot][room], 1);
331 }
332 if (!ct->terms().empty()) {
333 intermediate_vars[class_index][time_slot] = mip_solver.MakeBoolVar(
334 absl::StrFormat("intermediate_%d_%d", class_index, time_slot));
335 ct->SetCoefficient(intermediate_vars[class_index][time_slot], -1);
336 }
337 }
338 }
339
340 // 1. Each course meets no more than its number of consecutive time slots a
341 // day.
342 for (int day = 0; day < model.days_count(); ++day) {
343 for (int course = 0; course < model.courses_size(); ++course) {
344 const int consecutive_slot_count =
345 model.courses(course).consecutive_slots_count();
346 for (const int class_index : course_to_classes_[course]) {
347 MPConstraint* const ct =
348 mip_solver.MakeRowConstraint(0, consecutive_slot_count);
349 for (int daily_time_slot = 0;
350 daily_time_slot < model.daily_time_slot_count();
351 ++daily_time_slot) {
352 AddVariableIfNonNull(
353 1,
354 intermediate_vars[class_index]
355 [(day * model.daily_time_slot_count()) +
356 daily_time_slot],
357 ct);
358 }
359 }
360 }
361 }
362
363 // 2. Each course must meet the given number of times * its number of
364 // consecutive time slots.
365 for (int course = 0; course < model.courses_size(); ++course) {
366 const int meeting_count = model.courses(course).meetings_count();
367 const int consecutive_slot_count =
368 model.courses(course).consecutive_slots_count();
369 for (const int class_index : course_to_classes_[course]) {
370 MPConstraint* const ct =
371 mip_solver.MakeRowConstraint(meeting_count * consecutive_slot_count,
372 meeting_count * consecutive_slot_count);
373 for (int time_slot = 0; time_slot < time_slot_count_; ++time_slot) {
374 AddVariableIfNonNull(1, intermediate_vars[class_index][time_slot], ct);
375 }
376 }
377 }
378
379 // 3. Teachers are scheduled for no more than one course per time slot.
380 for (int time_slot = 0; time_slot < time_slot_count_; ++time_slot) {
381 for (int teacher = 0; teacher < model.teachers_size(); ++teacher) {
382 MPConstraint* const ct = mip_solver.MakeRowConstraint(0, 1);
383 for (const int class_index : teacher_to_classes_[teacher]) {
384 AddVariableIfNonNull(1, intermediate_vars[class_index][time_slot], ct);
385 }
386 }
387 }
388
389 // 4. Each room can only be occupied by one course for each time slot.
390 if (solve_for_rooms_) {
391 for (int time_slot = 0; time_slot < time_slot_count_; ++time_slot) {
392 for (int room = 0; room < room_count_; ++room) {
393 MPConstraint* const ct = mip_solver.MakeRowConstraint(0, 1);
394 for (int class_index = 0; class_index < class_count_; ++class_index) {
395 AddVariableIfNonNull(1, variables[class_index][time_slot][room], ct);
396 }
397 }
398 }
399 }
400
401 // 5. Ensure each class is scheduled for the correct number of consecutive
402 // time slots.
403 for (int course = 0; course < model.courses_size(); ++course) {
404 const int consecutive_slot_count =
405 model.courses(course).consecutive_slots_count();
406 if (consecutive_slot_count == 1) continue;
407 for (const int class_index : course_to_classes_[course]) {
408 for (int day = 0; day < model.days_count(); ++day) {
409 for (int room = 0; room < room_count_; ++room) {
410 // If only the previous time slot is chosen, force the current time
411 // slot to be chosen.
412 for (int daily_time_slot = 0;
413 daily_time_slot < model.daily_time_slot_count();
414 ++daily_time_slot) {
415 MPConstraint* const ct = mip_solver.MakeRowConstraint(0, kInfinity);
416 const int time_slot_offset =
417 day * model.daily_time_slot_count() + daily_time_slot;
418
419 if (daily_time_slot > 0) {
420 AddVariableIfNonNull(
421 1, variables[class_index][time_slot_offset - 1][room], ct);
422 }
423 AddVariableIfNonNull(
424 -0.5, variables[class_index][time_slot_offset][room], ct);
425 if (daily_time_slot < model.daily_time_slot_count() - 1) {
426 AddVariableIfNonNull(
427 0.5, variables[class_index][time_slot_offset + 1][room], ct);
428 }
429 }
430 }
431 }
432 }
433 }
434
435 // 6. Ensure that there are at least two sections of each course_conflicts_
436 // pair that are scheduled for different time slots.
437 for (const auto& conflict_pair : course_conflicts_) {
438 const int course_1 = conflict_pair.first;
439 const int course_2 = conflict_pair.second;
440 for (int time_slot = 0; time_slot < time_slot_count_; ++time_slot) {
441 const int bound = course_to_classes_[course_1].size() +
442 course_to_classes_[course_2].size() - 1;
443 MPConstraint* const ct = mip_solver.MakeRowConstraint(0, bound);
444 for (const int class_1 : course_to_classes_[course_1]) {
445 AddVariableIfNonNull(1, intermediate_vars[class_1][time_slot], ct);
446 }
447 for (const int class_2 : course_to_classes_[course_2]) {
448 AddVariableIfNonNull(1, intermediate_vars[class_2][time_slot], ct);
449 }
450 }
451 }
452
453 // 7. Ensure that conflicting class pairs are not scheduled for the same time
454 // slot.
455 for (const auto& conflict_pair : class_conflicts) {
456 for (int time_slot = 0; time_slot < time_slot_count_; ++time_slot) {
457 MPConstraint* const ct = mip_solver.MakeRowConstraint(0, 1);
458 AddVariableIfNonNull(1, intermediate_vars[conflict_pair.first][time_slot],
459 ct);
460 AddVariableIfNonNull(
461 1, intermediate_vars[conflict_pair.second][time_slot], ct);
462 }
463 }
464
465 MPSolver::ResultStatus status = mip_solver.Solve();
466
467 CourseSchedulingResult result;
468 result.set_solver_status(MipStatusToCourseSchedulingResultStatus(status));
469 if (status != MPSolver::OPTIMAL && status != MPSolver::FEASIBLE) {
470 if (status == MPSolver::UNBOUNDED) {
471 result.set_message(
472 "MIP solver returned UNBOUNDED: the model is solved but the solution "
473 "is infinity");
474 } else if (status == MPSolver::ABNORMAL) {
475 result.set_message(
476 "MIP solver returned ABNORMAL: some error occurred while solving");
477 }
478 return result;
479 }
480
481 for (int course = 0; course < model.courses_size(); ++course) {
482 for (int section = 0; section < course_to_classes_[course].size();
483 ++section) {
484 ClassAssignment class_assignment;
485 class_assignment.set_course_index(course);
486 class_assignment.set_section_number(section);
487 const int class_index = course_to_classes_[course][section];
488
489 for (int time_slot = 0; time_slot < time_slot_count_; ++time_slot) {
490 for (int room = 0; room < room_count_; ++room) {
491 MPVariable* x_i = variables[class_index][time_slot][room];
492 if (x_i != nullptr && x_i->solution_value() == 1) {
493 if (solve_for_rooms_) {
494 class_assignment.add_room_indices(room);
495 }
496 class_assignment.add_time_slots(time_slot);
497 }
498 }
499 }
500 *result.add_class_assignments() = class_assignment;
501 }
502 }
503 return result;
504}
505
506CourseSchedulingSolver::ConflictPairs CourseSchedulingSolver::AssignStudents(
507 const CourseSchedulingModel& model, CourseSchedulingResult* result) {
508 LOG(INFO) << "Starting assign students solver.";
509 MPSolver mip_solver("AssignStudentsMIP",
511
512 // Create binary variables y(s,n) where y(s,n) = 1 indicates that student s is
513 // enrolled in class n. Variables are created for a student and each section
514 // of a course that they are signed up to take.
515 std::vector<std::vector<MPVariable*>> variables(
516 model.students_size(), std::vector<MPVariable*>(class_count_, nullptr));
517 for (int student_index = 0; student_index < model.students_size();
518 ++student_index) {
519 const Student& student = model.students(student_index);
520 for (const int course_index : student.course_indices()) {
521 for (const int class_index : course_to_classes_[course_index]) {
522 variables[student_index][class_index] = mip_solver.MakeBoolVar(
523 absl::StrFormat("y_%d_%d", student_index, class_index));
524 }
525 }
526 }
527
528 // 1. Each student must be assigned to exactly one section for each course
529 // they are signed up to take.
530 for (int student_index = 0; student_index < model.students_size();
531 ++student_index) {
532 const Student& student = model.students(student_index);
533 for (const int course_index : student.course_indices()) {
534 MPConstraint* const ct = mip_solver.MakeRowConstraint(1, 1);
535 for (const int class_index : course_to_classes_[course_index]) {
536 AddVariableIfNonNull(1, variables[student_index][class_index], ct);
537 }
538 }
539 }
540
541 // 2. Ensure that the minimum and maximum capacities for each class are met.
542 for (int course_index = 0; course_index < model.courses_size();
543 ++course_index) {
544 const Course& course = model.courses(course_index);
545 const int min_cap = course.min_capacity();
546 const int max_cap = course.max_capacity();
547 for (const int class_index : course_to_classes_[course_index]) {
548 MPConstraint* const ct = mip_solver.MakeRowConstraint(min_cap, max_cap);
549 for (int student = 0; student < model.students_size(); ++student) {
550 AddVariableIfNonNull(1, variables[student][class_index], ct);
551 }
552 }
553 }
554
555 // 3. Each student should be assigned to one class per time slot. This is a
556 // soft constraint -- if violated, then the variable infeasibility_var(s,t)
557 // will be greater than 0 for that student s and time slot t.
558 std::vector<std::vector<MPVariable*>> infeasibility_vars(
559 model.students_size(),
560 std::vector<MPVariable*>(time_slot_count_, nullptr));
561 std::vector<absl::flat_hash_set<int>> time_slot_to_classes =
562 GetClassesByTimeSlot(result);
563 for (int time_slot = 0; time_slot < time_slot_count_; ++time_slot) {
564 for (int student_index = 0; student_index < model.students_size();
565 ++student_index) {
566 infeasibility_vars[student_index][time_slot] = mip_solver.MakeIntVar(
567 0, class_count_,
568 absl::StrFormat("f_%d_%d", student_index, time_slot));
569 const Student& student = model.students(student_index);
570
571 MPConstraint* const ct = mip_solver.MakeRowConstraint(0, 1);
572 ct->SetCoefficient(infeasibility_vars[student_index][time_slot], -1);
573 for (const int course_index : student.course_indices()) {
574 for (const int class_index : course_to_classes_[course_index]) {
575 if (!time_slot_to_classes[time_slot].contains(class_index)) continue;
576
577 AddVariableIfNonNull(1, variables[student_index][class_index], ct);
578 }
579 }
580 }
581 }
582
583 // Minimize the infeasibility vars. If the objective is 0, then we have found
584 // a feasible solution for the course schedule. If the objective is greater
585 // than 0, then some students were assigned to multiple courses for the same
586 // time slot and we need to find a new schedule for the courses.
587 MPObjective* const objective = mip_solver.MutableObjective();
588 for (int student_index = 0; student_index < model.students_size();
589 ++student_index) {
590 for (int time_slot = 0; time_slot < time_slot_count_; ++time_slot) {
591 objective->SetCoefficient(infeasibility_vars[student_index][time_slot],
592 1);
593 }
594 }
595
596 mip_solver.SetSolverSpecificParametersAsString("limits/gap=0.01");
597 MPSolver::ResultStatus status = mip_solver.Solve();
598 ConflictPairs class_conflict_pairs;
599
600 // This model will only be infeasible if the minimum or maximum capacities
601 // are violated.
602 if (status != MPSolver::OPTIMAL && status != MPSolver::FEASIBLE) {
603 result->set_solver_status(MipStatusToCourseSchedulingResultStatus(status));
604 result->clear_class_assignments();
605 if (status == MPSolver::INFEASIBLE) {
606 result->set_message(
607 "Check the minimum or maximum capacity constraints for your "
608 "classes.");
609 }
610
611 return class_conflict_pairs;
612 }
613
614 LOG(INFO) << "Finished assign students solver with " << objective->Value()
615 << " schedule violations.";
616 if (objective->Value() > 0) {
617 for (int time_slot = 0; time_slot < time_slot_count_; ++time_slot) {
618 for (int student_index = 0; student_index < model.students_size();
619 ++student_index) {
620 std::vector<int> conflicting_classes;
621 MPVariable* const f_i = infeasibility_vars[student_index][time_slot];
622 if (f_i != nullptr && f_i->solution_value() == 0) continue;
623
624 for (const int class_index : time_slot_to_classes[time_slot]) {
625 MPVariable* const y_i = variables[student_index][class_index];
626 if (y_i != nullptr && y_i->solution_value() == 1) {
627 conflicting_classes.push_back(class_index);
628 }
629 }
630 InsertSortedPairs(conflicting_classes, &class_conflict_pairs);
631 }
632 }
633 return class_conflict_pairs;
634 }
635
636 for (int student_index = 0; student_index < model.students_size();
637 ++student_index) {
638 StudentAssignment student_assignment;
639 student_assignment.set_student_index(student_index);
640
641 const Student& student = model.students(student_index);
642 for (const int course_index : student.course_indices()) {
643 for (int section_index = 0;
644 section_index < course_to_classes_[course_index].size();
645 ++section_index) {
646 int class_index = course_to_classes_[course_index][section_index];
647 MPVariable* const y_i = variables[student_index][class_index];
648
649 if (y_i->solution_value() == 1) {
650 student_assignment.add_course_indices(course_index);
651 student_assignment.add_section_indices(section_index);
652 }
653 }
654 }
655 *result->add_student_assignments() = student_assignment;
656 }
657
658 return class_conflict_pairs;
659}
660
662CourseSchedulingSolver::MipStatusToCourseSchedulingResultStatus(
663 MPSolver::ResultStatus mip_status) {
664 switch (mip_status) {
666 return SOLVER_OPTIMAL;
668 return SOLVER_FEASIBLE;
670 return SOLVER_INFEASIBLE;
675 return SOLVER_NOT_SOLVED;
677 return ABNORMAL;
678 default:
680 }
681}
682
684 const CourseSchedulingModel& model, const CourseSchedulingResult& result) {
685 std::vector<absl::flat_hash_set<int>> class_to_time_slots(class_count_);
686 std::vector<absl::flat_hash_set<int>> room_to_time_slots(model.rooms_size());
687 for (const ClassAssignment& class_assignment : result.class_assignments()) {
688 const int course_index = class_assignment.course_index();
689 const int meetings_count = model.courses(course_index).meetings_count();
690 const int consecutive_time_slots =
691 model.courses(course_index).consecutive_slots_count();
692
693 // Verify that each class meets the correct number of times.
694 if (class_assignment.time_slots_size() !=
695 meetings_count * consecutive_time_slots) {
696 return absl::InvalidArgumentError(absl::StrFormat(
697 "Verification failed: The course titled %s and section number %d "
698 "meets %d times when it should meet %d times.",
699 model.courses(course_index).display_name(),
700 class_assignment.section_number(), class_assignment.time_slots_size(),
701 meetings_count * consecutive_time_slots));
702 }
703
704 const int class_index =
705 course_to_classes_[course_index][class_assignment.section_number()];
706 std::vector<std::vector<int>> day_to_time_slots(model.days_count());
707 for (const int time_slot : class_assignment.time_slots()) {
708 class_to_time_slots[class_index].insert(time_slot);
709 day_to_time_slots[std::floor(time_slot / model.daily_time_slot_count())]
710 .push_back(time_slot);
711 }
712
713 // Verify that a class meets no more than its consecutive time slot count
714 // per day. If a class needs 2 consecutive time slots, verify that it is
715 // scheduled accordingly.
716 for (int day = 0; day < model.days_count(); ++day) {
717 const int day_count = day_to_time_slots[day].size();
718 if (day_count != 0 && day_count != consecutive_time_slots) {
719 return absl::InvalidArgumentError(
720 absl::StrFormat("Verification failed: The course titled %s does "
721 "not meet the correct number of times in "
722 "day %d.",
723 model.courses(course_index).display_name(), day));
724 }
725 if (day_count != 2) continue;
726
727 const int first_slot = day_to_time_slots[day][0];
728 const int second_slot = day_to_time_slots[day][1];
729 if (std::abs(first_slot - second_slot) != 1) {
730 return absl::InvalidArgumentError(
731 absl::StrFormat("Verification failed: The course titled %s is not "
732 "scheduled for consecutive time slots "
733 "in day %d.",
734 model.courses(course_index).display_name(), day));
735 }
736 }
737
738 // Verify that their is no more than 1 class per room for each time slot.
739 if (solve_for_rooms_) {
740 for (int i = 0; i < class_assignment.room_indices_size(); ++i) {
741 const int room = class_assignment.room_indices(i);
742 const int time_slot = class_assignment.time_slots(i);
743 if (room_to_time_slots[room].contains(time_slot)) {
744 return absl::InvalidArgumentError(
745 absl::StrFormat("Verification failed: Multiple classes have "
746 "been assigned to room %s during time slot %d.",
747 model.rooms(room).display_name(), time_slot));
748 }
749 room_to_time_slots[room].insert(time_slot);
750 }
751 }
752 }
753
754 // Verify that each teacher is assigned to no more than one class per time
755 // slot and that each teacher is not assigned to their restricted time slots.
756 for (int teacher = 0; teacher < model.teachers_size(); ++teacher) {
757 const auto& class_list = teacher_to_classes_[teacher];
758 absl::flat_hash_set<int> teacher_time_slots;
759 for (const int class_index : class_list) {
760 for (const int time_slot : class_to_time_slots[class_index]) {
761 if (teacher_to_restricted_slots_[teacher].contains(time_slot)) {
762 return absl::InvalidArgumentError(absl::StrFormat(
763 "Verification failed: Teacher with name %s has been assigned to "
764 "restricted time slot %d.",
765 model.teachers(teacher).display_name(), time_slot));
766 }
767 if (teacher_time_slots.contains(time_slot)) {
768 return absl::InvalidArgumentError(absl::StrFormat(
769 "Verification failed: Teacher with name %s has been assigned to "
770 "multiple classes at time slot %d.",
771 model.teachers(teacher).display_name(), time_slot));
772 }
773 teacher_time_slots.insert(time_slot);
774 }
775 }
776 }
777
778 std::vector<int> class_student_count(class_count_);
779 for (const StudentAssignment& student_assignment :
780 result.student_assignments()) {
781 const int student_index = student_assignment.student_index();
782
783 // Verify that each student is assigned to the correct courses.
784 std::vector<int> enrolled_courses =
785 std::vector<int>(model.students(student_index).course_indices().begin(),
786 model.students(student_index).course_indices().end());
787 std::vector<int> assigned_courses =
788 std::vector<int>(student_assignment.course_indices().begin(),
789 student_assignment.course_indices().end());
790 std::sort(enrolled_courses.begin(), enrolled_courses.end());
791 std::sort(assigned_courses.begin(), assigned_courses.end());
792 if (enrolled_courses != assigned_courses) {
793 return absl::InvalidArgumentError(
794 absl::StrFormat("Verification failed: Student with name %s has not "
795 "been assigned the correct courses.",
796 model.students(student_index).display_name()));
797 }
798
799 // Verify that each student is assigned to no more than one class per time
800 // slot.
801 absl::flat_hash_set<int> student_time_slots;
802 for (int i = 0; i < student_assignment.course_indices_size(); ++i) {
803 const int course_index = student_assignment.course_indices(i);
804 const int section = student_assignment.section_indices(i);
805 const int class_index = course_to_classes_[course_index][section];
806 ++class_student_count[class_index];
807
808 for (const int time_slot : class_to_time_slots[class_index]) {
809 if (student_time_slots.contains(time_slot)) {
810 return absl::InvalidArgumentError(absl::StrFormat(
811 "Verification failed: Student with name %s has been assigned to "
812 "multiple classes at time slot %d.",
813 model.students(student_index).display_name(), time_slot));
814 }
815 student_time_slots.insert(time_slot);
816 }
817 }
818 }
819
820 // Verify size of each class is within the minimum and maximum capacities.
821 for (int course = 0; course < model.courses_size(); ++course) {
822 const int min_cap = model.courses(course).min_capacity();
823 const int max_cap = model.courses(course).max_capacity();
824 for (const int class_index : course_to_classes_[course]) {
825 const int class_size = class_student_count[class_index];
826 if (class_size < min_cap) {
827 return absl::InvalidArgumentError(absl::StrFormat(
828 "Verification failed: The course titled %s has %d students when it "
829 "should have at least %d students.",
830 model.courses(course).display_name(), class_size, min_cap));
831 }
832 if (class_size > max_cap) {
833 return absl::InvalidArgumentError(absl::StrFormat(
834 "Verification failed: The course titled %s has %d students when it "
835 "should have no more than %d students.",
836 model.courses(course).display_name(), class_size, max_cap));
837 }
838 }
839 }
840
841 return absl::OkStatus();
842}
843
844} // namespace operations_research
const ::operations_research::Teacher & teachers(int index) const
const ::operations_research::Course & courses(int index) const
const ::operations_research::Student & students(int index) const
const ::operations_research::Room & rooms(int index) const
const ::operations_research::ClassAssignment & class_assignments(int index) const
void set_message(Arg_ &&arg, Args_... args)
void set_solver_status(::operations_research::CourseSchedulingResultStatus value)
const ::operations_research::StudentAssignment & student_assignments(int index) const
::operations_research::CourseSchedulingResultStatus solver_status() const
absl::flat_hash_set< std::pair< int, int > > ConflictPairs
virtual CourseSchedulingResult SolveModel(const CourseSchedulingModel &model, const ConflictPairs &class_conflicts)
virtual absl::Status ValidateModelAndLoadClasses(const CourseSchedulingModel &model)
virtual absl::Status VerifyCourseSchedulingResult(const CourseSchedulingModel &model, const CourseSchedulingResult &result)
CourseSchedulingResult Solve(const CourseSchedulingModel &model)
::int32_t room_indices(int index) const
::int32_t teacher_indices(int index) const
::int32_t teacher_section_counts(int index) const
const ::std::string & display_name() const
@ MODEL_INVALID
the model is trivially invalid (NaN coefficients, etc).
@ FEASIBLE
feasible, or stopped by limit.
@ NOT_SOLVED
not been solved yet.
@ INFEASIBLE
proven infeasible.
@ ABNORMAL
abnormal, i.e., error of some kind.
The class for variables of a Mathematical Programming (MP) model.
static T IPow(T base, int exp)
Definition mathutil.h:119
const ::std::string & display_name() const
::int32_t course_indices(int index) const
const ::std::string & display_name() const
::int32_t restricted_time_slots(int index) const
const ::std::string & display_name() const
OR-Tools root namespace.
static constexpr double kInfinity
ClosedInterval::Iterator end(ClosedInterval interval)