diff --git a/java/hello-world/src/main/java/org/acme/schooltimetabling/TimetableApp.java b/java/hello-world/src/main/java/org/acme/schooltimetabling/TimetableApp.java index 4002711a..d62164ef 100644 --- a/java/hello-world/src/main/java/org/acme/schooltimetabling/TimetableApp.java +++ b/java/hello-world/src/main/java/org/acme/schooltimetabling/TimetableApp.java @@ -22,7 +22,7 @@ import java.util.*; public class TimetableApp { - +//TODO andrea schuman name udpate in excel file, update in the db as well or at least check over this private static final Logger LOGGER = LoggerFactory.getLogger(TimetableApp.class); private static final String YAML_FILE_PATH = "constants/config.yaml"; private static final boolean PRINT_DETAILED_SUMMARY = true; @@ -102,9 +102,9 @@ public static void main(String[] args) throws Exception{ }); } -// storeResults(solution); ResultSaver resultSaver = new ResultSaver(solution); resultSaver.saveSolution(); + resultSaver.teacherTimesToJson(); return; } diff --git a/java/hello-world/src/main/java/org/acme/schooltimetabling/apiCalls/surveyEndpoint/SurveyRecord.java b/java/hello-world/src/main/java/org/acme/schooltimetabling/apiCalls/surveyEndpoint/SurveyRecord.java index 1cefde96..3d88bd0d 100644 --- a/java/hello-world/src/main/java/org/acme/schooltimetabling/apiCalls/surveyEndpoint/SurveyRecord.java +++ b/java/hello-world/src/main/java/org/acme/schooltimetabling/apiCalls/surveyEndpoint/SurveyRecord.java @@ -4,7 +4,9 @@ import com.fasterxml.jackson.annotation.JsonProperty; import org.acme.schooltimetabling.TimetableApp; import org.acme.schooltimetabling.apiCalls.teacherEndpoint.TeacherRecord; +import org.acme.schooltimetabling.constants.Constants; import org.acme.schooltimetabling.constants.Days; +import org.acme.schooltimetabling.constants.Preference; import org.acme.schooltimetabling.domain.teacher.Faculty; import org.acme.schooltimetabling.domain.teacher.Teacher; import org.acme.schooltimetabling.helperClasses.BitSetHelper; @@ -98,7 +100,8 @@ else if(val.equalsIgnoreCase("acceptable")){ conflict.or(bsRep); } } - Teacher teacher = new Teacher(TeacherGenerator.getNextTeacherID(), nonCanonName, preferences, acceptable, conflict); + Teacher teacher = new Teacher(TeacherGenerator.getNextTeacherID(), Constants.TEACHER_NAME_TO_CANON.get(nonCanonName), + preferences, acceptable, conflict, Preference.parsePref((String) extraFields.get("gap"))); if(teacherRecord.isFaculty()) teacher = new Faculty(teacher); return teacher; } diff --git a/java/hello-world/src/main/java/org/acme/schooltimetabling/constants/Constants.java b/java/hello-world/src/main/java/org/acme/schooltimetabling/constants/Constants.java index 5a555336..8cfde997 100644 --- a/java/hello-world/src/main/java/org/acme/schooltimetabling/constants/Constants.java +++ b/java/hello-world/src/main/java/org/acme/schooltimetabling/constants/Constants.java @@ -14,6 +14,7 @@ import org.slf4j.LoggerFactory; import java.io.InputStream; +import java.time.format.DateTimeFormatter; import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -23,6 +24,10 @@ * fit anywhere else, like a class. */ public class Constants { + /** + * Global time formatter + */ + public static final DateTimeFormatter TIME_FMT = DateTimeFormatter.ofPattern("h:mma"); /** * Set to True for testing. Helps by pass some checks * */ diff --git a/java/hello-world/src/main/java/org/acme/schooltimetabling/constants/Preference.java b/java/hello-world/src/main/java/org/acme/schooltimetabling/constants/Preference.java new file mode 100644 index 00000000..7663a45d --- /dev/null +++ b/java/hello-world/src/main/java/org/acme/schooltimetabling/constants/Preference.java @@ -0,0 +1,27 @@ +package org.acme.schooltimetabling.constants; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public enum Preference { + + AGREE, NEUTRAL, DISAGREE; + + private static final Logger LOGGER = LoggerFactory.getLogger(Process.class); + + /** + *

case insensitive maps ["", "disagree"] -> {@link #DISAGREE}; ["agree"] -> {@link #AGREE}; + * ["neutral"] -> {@link #NEUTRAL}

+ *

if the string doesn't match any of these options. The program will exit until resolved

+ * @param pref String representation of preference + * @return enum representation or error/exits if no match is found + */ + public static Preference parsePref(String pref){ + if("".equals(pref) || "disagree".equalsIgnoreCase(pref)) return Preference.DISAGREE; + else if("agree".equalsIgnoreCase(pref)) return Preference.AGREE; + else if("neutral".equalsIgnoreCase(pref)) return Preference.NEUTRAL; + LOGGER.error("When reading a survey unknown preference was encountered '{}'.... EXITING", pref); + System.exit(1); + throw new IllegalStateException(String.format("Unknown preference '%s' encountered while reading survey", pref)); + } +} diff --git a/java/hello-world/src/main/java/org/acme/schooltimetabling/domain/lesson/Lesson.java b/java/hello-world/src/main/java/org/acme/schooltimetabling/domain/lesson/Lesson.java index 4fb5c63d..19334090 100644 --- a/java/hello-world/src/main/java/org/acme/schooltimetabling/domain/lesson/Lesson.java +++ b/java/hello-world/src/main/java/org/acme/schooltimetabling/domain/lesson/Lesson.java @@ -4,6 +4,7 @@ import ai.timefold.solver.core.api.domain.lookup.PlanningId; import ai.timefold.solver.core.api.domain.variable.PlanningVariable; import org.acme.schooltimetabling.constants.Constants; +import org.acme.schooltimetabling.constants.Days; import org.acme.schooltimetabling.domain.Room; import org.acme.schooltimetabling.domain.Timeslot; import org.acme.schooltimetabling.domain.teacher.Teacher; @@ -12,7 +13,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; import java.util.BitSet; +import java.util.List; +import java.util.Map; @PlanningEntity(difficultyComparatorClass = LessonComparator.class) //@PlanningEntity(comparator = LessonComparator.class) @@ -37,7 +43,6 @@ public class Lesson { public boolean hasLecture, hasLabAct; public int lecHours, labActHours; public Teacher teacherObj; - private Integer linker = null; @PlanningVariable @@ -55,38 +60,16 @@ private Lesson() { * https://docs.timefold.ai/timefold-solver/latest/using-timefold-solver/modeling-planning-problems#planningId*/ /* Test factory methods */ - - /** - * No linker - */ public static Lesson test_buildLesson(String Id, int lecSection, String courseName, String teacherName, String modifiers, String courseConfig, int courseID, Teacher teacherObj, Timeslot timeslot, Room room){ return new Lesson(Id, lecSection, courseName, teacherName, modifiers, courseConfig, courseID, teacherObj , timeslot, room); } - /** - * with linker - */ - public static Lesson test_buildLesson(String Id, int lecSection, String courseName, String teacherName, String modifiers, - String courseConfig, int courseID, Teacher teacherObj, Timeslot timeslot, Room room, - Integer linker){ - return new Lesson(Id, lecSection, courseName, teacherName, modifiers, courseConfig, courseID, teacherObj - , timeslot, room, linker); - } /* Test constructor(s)*/ - private Lesson(String Id, int lecSection, String courseName, String teacherName, String modifiers, - String courseConfig, int courseID, Teacher teacherObj, Timeslot timeslot, Room room, Integer linker){ - this(Id, lecSection, courseName, teacherName, modifiers, courseConfig, courseID, teacherObj - , timeslot, room); - this.linker = linker; - } - private Lesson(String Id, int lecSection, String courseName, String teacherName, String modifiers, String courseConfig, int courseID, Teacher teacherObj, Timeslot timeslot, Room room){ -// /*calling normal constructor used during setup*/ -// this(Id, lecSection, courseName, teacherName, modifiers, courseConfig, courseID, teacherObj, null); /*the courseConfig stream is assumed to come in the format * E-L-A where E is the number of lecture units, L is the number of * lab units, and A is the number of activity units */ @@ -131,7 +114,7 @@ private Lesson(String Id, int lecSection, String courseName, String teacherName, * @param teacherObj teacher object associated with the teacherName */ public Lesson(String Id, int lecSection, String courseName, String modifiers, - String courseConfig, Teacher teacherObj, Integer linker){ + String courseConfig, Teacher teacherObj){ /*the courseConfig stream is assumed to come in the format * E-L-A where E is the number of lecture units, L is the number of * lab units, and A is the number of activity units */ @@ -156,7 +139,6 @@ public Lesson(String Id, int lecSection, String courseName, String modifiers, /*TODO check if we can delete this field*/ this.teacherName = teacherObj.getName(); this.modifiers = modifiers; - this.linker = linker; } /** @@ -250,10 +232,6 @@ public Teacher getTeacherObj() { return teacherObj; } - public Integer getLinker(){ - return linker; - } - public boolean isStudio(){ return Constants.STUDIO_STYLE_COURSES.contains(this.courseName); } @@ -313,4 +291,35 @@ public BitSet maskOutCmprs(){ copy.and(ScheduleConfig.getCompressOutMask()); return copy; } + + public List> toJson(){ + List> res = new ArrayList<>(); + if (timeslot == null) { + return res; + } + + BitSet schedule = new BitSet(); + schedule.or(timeslot.getAllTimesBitSet()); + LocalTime BASE_TIME = LocalTime.of(7, 0); + + for (Days day : Days.values()) { + int offset = BitSetHelper.DAY_OFFSET.get(day); + BitSet dayBits = schedule.get(offset, offset + BitSetHelper.MAX_BITS_PER_DAY); + int start = dayBits.nextSetBit(0); + while (start != -1) { + + int end = dayBits.nextClearBit(start) - 1; + LocalTime startTime = BASE_TIME.plusMinutes(start * 30L); + LocalTime endTime = BASE_TIME.plusMinutes((end + 1) * 30L); + res.add(Map.of( + "day", day.name(), + "start", startTime.format(Constants.TIME_FMT), + "end", endTime.format(Constants.TIME_FMT) + )); + start = dayBits.nextSetBit(end + 1); + } + } + + return res; + } } diff --git a/java/hello-world/src/main/java/org/acme/schooltimetabling/domain/teacher/Faculty.java b/java/hello-world/src/main/java/org/acme/schooltimetabling/domain/teacher/Faculty.java index 2e555e02..619abd9a 100644 --- a/java/hello-world/src/main/java/org/acme/schooltimetabling/domain/teacher/Faculty.java +++ b/java/hello-world/src/main/java/org/acme/schooltimetabling/domain/teacher/Faculty.java @@ -4,6 +4,7 @@ import org.acme.schooltimetabling.TimetableApp; import org.acme.schooltimetabling.constants.Constants; import org.acme.schooltimetabling.constants.Days; +import org.acme.schooltimetabling.constants.Preference; import org.acme.schooltimetabling.helperClasses.BitSetHelper; import org.acme.schooltimetabling.helperClasses.ParseInput; import org.acme.schooltimetabling.helperClasses.ScheduleConfig; @@ -78,6 +79,10 @@ public Faculty(int id, String name, BitSet preferences, BitSet acceptable, BitSe super(id, name, preferences, acceptable, conflict); } + public Faculty(int id, String name, BitSet preferences, BitSet acceptable, BitSet conflict, Preference gap) { + super(id, name, preferences, acceptable, conflict, gap); + } + public Faculty(Teacher teacher){ super(teacher); } diff --git a/java/hello-world/src/main/java/org/acme/schooltimetabling/domain/teacher/Teacher.java b/java/hello-world/src/main/java/org/acme/schooltimetabling/domain/teacher/Teacher.java index 471d6015..47ffce45 100644 --- a/java/hello-world/src/main/java/org/acme/schooltimetabling/domain/teacher/Teacher.java +++ b/java/hello-world/src/main/java/org/acme/schooltimetabling/domain/teacher/Teacher.java @@ -1,5 +1,7 @@ package org.acme.schooltimetabling.domain.teacher; +import org.acme.schooltimetabling.constants.Preference; + import java.util.BitSet; public class Teacher { @@ -12,6 +14,12 @@ public class Teacher { public BitSet preferences; /*impossible timeslots*/ public BitSet conflict; + private Preference gapPref; + + public Teacher(int id, String name, BitSet preferences, BitSet acceptable, BitSet conflict, Preference gapPref){ + this(id, name, preferences, acceptable, conflict); + this.gapPref = gapPref; + } public Teacher(int id, String name, BitSet preferences, BitSet acceptable, BitSet conflict) { this.id = id; @@ -27,6 +35,7 @@ protected Teacher(Teacher copyMe){ this.preferences = (BitSet) copyMe.preferences.clone(); this.acceptable = (BitSet) copyMe.acceptable.clone(); this.conflict = (BitSet) copyMe.conflict.clone(); + this.gapPref = copyMe.gapPref; } public int getId() { @@ -48,4 +57,6 @@ public BitSet getPreferences() { public BitSet getConflict() { return conflict; } + + public Preference getGapPref(){ return gapPref; } } diff --git a/java/hello-world/src/main/java/org/acme/schooltimetabling/helperClasses/BitSetHelper.java b/java/hello-world/src/main/java/org/acme/schooltimetabling/helperClasses/BitSetHelper.java index c5abab9f..38e001cd 100644 --- a/java/hello-world/src/main/java/org/acme/schooltimetabling/helperClasses/BitSetHelper.java +++ b/java/hello-world/src/main/java/org/acme/schooltimetabling/helperClasses/BitSetHelper.java @@ -1,26 +1,36 @@ package org.acme.schooltimetabling.helperClasses; +import org.acme.schooltimetabling.constants.Constants; import org.acme.schooltimetabling.constants.Days; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.time.LocalTime; import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; import java.util.BitSet; import java.util.EnumSet; +import java.util.Map; public class BitSetHelper { private static final Logger LOGGER = LoggerFactory.getLogger(BitSetHelper.class); - private static final int MONDAY_OFFSET = 0; - private static final int TUESDAY_OFFSET = 30; - private static final int WEDNESDAY_OFFSET = 60; - private static final int THURSDAY_OFFSET = 90; - private static final int FRIDAY_OFFSET = 120; + public static final int MONDAY_OFFSET = 0; + public static final int TUESDAY_OFFSET = 30; + public static final int WEDNESDAY_OFFSET = 60; + public static final int THURSDAY_OFFSET = 90; + public static final int FRIDAY_OFFSET = 120; private static final int NUM_OF_BITS = 150; private static final int PRIME_TIME_DAY_START_OFFSET = 4; private static final int PRIME_TIME_DAY_END_OFFSET = 16; - private static final int MAX_BITS_PER_DAY = 30; + public static final int MAX_BITS_PER_DAY = 30; public static final BitSet NON_PRIME_TIME_MASK; public static final BitSet PRIME_TIME_MASK; + public static final Map DAY_OFFSET = Map.of( + Days.MONDAY, BitSetHelper.MONDAY_OFFSET, + Days.TUESDAY, BitSetHelper.TUESDAY_OFFSET, + Days.WEDNESDAY, BitSetHelper.WEDNESDAY_OFFSET, + Days.THURSDAY, BitSetHelper.THURSDAY_OFFSET, + Days.FRIDAY, BitSetHelper.FRIDAY_OFFSET + ); static { PRIME_TIME_MASK = new BitSet(); @@ -179,4 +189,28 @@ public static BitSet old_surveyBitset(String header) throws Exception{ return bitset; } + + + public static BitSet timeJsonToBs(Map time) { + if (time == null || time.get("day") == null || time.get("start") == null || time.get("end") == null) { + throw new IllegalArgumentException("Time map must contain day, start, and end"); + } + LocalTime start = LocalTime.parse(time.get("start"), Constants.TIME_FMT); + LocalTime end = LocalTime.parse(time.get("end"), Constants.TIME_FMT); + LocalTime earliest = LocalTime.of(7, 0); + LocalTime latest = LocalTime.of(22, 0); + if (start.isBefore(earliest) || end.isAfter(latest) || !end.isAfter(start)) { + throw new IllegalArgumentException("Times must be between 7:00AM and 10:00PM and end after start"); + } + if ((start.getMinute() % 30) != 0 || (end.getMinute() % 30) != 0) { + throw new IllegalArgumentException("Times must fall exactly on the hour or half-hour"); + } + Days day = Days.valueOf(time.get("day").toUpperCase()); + int startBlock = (int) ChronoUnit.MINUTES.between(earliest, start) / 30; + int endBlock = (int) ChronoUnit.MINUTES.between(earliest, end) / 30; + int dayOffset = DAY_OFFSET.get(day); + BitSet bitSet = new BitSet(); + bitSet.set(dayOffset + startBlock, dayOffset + endBlock); + return bitSet; + } } diff --git a/java/hello-world/src/main/java/org/acme/schooltimetabling/helperClasses/Generators/LessonGenerator.java b/java/hello-world/src/main/java/org/acme/schooltimetabling/helperClasses/Generators/LessonGenerator.java index e255a2bf..6e14fcdf 100644 --- a/java/hello-world/src/main/java/org/acme/schooltimetabling/helperClasses/Generators/LessonGenerator.java +++ b/java/hello-world/src/main/java/org/acme/schooltimetabling/helperClasses/Generators/LessonGenerator.java @@ -1,8 +1,10 @@ package org.acme.schooltimetabling.helperClasses.Generators; import org.acme.schooltimetabling.constants.Constants; +import org.acme.schooltimetabling.constants.Preference; import org.acme.schooltimetabling.domain.lesson.Lesson; import org.acme.schooltimetabling.domain.teacher.Faculty; +import org.acme.schooltimetabling.helperClasses.ParseInput; import org.acme.schooltimetabling.helperClasses.ScheduleConfig; import org.acme.schooltimetabling.helperClasses.ScheduleFormat; import org.acme.schooltimetabling.domain.teacher.Teacher; @@ -17,6 +19,7 @@ public class LessonGenerator extends Generator{ public static boolean OLD_studio_detected = false; public static boolean proper_studio_detected = false; private static final Logger LOGGER = LoggerFactory.getLogger(LessonGenerator.class); + private static final Map>> PRESCHED_TIMES = ParseInput.readPrescheduledFile(); /** * Keeps track of the next available section number available for a course */ @@ -26,7 +29,6 @@ public class LessonGenerator extends Generator{ * available lesson ID rather than using this attribute directly. */ private static int lessonID = 1; - private static int availableLinkerID = 1; private static List skippedLessons = new ArrayList<>(); static { @@ -133,7 +135,6 @@ public static ArrayList generateLessons(List schedules, */ private static Lesson generateLesson(Map teacherHashMap, Map courseSectionCounter, String course, String teacherName){ - final Integer NO_LESSON_LINKER = null; String courseConfig; boolean hasLabOrAct; int sectionNumber; @@ -159,7 +160,7 @@ private static Lesson generateLesson(Map teacherHashMap, Map teacherHashMap, String te if(teacher == null){ if(Constants.DEBUG){ LOGGER.warn(String.format("Couldn't find a teacher object for '%s'. Most likely due to them not having" + - " a survey filled out;" + + " a survey filled out or old noncanon-canon mapping is used;" + "Creating one for them with now with no conflict, pref, or acceptable times.", teacherName)); } //create teacher object teacher = noSurveyTeacher(teacherName); + //add prescheduled times if possible + if(PRESCHED_TIMES.containsKey(teacherName)){ + LOGGER.info(String.format("Found a prescheduled time for '%s'. Adding the time to their conflict bitset.", + teacherName)); + + try { + BitSet addConflict = TeacherGenerator.createPreschedBs(PRESCHED_TIMES.get(teacherName)); + teacher.getAcceptable().andNot(addConflict); + teacher.getPreferences().andNot(addConflict); + teacher.getConflict().or(addConflict); + } catch (Exception e) { + LOGGER.info("Couldn't parse prescheduled times for '{}' because of error: '{}'", + teacherName, e.getMessage()); + } + } + teacherHashMap.put(teacherName, teacher); } @@ -291,76 +308,6 @@ private static boolean skipCourse(String modifier, String name, String config){ } - - - /** NOTE this not how true studios should be handled - * Studio style helper to create special lessons for the studio style courses - * @param name name of the course (i.e. csc457) - * @param modifier course modifier; if none present use them empty string - * @param config configuration to use for the split - * @param teacher instructor teaching the course - * @return a studio style course split into a lecture and either a lab or activity lesson; First element is the - * lecture lesson - */ - private static Pair studioHelper(String name, String modifier, String config, Teacher teacher){ - //leaving out while proper studio implementation - if(true) throw new UnsupportedOperationException("This is not what a proper studio is. This implementation actually" + - " is a nice to have."); - /* units lecture-lab-activity */ - final int LECTURE = 0; - final int LAB = 1; - final int ACT = 2; - String[] configParsed = config.split("-"); - int lecUnits = Integer.parseInt(configParsed[LECTURE]); - int labUnits = Integer.parseInt(configParsed[LAB]); - int actUnits = Integer.parseInt(configParsed[ACT]); - - String newConfig; - int idToUse; - int sectionNumber; - - //debug comments - if(lecUnits == 0) { - LOGGER.error(String.format("studio style course '%s' has no lecture; implement logic for this", name)); - return null; - } - if(labUnits == 0 && actUnits == 0){ - LOGGER.error(String.format("studio style course '%s' has no lab or activity; implement logic for this. " + - "I don't think this is possible though", name)); - return null; - } - - /*TODO: I could have sworn I saw a studio style course that had no lecture. If this is possible, - * then we will make a course with only a lecture or activity sort of like a normal course but force - * studio space to be all on the same day continuously*/ - /*Assuming studio style courses have a lecture and either a lab or activity*/ - /*create a lesson for the lecture portion*/ - final int LINKER_ID = nxtLinkerID(); - newConfig = String.format("%d-0-0", lecUnits); - sectionNumber = COURSE_SECTION_COUNTER.get(name); - COURSE_SECTION_COUNTER.replace(name, sectionNumber + 1); - Lesson lecLesson = new Lesson(Integer.toString(nxtLessonID()), sectionNumber, name - , modifier, newConfig, teacher, LINKER_ID); - - - /*create a lesson for the lab or activity portion of the course*/ - if(labUnits > 0){ - newConfig = String.format("0-%d-0", labUnits); - } - else{ - newConfig = String.format("0-0-%d", actUnits); - } - sectionNumber = COURSE_SECTION_COUNTER.get(name); - COURSE_SECTION_COUNTER.replace(name, sectionNumber + 1); - Lesson labActLesson = new Lesson(Integer.toString(nxtLessonID()), sectionNumber, name - , modifier, newConfig, teacher, LINKER_ID); - - return new Pair<>(lecLesson, labActLesson); - } - - - - /** * This function is used to create a teacher object during lesson creation if a teacher object can't be found * for the name. It will return a faculty object if the person is found to be a faculty member. Note that the object @@ -378,10 +325,12 @@ private static Teacher noSurveyTeacher(String name){ if(Constants.FACULTY_LAST_NAMES.contains(nameFragments[LAST_NAME_POS])){ LOGGER.info(String.format("Found teacher '%s' to be a faculty member. Promoting Teacher obj to Faculty" , name)); - return new Faculty(TeacherGenerator.getNextTeacherID(), name, new BitSet(), new BitSet(), new BitSet()); + return new Faculty(TeacherGenerator.getNextTeacherID(), name, new BitSet(), new BitSet(), new BitSet(), + Preference.NEUTRAL); } - return new Teacher(TeacherGenerator.getNextTeacherID(), name, new BitSet(), new BitSet(), new BitSet()); + return new Teacher(TeacherGenerator.getNextTeacherID(), name, new BitSet(), new BitSet(), new BitSet(), + Preference.NEUTRAL); } @@ -417,17 +366,6 @@ private static int nxtLessonID(){ } - - - /** - * Helper function to ensure the {@link #availableLinkerID} is updated when the current linker ID is used. - * Note that only one ID should be used per a pair of lecture and lab/act. - * @return give the next available linker - */ - private static int nxtLinkerID(){ - return availableLinkerID++; - } - public static List getSkippedLessons(){ return skippedLessons; } } diff --git a/java/hello-world/src/main/java/org/acme/schooltimetabling/helperClasses/Generators/TeacherGenerator.java b/java/hello-world/src/main/java/org/acme/schooltimetabling/helperClasses/Generators/TeacherGenerator.java index fdfcbeb8..33ab5daf 100644 --- a/java/hello-world/src/main/java/org/acme/schooltimetabling/helperClasses/Generators/TeacherGenerator.java +++ b/java/hello-world/src/main/java/org/acme/schooltimetabling/helperClasses/Generators/TeacherGenerator.java @@ -5,6 +5,7 @@ import org.acme.schooltimetabling.apiCalls.teacherEndpoint.TeacherRecord; import org.acme.schooltimetabling.constants.Constants; import org.acme.schooltimetabling.constants.Days; +import org.acme.schooltimetabling.constants.Preference; import org.acme.schooltimetabling.domain.teacher.Faculty; import org.acme.schooltimetabling.helperClasses.BitSetHelper; import org.acme.schooltimetabling.domain.teacher.Teacher; @@ -25,9 +26,8 @@ public class TeacherGenerator extends Generator{ * * @return A Map mapping a teacher's canon name to their teacher object */ - public static Map teacherGenDriver() throws Exception{ - String YAML_FILE_PATH = "constants/config.yaml"; - ScheduleConfig.loadConfig(YAML_FILE_PATH); + public static Map teacherGenDriver(){ + ScheduleConfig.loadConfig("constants/config.yaml"); Map teacherMap = new HashMap<>(); boolean success = false; if(ScheduleConfig.isUseApi()){ @@ -39,6 +39,7 @@ public static Map teacherGenDriver() throws Exception{ } else{ for(SurveyRecord surveyRecord: surveys){ + //just make getTEacherRecord the only one called surveyRecord.toTeacher(); TeacherRecord teacherRecord = surveyRecord.getTeacherRecord(); teacherMap.put(teacherRecord.getCanon(), surveyRecord.toTeacher()); @@ -94,6 +95,12 @@ public static Map teacherGenDriver() throws Exception{ teacherMap = TeacherGenerator.generateTeachers(curQuarterSurveys, prevQuarterSurveys); } + if(ScheduleConfig.getPrescheduledFileName() != null){ + LOGGER.info("Reading in prescheduled time file"); + Map>> preschedTimes = ParseInput.readPrescheduledFile(); + if(!preschedTimes.isEmpty()) prescheduleUpdate(teacherMap, preschedTimes); + } + return teacherMap; } @@ -171,7 +178,7 @@ public static HashMap generateTeachers(List surveyEntry){ } String[] splitName = canonName.split(","); - + Preference pref = Preference.parsePref(surveyEntry.get("gap")); if(Constants.FACULTY_LAST_NAMES.contains(splitName[0].strip())){ LOGGER.info(String.format("Instructor '%s' identified as faculty", canonName)); - return new Faculty(getNextTeacherID(), instructorName, preferred, acceptable, conflicts); + return new Faculty(getNextTeacherID(), canonName, preferred, acceptable, conflicts, pref); + } + return new Teacher(getNextTeacherID(), canonName, preferred, acceptable, conflicts, pref); + } + + //TODO note to update the teachers that get made on the fly if their teacher object is not found + private static void prescheduleUpdate(Map teacherMap, Map>> presched){ + Iterator>>> iterator = presched.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry>> entry = iterator.next(); + String name = entry.getKey(); + Teacher teacher = teacherMap.get(name); + if (teacher == null) continue; + + try { + BitSet addConflict = createPreschedBs(entry.getValue()); + teacher.getAcceptable().andNot(addConflict); + teacher.getPreferences().andNot(addConflict); + teacher.getConflict().or(addConflict); + iterator.remove(); // safe removal while iterating + } catch (Exception e) { + LOGGER.info("Couldn't parse prescheduled times for '{}' because of error: '{}'", + name, e.getMessage()); + } } - return new Teacher(getNextTeacherID(), canonName, preferred, acceptable, conflicts); + + if(!presched.isEmpty()){ + LOGGER.info("When updating teachers with their prescheduled conflicts, the following teachers " + + "had no corresponding object: {}", presched.keySet()); + } + } + + public static BitSet createPreschedBs(List> timeJson){ + BitSet bs = new BitSet(); + for(Map time: timeJson){ + bs.or(BitSetHelper.timeJsonToBs(time)); + } + + return bs; } diff --git a/java/hello-world/src/main/java/org/acme/schooltimetabling/helperClasses/ParseInput.java b/java/hello-world/src/main/java/org/acme/schooltimetabling/helperClasses/ParseInput.java index 33a9a3f4..acd4700e 100644 --- a/java/hello-world/src/main/java/org/acme/schooltimetabling/helperClasses/ParseInput.java +++ b/java/hello-world/src/main/java/org/acme/schooltimetabling/helperClasses/ParseInput.java @@ -218,5 +218,18 @@ public static Set getFaculty(String file) { return faculty; } + public static Map>> readPrescheduledFile(){ + String filePath = "input/" + ScheduleConfig.getPrescheduledFileName(); + try(InputStream inputStream = getResourceAsStream(filePath)){ + ObjectMapper mapper = new ObjectMapper(); + + return mapper.readValue(inputStream, new TypeReference<>() {}); + } catch (Exception e) { + ParseInput.LOGGER.error("ERROR reading file containing teachers prescheduled times. Skipping inclusion of " + + "these times."); + return new HashMap<>(); + } + } + } diff --git a/java/hello-world/src/main/java/org/acme/schooltimetabling/helperClasses/ResultSaver.java b/java/hello-world/src/main/java/org/acme/schooltimetabling/helperClasses/ResultSaver.java index 75cf1b76..0e107b50 100644 --- a/java/hello-world/src/main/java/org/acme/schooltimetabling/helperClasses/ResultSaver.java +++ b/java/hello-world/src/main/java/org/acme/schooltimetabling/helperClasses/ResultSaver.java @@ -8,6 +8,8 @@ import ai.timefold.solver.core.api.solver.SolutionManager; import ai.timefold.solver.core.api.solver.SolverFactory; import ai.timefold.solver.core.config.solver.SolverConfig; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; import org.acme.schooltimetabling.constants.Constants; import org.acme.schooltimetabling.constants.Days; import org.acme.schooltimetabling.domain.lesson.Lesson; @@ -16,6 +18,7 @@ import org.acme.schooltimetabling.domain.teacher.Teacher; import org.acme.schooltimetabling.helperClasses.Generators.LessonGenerator; import org.acme.schooltimetabling.solver.justifications.WrongHoursAmountJustification; +import org.apache.commons.math3.util.Pair; import org.apache.poi.ss.usermodel.*; import org.apache.poi.ss.util.CellRangeAddress; import org.apache.poi.xssf.usermodel.XSSFSheet; @@ -24,9 +27,11 @@ import org.slf4j.LoggerFactory; import java.io.FileOutputStream; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.time.LocalDateTime; import java.time.LocalTime; import java.time.format.DateTimeFormatter; import java.util.*; @@ -83,16 +88,14 @@ private void buildTeacherMap(){ } } - public void saveSolution(){ - final XSSFWorkbook workbook = new XSSFWorkbook(); - headerCellStyle = workbook.createCellStyle(); - headerCellStyle.setVerticalAlignment(VerticalAlignment.CENTER); - headerCellStyle.setAlignment(HorizontalAlignment.CENTER); - lessonCellStyle = workbook.createCellStyle(); - lessonCellStyle.setWrapText(true); - final XSSFSheet roomSheet = workbook.createSheet("Room Usage"); - final XSSFSheet teacherSheet = workbook.createSheet("Teacher schedule"); - final XSSFSheet lessonListSheet = workbook.createSheet("List View"); + /** + * Extracts lessons from the current solution and separates them out into those that violate do and don't violate + * hard constraints. If no solution has been passed it will return a pair of empty lists. + * @return a pair where the first value are lessons with no hard constraint violation and the second value are + * lessons that violate at least one hard constraint + */ + private Pair, List> extractLessons(){ + if(solToPrint == null) return new Pair<>(List.of(), List.of()); List allLessons = solToPrint.getLessons(); // your full list of lessons @@ -131,14 +134,37 @@ public void saveSolution(){ } } }); - List lessonsWithHardViolations = allLessons.stream() - .filter(penalizedLessons::contains) - .toList(); List lessonsWithoutHardViolations = allLessons.stream() .filter(lesson -> !penalizedLessons.contains(lesson)) .toList(); + List lessonsWithHardViolations = allLessons.stream() + .filter(penalizedLessons::contains) + .toList(); + + + return new Pair<>(lessonsWithoutHardViolations, lessonsWithHardViolations); + } + + + /** + * Saves the created solution to an Excel file + */ + public void saveSolution(){ + final XSSFWorkbook workbook = new XSSFWorkbook(); + headerCellStyle = workbook.createCellStyle(); + headerCellStyle.setVerticalAlignment(VerticalAlignment.CENTER); + headerCellStyle.setAlignment(HorizontalAlignment.CENTER); + lessonCellStyle = workbook.createCellStyle(); + lessonCellStyle.setWrapText(true); + final XSSFSheet roomSheet = workbook.createSheet("Room Usage"); + final XSSFSheet teacherSheet = workbook.createSheet("Teacher schedule"); + final XSSFSheet lessonListSheet = workbook.createSheet("List View"); + + Pair, List> extractedLessons = extractLessons(); + List lessonsWithoutHardViolations = extractedLessons.getKey(); + List lessonsWithHardViolations = extractedLessons.getValue(); roomView(roomSheet, lessonsWithoutHardViolations); teacherView(teacherSheet, lessonsWithoutHardViolations); @@ -474,4 +500,48 @@ private String lecToStr(Lesson lesson){ } + public void teacherTimesToJson() throws IOException { + Pair, List> extracted = extractLessons(); + //we only consider lessons that don't violate any hard constraints as actually being scheduled + List lsWithNoHard = extracted.getKey(); + + Map>> res = new HashMap<>(); + for(Lesson lesson: lsWithNoHard){ + final String teacherName = lesson.getTeacherObj().getName(); + if(!res.containsKey(teacherName)) res.put(teacherName, new ArrayList<>()); + List> teacherMap = res.get(teacherName); + teacherMap.addAll(lesson.toJson()); + } + + Scanner retryScanner = new Scanner(System.in); + boolean retryAllowed = true; + + + //try to save json to file + while (true) { + try { + DateTimeFormatter JSON_FILE_TIMESTAMP = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss"); + String FILE_NAME = "times_" + LocalDateTime.now().format(JSON_FILE_TIMESTAMP) + ".json"; + ObjectMapper OBJECT_MAPPER = new ObjectMapper() + .enable(SerializationFeature.INDENT_OUTPUT); + Path outputDir = Paths.get("generated"); + Path target = outputDir.resolve(FILE_NAME); + Files.createDirectories(outputDir); + OBJECT_MAPPER.writerWithDefaultPrettyPrinter().writeValue(target.toFile(), res); + LOGGER.info("Teacher schedule saved to JSON: {}", target); + break; + } catch (IOException e) { + if (!retryAllowed) { + LOGGER.error("Still unable to write JSON after retry: {}", e.getMessage()); + throw e; + } + LOGGER.warn("Unable to write teacher JSON (it might be open elsewhere): {}", e.getMessage()); + System.out.println("Press Enter once the file is closed, then the JSON write will be retried."); + retryScanner.nextLine(); + retryAllowed = false; + } + } + } + + } diff --git a/java/hello-world/src/main/java/org/acme/schooltimetabling/helperClasses/ScheduleConfig.java b/java/hello-world/src/main/java/org/acme/schooltimetabling/helperClasses/ScheduleConfig.java index ccdab32f..5a49831d 100644 --- a/java/hello-world/src/main/java/org/acme/schooltimetabling/helperClasses/ScheduleConfig.java +++ b/java/hello-world/src/main/java/org/acme/schooltimetabling/helperClasses/ScheduleConfig.java @@ -27,6 +27,7 @@ public class ScheduleConfig { public boolean useApi; public String compressStart; public String compressEnd; + public String prescheduledFileName; //------------ values calculated ------------ private BitSet cpmrsInBs; private BitSet cmprsOutBs; @@ -147,4 +148,9 @@ public static BitSet getCompressOutMask() { if(HOLDER.scheduleConfig == null) throw new IllegalStateException("Configuration must be loaded"); return HOLDER.scheduleConfig.cmprsOutBs; } + + public static String getPrescheduledFileName(){ + if(HOLDER.scheduleConfig == null) throw new IllegalStateException("Configuration must be loaded"); + return HOLDER.scheduleConfig.prescheduledFileName; + } } diff --git a/java/hello-world/src/main/java/org/acme/schooltimetabling/solver/TimetableConstraintProvider.java b/java/hello-world/src/main/java/org/acme/schooltimetabling/solver/TimetableConstraintProvider.java index de9da819..ea39a7bb 100644 --- a/java/hello-world/src/main/java/org/acme/schooltimetabling/solver/TimetableConstraintProvider.java +++ b/java/hello-world/src/main/java/org/acme/schooltimetabling/solver/TimetableConstraintProvider.java @@ -4,10 +4,12 @@ import ai.timefold.solver.core.api.score.stream.*; import org.acme.schooltimetabling.constants.Constants; import org.acme.schooltimetabling.constants.Days; +import org.acme.schooltimetabling.constants.Preference; import org.acme.schooltimetabling.domain.lesson.Lesson; import org.acme.schooltimetabling.domain.Room; import org.acme.schooltimetabling.domain.Timeslot; import org.acme.schooltimetabling.domain.teacher.Teacher; +import org.acme.schooltimetabling.helperClasses.BitSetHelper; import org.acme.schooltimetabling.helperClasses.Generators.LessonGenerator; import org.acme.schooltimetabling.helperClasses.ScheduleConfig; import org.acme.schooltimetabling.solver.justifications.*; @@ -38,7 +40,10 @@ public Constraint[] defineConstraints(ConstraintFactory constraintFactory) { wrongRoomType(constraintFactory), // Medium Constraints - prefTime(constraintFactory) + prefTime(constraintFactory), + compressTeachTime(constraintFactory), + rewardPreferredHourGap(constraintFactory), + penalizeDislikedHourGap(constraintFactory) // Soft constraints )); @@ -343,61 +348,6 @@ Constraint studioLabAfterLec(ConstraintFactory constraintFactory){ .asConstraint("Studio lab right after lecture"); } -//THE COMMENTED CODE BELOW ARE OLD CONSTRAINTS FROM WHEN I WAS DISCONNECTING LABS AND LECTURE. MIGHT BE USEFUL LATER -// /** -// * Checks studio courses' lab/act portion occurs on one day with consecutive time -// * @param constraintFactory constraint factory -// * @return constraint penalizing studio courses not using studio time -// */ -// Constraint studioSpace(ConstraintFactory constraintFactory){ -// return constraintFactory.forEach(Lesson.class) -// .filter(lesson -> { -// //only check lab/act portion of the studio style split -// if(!Constants.STUDIO_STYLE_COURSES.contains(lesson.getCourseName()) || -// !lesson.isHasLabAct() || -// Constants.TESTING) return false; -// -// -// //"lecture" portion will be used for lab/act space -// //The timeslot should only have the lecture portion set. For a single day -// return !lesson.getTimeslot().isContinuous(); -// -// }) -// .penalize(HardMediumSoftScore.ONE_HARD) -// .asConstraint("Studio space must be consecutive on a single day"); -// } -// -// -// /** -// * For studio courses, it makes sure that at least one lecture occurs before the lab occurs -// * //TODO check with beard. Not sure if this is actually a thing but leaving it here just in case -// * @param constraintFactory -// * @return constraint penalizing studio courses that have their lab time before any lecture has taken place -// */ -// Constraint studioLabAfterLesson(ConstraintFactory constraintFactory){ -// //filter for studio only courses -// //just check the first bit of the lab vs lec bitset. if lab comes before penalty -// return constraintFactory.forEachUniquePair(Lesson.class, -// Joiners.equal(Lesson::getLinker), -// //skip non-studio classes -// Joiners.filtering((lesson, lesson2) -> lesson.isStudio() && lesson.isStudio()) -// ) -// .filter((lesson, lesson2) -> { -// //NOTE: one lesson will be the lec and the other one will be the lab/act -// -// //if first lesson is the lec, get the lecture bitset else use lesson2's bitset -// final BitSet lecBS = lesson.isHasLecture() ? lesson.getTimeslot().getLectureBitSet() : -// lesson2.getTimeslot().getLectureBitSet(); -// //if first lesson is the lab, get the lecture (yes the lecture) bitset else use lesson2's bitset -// final BitSet labBS = lesson.isHasLabAct() ? lesson.getTimeslot().getLectureBitSet() : -// lesson2.getTimeslot().getLectureBitSet(); -// -// return labBS.nextSetBit(0) <= lecBS.nextSetBit(0); -// }) -// .penalize(HardMediumSoftScore.ONE_HARD) -// .asConstraint("Studio Penalty: lab before all lecture"); -// } - //-------------------------------------- Medium Constraints -------------------------------------- Constraint prefTime(ConstraintFactory constraintFactory){ @@ -416,6 +366,170 @@ Constraint prefTime(ConstraintFactory constraintFactory){ .asConstraint("Reward 30min blocks in prof's pref times"); } + //cap at 8 hour days + private static final int MAX_DAY_LEN = 16; + private static final int MAX_GAP = 6; + private boolean dayHasLrgGap(Days day, BitSet time){ + final int NO_NEXT_BIT_SET = -1; + final int OFFSET; + if(day == Days.MONDAY) OFFSET = BitSetHelper.MONDAY_OFFSET; + else if(day == Days.TUESDAY) OFFSET = BitSetHelper.TUESDAY_OFFSET; + else if(day == Days.WEDNESDAY) OFFSET = BitSetHelper.WEDNESDAY_OFFSET; + else if(day == Days.THURSDAY) OFFSET = BitSetHelper.THURSDAY_OFFSET; + else OFFSET = BitSetHelper.FRIDAY_OFFSET; + BitSet bs = time.get(OFFSET, OFFSET + BitSetHelper.MAX_BITS_PER_DAY); + + //skip if empty + int start = bs.nextSetBit(0); + if(start == -1) return false; + int end = bs.nextClearBit(start) - 1; + for(int nxtStrt = bs.nextSetBit(end + 1); + nxtStrt != NO_NEXT_BIT_SET; + nxtStrt = bs.nextSetBit(end + 1)){ + + int localDiff = nxtStrt - end - 1; + //to large a gap between lessons on the given day + if(localDiff > MAX_GAP) return true; + //the end of this lec or lab. + end = bs.nextClearBit(nxtStrt) - 1; + } + + //check if too much time in a day + return end - start + 1 > MAX_DAY_LEN; + } + + /** + * This constraint penalizes a teacher if there is a gap between lessons more than {@link #MAX_GAP} amount of 30-minute + * blocks between the end of a lesson and the start of another lesson. It will also penalize a teacher that has + * a day longer than 8 hours; i.e. the time from when their first lesson starts to when their last lesson ends. + */ + Constraint compressTeachTime(ConstraintFactory constraintFactory){ + return constraintFactory.forEach(Lesson.class) + .groupBy(Lesson::getTeacherObj, toList()) + .filter((teacher, lessons) -> { + //constraint is not relevant to anyone teaching only one class + if(lessons.size() == 1) return false; + + //constraint is not relevant if all lessons don't share a day; + EnumSet daysTeaching = EnumSet.noneOf(Days.class); + boolean lsSharedDay = false; + BitSet scheduledTime = new BitSet(); + for(Lesson lesson: lessons){ + Timeslot ts = lesson.getTimeslot(); + //flag if we find any two lessons that share a day + if(Collections.disjoint(daysTeaching, ts.getLecDays())) lsSharedDay = true; + if(Collections.disjoint(daysTeaching, ts.getNonLecDays())) lsSharedDay = true; + //collect the days being taught + daysTeaching.addAll(ts.getLecDays()); + daysTeaching.addAll(ts.getNonLecDays()); + //accumulate time taught + scheduledTime.or(ts.getLectureBitSet()); + scheduledTime.or(ts.getLabActBitSet()); + } + + //if no lessons share days, then there is nothing to penalize + if(!lsSharedDay) return false; + + for(Days day: daysTeaching){ + //if any day has a large gap, penalize + if(dayHasLrgGap(day, scheduledTime)) return true; + } + + return false; + }) + .penalize(HardMediumSoftScore.ONE_MEDIUM) + .asConstraint("Penalize teacher schedules with large gaps or long days"); + } + + //------------------------ + + private int countHourGaps(Days day, BitSet time, List lessons, Set> used) { + // Determine the day's offset + final int OFFSET; + if(day == Days.MONDAY) OFFSET = BitSetHelper.MONDAY_OFFSET; + else if(day == Days.TUESDAY) OFFSET = BitSetHelper.TUESDAY_OFFSET; + else if(day == Days.WEDNESDAY) OFFSET = BitSetHelper.WEDNESDAY_OFFSET; + else if(day == Days.THURSDAY) OFFSET = BitSetHelper.THURSDAY_OFFSET; + else OFFSET = BitSetHelper.FRIDAY_OFFSET; + BitSet bs = time.get(OFFSET, OFFSET + BitSetHelper.MAX_BITS_PER_DAY); + + int gaps = 0; + + // Start scanning the day's scheduled lessons + int start = bs.nextSetBit(0); + + while (start != -1) { + + // Find the end of this lesson block + int end = bs.nextClearBit(start) - 1; + + // Look for the next lesson block + int nextStart = bs.nextSetBit(end + 1); + + if (nextStart != -1) { + int gap = nextStart - end - 1; // number of empty 30-min blocks + + if (gap == 2) { // exactly 1 hour + int first = -1; + int second = -1; + for(int i = 0; i < lessons.size(); i++){ + BitSet lsBs = lessons.get(i).getTimeslot().getAllTimesBitSet(); + if(lsBs.get(OFFSET + end)) first = i; + if(lsBs.get(OFFSET + nextStart)) second = i; + } + //need to check that they aren't the same or else the creation of inline set will error + if(first != second && used.add(Set.of(first, second))) gaps++; + } + } + + // Move to the next lesson block + start = nextStart; + } + + return gaps; + } + + private int countTeacherHourGaps(List lessons) { + BitSet scheduledTime = new BitSet(); + EnumSet daysTeaching = EnumSet.noneOf(Days.class); + Set> used = new HashSet<>(); + + for (Lesson lesson : lessons) { + Timeslot ts = lesson.getTimeslot(); + scheduledTime.or(ts.getAllTimesBitSet()); + daysTeaching.addAll(lesson.getTimeslot().getLecDays()); + daysTeaching.addAll(lesson.getTimeslot().getNonLecDays()); + } + + int totalGaps = 0; + for (Days day : daysTeaching) { + totalGaps += countHourGaps(day, scheduledTime, lessons, used); + } + + //divide to account for potentially multiple classes counted + return totalGaps; + } + + Constraint rewardPreferredHourGap(ConstraintFactory constraintFactory) { + return constraintFactory.forEach(Lesson.class) + .groupBy(Lesson::getTeacherObj, toList()) + .filter((teacher, lessons) -> + teacher.getGapPref() == Preference.AGREE) + .reward(HardMediumSoftScore.ONE_MEDIUM, + (teacher, lessons) -> countTeacherHourGaps(lessons)) + .asConstraint("Reward teachers who prefer 1-hour gaps"); + } + + Constraint penalizeDislikedHourGap(ConstraintFactory constraintFactory) { + return constraintFactory.forEach(Lesson.class) + .groupBy(Lesson::getTeacherObj, toList()) + .filter((teacher, lessons) -> + teacher.getGapPref() == Preference.DISAGREE) + .penalize(HardMediumSoftScore.ONE_MEDIUM, + (teacher, lessons) -> countTeacherHourGaps(lessons)) + .asConstraint("Penalize teachers who dislike 1-hour gaps"); + } + //-------------------------------------- Soft Constraints -------------------------------------- /*at least 50 percent of the time for scheduled Department courses should be outside Prime Time hours diff --git a/java/hello-world/src/main/resources/solverConfig.xml b/java/hello-world/src/main/resources/solverConfig.xml index e2dceff7..f69b9a83 100644 --- a/java/hello-world/src/main/resources/solverConfig.xml +++ b/java/hello-world/src/main/resources/solverConfig.xml @@ -36,13 +36,15 @@ + OR 0hard/*medium/*soft + 30 - 2hard/10medium/30soft + 3hard/10medium/30soft @@ -68,7 +70,7 @@ - 2hard/5medium/5soft + 3hard/5medium/5soft 10 diff --git a/java/hello-world/src/test/java/org/acme/schooltimetabling/TestClasses/TestLessons.java b/java/hello-world/src/test/java/org/acme/schooltimetabling/TestClasses/TestLessons.java index 716411f8..b7ea4c01 100644 --- a/java/hello-world/src/test/java/org/acme/schooltimetabling/TestClasses/TestLessons.java +++ b/java/hello-world/src/test/java/org/acme/schooltimetabling/TestClasses/TestLessons.java @@ -63,48 +63,4 @@ void checkScheduledClasses(){ })); } - //Note this test is currently not applicable using a different definition of a studio style course - @Disabled - @Test - @DisplayName("Check studio courses") - void checkStudio(){ - //set once the pair has been verified - Set linked = new HashSet<>(); - //holds lesson whose pair needs to be found - Map findPair = new HashMap<>(); - assertAll("Loop checking all potential studio style courses", - lessonList.stream() - .filter(lesson -> - Constants.STUDIO_STYLE_COURSES.contains(lesson.courseName.toLowerCase()) - ) - .map(lesson -> (Executable) () -> { - //checks for a specific lesson - assertAll("Checking studio course", - //checks that studio courses have a link - () -> assertNotNull(lesson.getLinker()), - //check if lesson's link has a pair already - () -> assertFalse(linked.contains(lesson.getLinker())), - //check for pair - () -> assertTrue(() -> { - Lesson prev = findPair.getOrDefault(lesson.getLinker(), null); - if(prev != null){ - //make sure the one lesson is lab/act and the other is the lecture - if(prev.hasLecture == lesson.hasLecture - || prev.hasLabAct == lesson.hasLabAct) return false; - else{ - //if pair has been validated add it to paired lesson verified - linked.add(lesson.getLinker()); - return true; - } - } - else{ - findPair.put(lesson.getLinker(), lesson); - return true; - } - }) - - ); - }).toList()); - } - } diff --git a/java/hello-world/src/test/java/org/acme/schooltimetabling/solver/TestConstraints.java b/java/hello-world/src/test/java/org/acme/schooltimetabling/solver/TestConstraints.java index c7dbb8f1..c4c310ad 100644 --- a/java/hello-world/src/test/java/org/acme/schooltimetabling/solver/TestConstraints.java +++ b/java/hello-world/src/test/java/org/acme/schooltimetabling/solver/TestConstraints.java @@ -2,6 +2,7 @@ import org.acme.schooltimetabling.constants.Constants; import org.acme.schooltimetabling.constants.Days; +import org.acme.schooltimetabling.constants.Preference; import org.acme.schooltimetabling.domain.lesson.Lesson; import org.acme.schooltimetabling.domain.Room; import org.acme.schooltimetabling.domain.Timeslot; @@ -13,10 +14,12 @@ import org.acme.schooltimetabling.helperClasses.ScheduleConfig; import org.glassfish.jaxb.runtime.v2.runtime.reflect.opt.Const; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import ai.timefold.solver.test.api.score.stream.ConstraintVerifier; +import java.sql.Time; import java.time.LocalTime; import java.time.format.DateTimeFormatter; import java.util.BitSet; @@ -476,6 +479,237 @@ void hardPrimeTimePenalty(){ .given(lessonOutPrimeTime, lessonInPrimeTime, lsLabOnly) .penalizesBy(1); } + + + @Test + @DisplayName("no Pen: teacher with small gaps") + void teacherNoLargeGaps(){ + EnumSet MWF = EnumSet.of(Days.MONDAY, Days.WEDNESDAY, Days.FRIDAY); + EnumSet TR = EnumSet.of(Days.TUESDAY, Days.THURSDAY); + + BitSet bs_mwf_8am_2blcks = BitSetHelper.timeSlotBitSet(LocalTime.parse("8:00AM", formatter), 2, MWF); + BitSet bs_mwf_9am_2blcks = BitSetHelper.timeSlotBitSet(LocalTime.parse("9:00AM", formatter), 2, MWF); + BitSet bs_mwf_1PM_2blcks = BitSetHelper.timeSlotBitSet(LocalTime.parse("1:00PM", formatter), 2, MWF); + BitSet bs_tr_830pm_3blcks = BitSetHelper.timeSlotBitSet(LocalTime.parse("8:30PM", formatter), 3, TR); + BitSet bs_mwf_3pm_2blcks = BitSetHelper.timeSlotBitSet(LocalTime.parse("3:00PM", formatter), 2, MWF); + + Timeslot ts_mwf_8am_2blcks_9am_2blcks = Timeslot.test_lecLabBitAndDays(1, bs_mwf_8am_2blcks, bs_mwf_9am_2blcks, MWF, MWF); + Timeslot ts_mwf_1pm_2blcks = Timeslot.test_lecLabBitAndDays(2, bs_mwf_1PM_2blcks, ConstraintTestHelper.EMPTY_BS, + ConstraintTestHelper.NO_DAYS, ConstraintTestHelper.NO_DAYS); + Timeslot ts_tr_830pm_3blcks = Timeslot.test_lecLabBitAndDays(3, ConstraintTestHelper.EMPTY_BS, bs_tr_830pm_3blcks, + ConstraintTestHelper.NO_DAYS, ConstraintTestHelper.NO_DAYS); + Timeslot ts_mwf_3pm_2blcks = + Timeslot.test_lecLabBitAndDays(4, bs_mwf_3pm_2blcks, ConstraintTestHelper.EMPTY_BS, + MWF, ConstraintTestHelper.NO_DAYS); + + Lesson ls1 = Lesson.test_buildLesson("1", 1, "", "", "", + "0-0-0", 1, ConstraintTestHelper.DUMMY_TEACHER , ts_mwf_8am_2blcks_9am_2blcks, + ConstraintTestHelper.DUMMY_ROOM); + Lesson ls2 = Lesson.test_buildLesson("2", 1, "", "", "", + "0-0-0", 1, ConstraintTestHelper.DUMMY_TEACHER , ts_mwf_1pm_2blcks, + ConstraintTestHelper.DUMMY_ROOM); + Lesson ls3 = Lesson.test_buildLesson("3", 1, "", "", "", + "0-0-0", 1, ConstraintTestHelper.DUMMY_TEACHER , ts_tr_830pm_3blcks, + ConstraintTestHelper.DUMMY_ROOM); + Lesson ls4 = Lesson.test_buildLesson("4", 1, "", "", "", + "0-0-0", 1, ConstraintTestHelper.DUMMY_TEACHER , ts_mwf_3pm_2blcks, + ConstraintTestHelper.DUMMY_ROOM); + + constraintVerifier.verifyThat(TimetableConstraintProvider::compressTeachTime) + .given(ls1, ls2, ls3, ls4) + .penalizesBy(0); + } + + + @Test + @DisplayName("Penalties: large gaps and long days") + void penalizeLargeGapsAndLongDays(){ + + EnumSet MWF = EnumSet.of(Days.MONDAY, Days.WEDNESDAY, Days.FRIDAY); + Teacher TEACHER1 = new Teacher( 1, "dummyInstructor", ConstraintTestHelper.EMPTY_BS, + ConstraintTestHelper.EMPTY_BS, ConstraintTestHelper.EMPTY_BS); + Teacher TEACHER2 = new Teacher( 1, "dummyInstructor", ConstraintTestHelper.EMPTY_BS, + ConstraintTestHelper.EMPTY_BS, ConstraintTestHelper.EMPTY_BS); + + + /* --------------------------- + Teacher 1: gap > 3 hours + --------------------------- */ + + BitSet t1_8am = BitSetHelper.timeSlotBitSet(LocalTime.parse("8:00AM", formatter), 2, MWF); + BitSet t1_1230pm = BitSetHelper.timeSlotBitSet(LocalTime.parse("12:30PM", formatter), 2, MWF); // 3.5 hr gap + + Timeslot ts_t1_a = Timeslot.test_lecLabBitAndDays(1, t1_8am, ConstraintTestHelper.EMPTY_BS, + MWF, ConstraintTestHelper.NO_DAYS); + + Timeslot ts_t1_b = Timeslot.test_lecLabBitAndDays(1, t1_1230pm, ConstraintTestHelper.EMPTY_BS, + MWF, ConstraintTestHelper.NO_DAYS); + + Lesson t1_l1 = Lesson.test_buildLesson("t1_l1", 1,"","","","0-0-0",1, + TEACHER1, ts_t1_a, ConstraintTestHelper.DUMMY_ROOM); + + Lesson t1_l2 = Lesson.test_buildLesson("t1_l2", 1,"","","","0-0-0",1, + TEACHER1, ts_t1_b, ConstraintTestHelper.DUMMY_ROOM); + + +/* --------------------------- + Teacher 2: day > 8 hours (8.5h) with NO >3hr gaps + --------------------------- */ + + BitSet t2_8am = BitSetHelper.timeSlotBitSet(LocalTime.parse("8:00AM", formatter), 2, MWF); + BitSet t2_12pm = BitSetHelper.timeSlotBitSet(LocalTime.parse("12:00PM", formatter), 2, MWF); + BitSet t2_430pm = BitSetHelper.timeSlotBitSet(LocalTime.parse("4:30PM", formatter), 1, MWF); + + Timeslot ts_t2_a = Timeslot.test_lecLabBitAndDays(1, t2_8am, ConstraintTestHelper.EMPTY_BS, + MWF, ConstraintTestHelper.NO_DAYS); + + Timeslot ts_t2_b = Timeslot.test_lecLabBitAndDays(1, t2_12pm, ConstraintTestHelper.EMPTY_BS, + MWF, ConstraintTestHelper.NO_DAYS); + + Timeslot ts_t2_c = Timeslot.test_lecLabBitAndDays(1, t2_430pm, ConstraintTestHelper.EMPTY_BS, + MWF, ConstraintTestHelper.NO_DAYS); + + Lesson t2_l1 = Lesson.test_buildLesson("t2_l1",1,"","","","0-0-0",1, + TEACHER2, ts_t2_a, ConstraintTestHelper.DUMMY_ROOM); + + Lesson t2_l2 = Lesson.test_buildLesson("t2_l2",1,"","","","0-0-0",1, + TEACHER2, ts_t2_b, ConstraintTestHelper.DUMMY_ROOM); + + Lesson t2_l3 = Lesson.test_buildLesson("t2_l3",1,"","","","0-0-0",1, + TEACHER2, ts_t2_c, ConstraintTestHelper.DUMMY_ROOM); + + + + /* --------------------------- + Teacher 5: multiple large gaps + still only one penalty + --------------------------- */ + + BitSet t5_8am = BitSetHelper.timeSlotBitSet(LocalTime.parse("8:00AM", formatter), 2, MWF); + BitSet t5_1230pm = BitSetHelper.timeSlotBitSet(LocalTime.parse("12:30PM", formatter), 2, MWF); + BitSet t5_6pm = BitSetHelper.timeSlotBitSet(LocalTime.parse("6:00PM", formatter), 2, MWF); + + Timeslot ts_t5_a = Timeslot.test_lecLabBitAndDays(1, t5_8am, ConstraintTestHelper.EMPTY_BS, + MWF, ConstraintTestHelper.NO_DAYS); + + Timeslot ts_t5_b = Timeslot.test_lecLabBitAndDays(1, t5_1230pm, ConstraintTestHelper.EMPTY_BS, + MWF, ConstraintTestHelper.NO_DAYS); + + Timeslot ts_t5_c = Timeslot.test_lecLabBitAndDays(1, t5_6pm, ConstraintTestHelper.EMPTY_BS, + MWF, ConstraintTestHelper.NO_DAYS); + + Lesson t5_l1 = Lesson.test_buildLesson("t5_l1",1,"","","","0-0-0",1, + ConstraintTestHelper.DUMMY_TEACHER, ts_t5_a, ConstraintTestHelper.DUMMY_ROOM); + + Lesson t5_l2 = Lesson.test_buildLesson("t5_l2",1,"","","","0-0-0",1, + ConstraintTestHelper.DUMMY_TEACHER, ts_t5_b, ConstraintTestHelper.DUMMY_ROOM); + + Lesson t5_l3 = Lesson.test_buildLesson("t5_l3",1,"","","","0-0-0",1, + ConstraintTestHelper.DUMMY_TEACHER, ts_t5_c, ConstraintTestHelper.DUMMY_ROOM); + + + constraintVerifier.verifyThat(TimetableConstraintProvider::compressTeachTime) + .given( + t1_l1, t1_l2, + t2_l1, t2_l2, t2_l3, + t5_l1, t5_l2, t5_l3 + ) + .penalizesBy(3); + } + + + @Test + @DisplayName("Reward: teacher prefers one-hour gaps") + void rewardPreferredHourGap() { + EnumSet MWF = EnumSet.of(Days.MONDAY, Days.WEDNESDAY, Days.FRIDAY); + Teacher teacher = new Teacher(1, "prefers gaps", ConstraintTestHelper.EMPTY_BS, + ConstraintTestHelper.EMPTY_BS, ConstraintTestHelper.EMPTY_BS, Preference.AGREE); + + BitSet early = BitSetHelper.timeSlotBitSet(LocalTime.parse("8:00AM", formatter), 2, MWF); + BitSet late = BitSetHelper.timeSlotBitSet(LocalTime.parse("10:00AM", formatter), 2, MWF); + + Timeslot tsEarly = Timeslot.test_lecLabBitAndDays(1, early, ConstraintTestHelper.EMPTY_BS, MWF, ConstraintTestHelper.NO_DAYS); + Timeslot tsLate = Timeslot.test_lecLabBitAndDays(2, ConstraintTestHelper.EMPTY_BS, late, ConstraintTestHelper.NO_DAYS, MWF); + + Lesson lesson1 = Lesson.test_buildLesson("gap1", 1, "", "", "", + "0-0-0", 1, teacher, tsEarly, ConstraintTestHelper.DUMMY_ROOM); + Lesson lesson2 = Lesson.test_buildLesson("gap2", 1, "", "", "", + "0-0-0", 1, teacher, tsLate, ConstraintTestHelper.DUMMY_ROOM); + + //------ no reward (hour gap between lec and lab) + EnumSet RF = EnumSet.of(Days.THURSDAY, Days.FRIDAY); + BitSet rfLecture = BitSetHelper.timeSlotBitSet(LocalTime.parse("3:00PM", formatter), 2, RF); + BitSet rfLab = BitSetHelper.timeSlotBitSet(LocalTime.parse("5:00PM", formatter), 2, RF); + Timeslot rfTimeslot = Timeslot.test_lecLabBitAndDays(6, rfLecture, rfLab, RF, RF); + Teacher singleCourseTeacher = new Teacher(4, "single slot", ConstraintTestHelper.EMPTY_BS, + ConstraintTestHelper.EMPTY_BS, ConstraintTestHelper.EMPTY_BS, Preference.AGREE); + Lesson singleCourse = Lesson.test_buildLesson("gap3", 1, "", "", "", + "0-0-0", 1, singleCourseTeacher, rfTimeslot, ConstraintTestHelper.DUMMY_ROOM); + + //------ no reward + Teacher disagreeTeacher = new Teacher(2, "hates gaps", ConstraintTestHelper.EMPTY_BS, + ConstraintTestHelper.EMPTY_BS, ConstraintTestHelper.EMPTY_BS, Preference.DISAGREE); + + Lesson disagreeLesson1 = Lesson.test_buildLesson("gapDisagree1", 1, "", "", "", + "0-0-0", 1, disagreeTeacher, tsEarly, ConstraintTestHelper.DUMMY_ROOM); + Lesson disagreeLesson2 = Lesson.test_buildLesson("gapDisagree2", 1, "", "", "", + "0-0-0", 1, disagreeTeacher, tsLate, ConstraintTestHelper.DUMMY_ROOM); + + constraintVerifier.verifyThat(TimetableConstraintProvider::rewardPreferredHourGap) + .given(lesson1, lesson2, + singleCourse, + disagreeLesson1, disagreeLesson2) + .rewardsWith(1); + } + + @Test + @DisplayName("Penalty: teacher dislikes one-hour gaps") + void penalizeDislikedHourGap() { + EnumSet TR = EnumSet.of(Days.TUESDAY, Days.THURSDAY); + EnumSet T = EnumSet.of(Days.TUESDAY); + Teacher teacher = new Teacher(2, "hates gaps", ConstraintTestHelper.EMPTY_BS, + ConstraintTestHelper.EMPTY_BS, ConstraintTestHelper.EMPTY_BS, Preference.DISAGREE); + + BitSet blockA = BitSetHelper.timeSlotBitSet(LocalTime.parse("9:00AM", formatter), 2, TR); + BitSet blockB = BitSetHelper.timeSlotBitSet(LocalTime.parse("11:00AM", formatter), 2, TR); + BitSet blockC = BitSetHelper.timeSlotBitSet(LocalTime.parse("1:00PM", formatter), 2, T); + + Timeslot tsA = Timeslot.test_lecLabBitAndDays(3, blockA, ConstraintTestHelper.EMPTY_BS, TR, ConstraintTestHelper.NO_DAYS); + Timeslot tsB = Timeslot.test_lecLabBitAndDays(4, blockB, ConstraintTestHelper.EMPTY_BS, TR, ConstraintTestHelper.NO_DAYS); + Timeslot tsC = Timeslot.test_lecLabBitAndDays(5, ConstraintTestHelper.EMPTY_BS, blockC, ConstraintTestHelper.NO_DAYS, T); + + Lesson lessonA = Lesson.test_buildLesson("penalty1", 1, "", "", "", + "0-0-0", 1, teacher, tsA, ConstraintTestHelper.DUMMY_ROOM); + Lesson lessonB = Lesson.test_buildLesson("penalty2", 1, "", "", "", + "0-0-0", 1, teacher, tsB, ConstraintTestHelper.DUMMY_ROOM); + Lesson lessonC = Lesson.test_buildLesson("penalty2", 1, "", "", "", + "0-0-0", 1, teacher, tsC, ConstraintTestHelper.DUMMY_ROOM); + + //------ No penalty + EnumSet RF = EnumSet.of(Days.THURSDAY, Days.FRIDAY); + BitSet lectureRf = BitSetHelper.timeSlotBitSet(LocalTime.parse("3:00PM", formatter), 2, RF); + BitSet labRf = BitSetHelper.timeSlotBitSet(LocalTime.parse("5:00PM", formatter), 2, RF); + Timeslot rfTimeslot = Timeslot.test_lecLabBitAndDays(6, lectureRf, labRf, RF, RF); + Teacher singleCourseTeacher = new Teacher(4, "single course", ConstraintTestHelper.EMPTY_BS, + ConstraintTestHelper.EMPTY_BS, ConstraintTestHelper.EMPTY_BS, Preference.DISAGREE); + Lesson singleCourse = Lesson.test_buildLesson("singleCourse", 1, "", "", "", + "0-0-0", 1, singleCourseTeacher, rfTimeslot, ConstraintTestHelper.DUMMY_ROOM); + //-------- No penalty + Teacher gapPrefTeacher = new Teacher(3, "likes gaps", ConstraintTestHelper.EMPTY_BS, + ConstraintTestHelper.EMPTY_BS, ConstraintTestHelper.EMPTY_BS, Preference.AGREE); + + Lesson likedGap1 = Lesson.test_buildLesson("agree1", 1, "", "", "", + "0-0-0", 1, gapPrefTeacher, tsA, ConstraintTestHelper.DUMMY_ROOM); + Lesson likedGap2 = Lesson.test_buildLesson("agree2", 1, "", "", "", + "0-0-0", 1, gapPrefTeacher, tsB, ConstraintTestHelper.DUMMY_ROOM); + + + constraintVerifier.verifyThat(TimetableConstraintProvider::penalizeDislikedHourGap) + .given(lessonA, lessonB, lessonC, + singleCourse, + likedGap1, likedGap2) + .penalizesBy(2); + } }