// ############################################################################
// Hard constraints
// ############################################################################
// Lectures: A missing or extra lecture of a course.
// This hard constraint is built-in: the correct number of Lecture's is created
// Conflicts: Two conflicting lectures in the same period.
// Three conflicting lectures count as 3 violations: one for each pair.
rule "conflictingLecturesDifferentCourseInSamePeriod"
when
$courseConflict : CourseConflict($leftCourse : leftCourse, $rightCourse : rightCourse)
$leftLecture : Lecture(course == $leftCourse, $period : period, period != null)
$rightLecture : Lecture(course == $rightCourse, period == $period, this != $leftLecture)
then
scoreHolder.addHardConstraintMatch(kcontext, - $courseConflict.getConflictCount());
end
rule "conflictingLecturesSameCourseInSamePeriod"
when
Lecture($leftId : id, $leftCourse : course, $period : period, period != null)
Lecture(course == $leftCourse, period == $period, id > $leftId)
then
scoreHolder.addHardConstraintMatch(kcontext, - (1 + $leftCourse.getCurriculumList().size()));
end
// RoomOccupancy: Two lectures in the same room at the same period.
// Any extra lecture in the same period and room counts as one more violation.
rule "roomOccupancy"
when
Lecture($leftId : id, period != null, $period : period, room != null, $room : room)
// $leftLecture has lowest id of the period+room combo
not Lecture(period == $period, room == $room, id < $leftId)
// rightLecture has the same period
Lecture(period == $period, room == $room, id > $leftId, $rightId : id)
then
scoreHolder.addHardConstraintMatch(kcontext, -1);
end
// Availabilities: Each lecture in a period unavailable for that course.
rule "unavailablePeriodPenalty"
when
UnavailablePeriodPenalty($course : course, $period : period)
Lecture(course == $course, period == $period)
then
scoreHolder.addHardConstraintMatch(kcontext, -10);
end
// ############################################################################
// Soft constraints
// ############################################################################
// RoomCapacity: For each lecture, the number of students that attend the course should be less or equal
// than the number of seats of all the rooms that host its lectures.
// Each student above the capacity counts as 1 point of penalty.
rule "roomCapacity"
when
$room : Room($capacity : capacity)
Lecture(room == $room, studentSize > $capacity, $studentSize : studentSize)
then
scoreHolder.addSoftConstraintMatch(kcontext, ($capacity - $studentSize));
end
// MinimumWorkingDays: The lectures of each course should be spread into a minimum number of days.
// Each day below the minimum counts as 5 points of penalty.
rule "minimumWorkingDays"
when
$course : Course($minWorkingDaySize : minWorkingDaySize)
accumulate(
$day : Day()
and exists Lecture(course == $course, day == $day);
$dayCount : count($day);
$dayCount < $minWorkingDaySize
)
// An uninitialized schedule should have no constraints broken
exists Lecture(course == $course, period != null)
then
scoreHolder.addSoftConstraintMatch(kcontext, (($dayCount.intValue() - $minWorkingDaySize) * 5));
end
// CurriculumCompactness: Lectures belonging to a curriculum should be adjacent
// to each other (i.e., in consecutive periods).
// For a given curriculum we account for a violation every time there is one lecture not adjacent
// to any other lecture within the same day.
// Each isolated lecture in a curriculum counts as 2 points of penalty.
rule "curriculumCompactness"
when
$curriculum : Curriculum()
Lecture(curriculumList contains $curriculum,
$day : day, $timeslotIndex : timeslotIndex, period != null
)
not Lecture(curriculumList contains $curriculum,
day == $day, timeslotIndex == ($timeslotIndex - 1)
)
not Lecture(curriculumList contains $curriculum,
day == $day, timeslotIndex == ($timeslotIndex + 1)
)
then
scoreHolder.addSoftConstraintMatch(kcontext, -2);
end
// RoomStability: All lectures of a course should be given in the same room.
// Each distinct room used for the lectures of a course, but the first, counts as 1 point of penalty.
rule "roomStability"
when
$course : Course()
accumulate(
$room : Room()
and exists Lecture(course == $course, room == $room);
$roomCount : count($room);
$roomCount > 1
)
then
scoreHolder.addSoftConstraintMatch(kcontext, -($roomCount.intValue() - 1));
end
public CourseSchedule createCourseSchedule(String fileName, int teacherListSize, int curriculumListSize, int courseListSize, int lectureListSize, int roomListSize) {
random = new Random(37);
CourseSchedule schedule = new CourseSchedule();
schedule.setId(0L);
createDayList(schedule);
createTimeslotList(schedule);
createPeriodList(schedule);
createTeacherList(schedule, teacherListSize);
createCourseList(schedule, courseListSize);
createLectureList(schedule, lectureListSize);
createRoomList(schedule, roomListSize);
createCurriculumList(schedule, curriculumListSize);
createUnavailablePeriodPenaltyList(schedule);
int possibleForOneLectureSize = schedule.getPeriodList().size() * schedule.getRoomList().size();
BigInteger possibleSolutionSize = BigInteger.valueOf(possibleForOneLectureSize).pow(
schedule.getLectureList().size());
logger.info("CourseSchedule {} has {} teachers, {} curricula, {} courses, {} lectures," +
" {} periods, {} rooms and {} unavailable period constraints with a search space of {}.",
fileName,
schedule.getTeacherList().size(),
schedule.getCurriculumList().size(),
schedule.getCourseList().size(),
schedule.getLectureList().size(),
schedule.getPeriodList().size(),
schedule.getRoomList().size(),
schedule.getUnavailablePeriodPenaltyList().size(),
getFlooredPossibleSolutionSize(possibleSolutionSize));
return schedule;
}
private void createDayList(CourseSchedule schedule) {
List<Day> dayList = new ArrayList<>(DAY_LIST_SIZE);
for (int i = 0; i < DAY_LIST_SIZE; i++) {
Day day = new Day();
day.setId((long) i);
day.setDayIndex(i);
day.setPeriodList(new ArrayList<>(TIMESLOT_LIST_SIZE));
dayList.add(day);
}
schedule.setDayList(dayList);
}
private void createTimeslotList(CourseSchedule schedule) {
List<Timeslot> timeslotList = new ArrayList<>(TIMESLOT_LIST_SIZE);
for (int i = 0; i < TIMESLOT_LIST_SIZE; i++) {
Timeslot timeslot = new Timeslot();
timeslot.setId((long) i);
timeslot.setTimeslotIndex(i);
timeslotList.add(timeslot);
}
schedule.setTimeslotList(timeslotList);
}
private void createPeriodList(CourseSchedule schedule) {
List<Period> periodList = new ArrayList<>(schedule.getDayList().size() * schedule.getTimeslotList().size());
long periodId = 0L;
for (Day day : schedule.getDayList()) {
for (Timeslot timeslot : schedule.getTimeslotList()) {
if (day.getDayIndex() == 2 && timeslot.getTimeslotIndex() >= 4) {
// No lectures Wednesday afternoon
continue;
}
Period period = new Period();
period.setId(periodId);
periodId++;
period.setDay(day);
day.getPeriodList().add(period);
period.setTimeslot(timeslot);
periodList.add(period);
}
}
schedule.setPeriodList(periodList);
}
private void createTeacherList(CourseSchedule schedule, int teacherListSize) {
List<Teacher> teacherList = new ArrayList<>(teacherListSize);
teacherNameGenerator.predictMaximumSizeAndReset(teacherListSize);
for (int i = 0; i < teacherListSize; i++) {
Teacher teacher = new Teacher();
teacher.setId((long) i);
teacher.setCode(teacherNameGenerator.generateNextValue());
teacherList.add(teacher);
}
schedule.setTeacherList(teacherList);
}
private void createCourseList(CourseSchedule schedule, int courseListSize) {
List<Teacher> teacherList = schedule.getTeacherList();
List<Course> courseList = new ArrayList<>(courseListSize);
Set<String> codeSet = new HashSet<>();
for (int i = 0; i < courseListSize; i++) {
Course course = new Course();
course.setId((long) i);
String code = (i < courseCodes.length * 2)
? courseCodes[i % courseCodes.length]
: courseCodes[random.nextInt(courseCodes.length)];
StringDataGenerator codeSuffixGenerator = new StringDataGenerator("")
.addAToZPart(true, 0);
if (courseListSize >= courseCodes.length) {
String codeSuffix = codeSuffixGenerator.generateNextValue();
while (codeSet.contains(code + codeSuffix)) {
codeSuffix = codeSuffixGenerator.generateNextValue();
}
code = code + codeSuffix;
codeSet.add(code);
}
course.setCode(code);
Teacher teacher = (i < teacherList.size() * 2)
? teacherList.get(i % teacherList.size())
: teacherList.get(random.nextInt(teacherList.size()));
course.setTeacher(teacher);
course.setLectureSize(0);
course.setMinWorkingDaySize(1);
course.setCurriculumList(new ArrayList<>());
course.setStudentSize(0);
courseList.add(course);
}
schedule.setCourseList(courseList);
}
private void createLectureList(CourseSchedule schedule, int lectureListSize) {
List<Course> courseList = schedule.getCourseList();
List<Lecture> lectureList = new ArrayList<>(lectureListSize);
for (int i = 0; i < lectureListSize; i++) {
Lecture lecture = new Lecture();
lecture.setId((long) i);
Course course = (i < courseList.size() * 2)
? courseList.get(i % courseList.size())
: courseList.get(random.nextInt(courseList.size()));
lecture.setCourse(course);
lecture.setLectureIndexInCourse(course.getLectureSize());
course.setLectureSize(course.getLectureSize() + 1);
lecture.setPinned(false);
lectureList.add(lecture);
}
schedule.setLectureList(lectureList);
}
private void createRoomList(CourseSchedule schedule, int roomListSize) {
List<Room> roomList = new ArrayList<>(roomListSize);
for (int i = 0; i < roomListSize; i++) {
Room room = new Room();
room.setId((long) i);
room.setCode("R" + ((i / 50 * 100) + 1 + i));
room.setCapacity(roomCapacityOptions[random.nextInt(roomCapacityOptions.length)]);
roomList.add(room);
}
schedule.setRoomList(roomList);
}
private void createCurriculumList(CourseSchedule schedule, int curriculumListSize) {
int maximumCapacity = schedule.getRoomList().stream().mapToInt(Room::getCapacity).max().getAsInt();
List<Course> courseList = schedule.getCourseList();
List<Curriculum> curriculumList = new ArrayList<>(curriculumListSize);
StringDataGenerator codeGenerator = new StringDataGenerator("")
.addAToZPart(true, 0).addAToZPart(false, 1).addAToZPart(false, 1).addAToZPart(false, 1);
codeGenerator.predictMaximumSizeAndReset(curriculumListSize);
for (int i = 0; i < curriculumListSize; i++) {
Curriculum curriculum = new Curriculum();
curriculum.setId((long) i);
curriculum.setCode("Group " + codeGenerator.generateNextValue());
// The studentSize is more likely to be 15 than 5 or 25
int studentSize = 5 + random.nextInt(10) + random.nextInt(10);
List<Course> courseSubList = courseList.stream()
.filter(course -> course.getStudentSize() + studentSize < maximumCapacity)
.collect(Collectors.toList());
Collections.shuffle(courseSubList, random);
int lectureCount = 0;
for (Course course : courseSubList) {
lectureCount += course.getLectureSize();
if (lectureCount > PERIOD_LIST_SIZE) {
break;
}
course.getCurriculumList().add(curriculum);
course.setStudentSize(course.getStudentSize() + studentSize);
}
curriculumList.add(curriculum);
}
schedule.setCurriculumList(curriculumList);
}
private void createUnavailablePeriodPenaltyList(CourseSchedule schedule) {
List<Course> courseList = schedule.getCourseList();
List<Period> periodList = schedule.getPeriodList();
List<UnavailablePeriodPenalty> unavailablePeriodPenaltyList = new ArrayList<>(courseList.size());
long penaltyId = 0L;
for (Course course : courseList) {
UnavailablePeriodPenalty penalty = new UnavailablePeriodPenalty();
penalty.setId(penaltyId);
penaltyId++;
penalty.setCourse(course);
penalty.setPeriod(periodList.get(random.nextInt(periodList.size())));
unavailablePeriodPenaltyList.add(penalty);
}
schedule.setUnavailablePeriodPenaltyList(unavailablePeriodPenaltyList);
}