diff --git a/.idea/misc.xml b/.idea/misc.xml index 7731818..6abc40f 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -9,7 +9,7 @@ - + \ No newline at end of file diff --git a/src/main/java/org/example/se302/controller/ScheduleCalendarController.java b/src/main/java/org/example/se302/controller/ScheduleCalendarController.java index 550cc86..2299dc5 100644 --- a/src/main/java/org/example/se302/controller/ScheduleCalendarController.java +++ b/src/main/java/org/example/se302/controller/ScheduleCalendarController.java @@ -105,8 +105,8 @@ public class ScheduleCalendarController { // Set default date to next week startDatePicker.setValue(LocalDate.now().plusDays(7)); - // Set default checkbox - allow back-to-back for easier scheduling - allowBackToBackCheckBox.setSelected(true); + // Set default checkbox - student-friendly (no back-to-back exams) + allowBackToBackCheckBox.setSelected(false); // Add listeners to update summary numDaysSpinner.valueProperty().addListener((obs, oldVal, newVal) -> updateSummary()); @@ -135,16 +135,13 @@ public class ScheduleCalendarController { } private void initializeComboBoxes() { - // Optimization strategies + // Optimization strategies - consolidated to 3 meaningful options List strategies = Arrays.asList( - "Default (Balanced)", + "Student Friendly (Default)", "Minimize Days", - "Balanced Distribution", - "Minimize Classrooms", - "Balance Classrooms", - "Student Friendly"); + "Minimize Classrooms"); strategyComboBox.setItems(FXCollections.observableArrayList(strategies)); - strategyComboBox.getSelectionModel().selectFirst(); + strategyComboBox.getSelectionModel().selectFirst(); // Default: Student Friendly // Start times (8:00 - 14:00) List times = Arrays.asList( @@ -236,22 +233,17 @@ public class ScheduleCalendarController { config.setDayStartTime(LocalTime.of(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]))); } - // Set optimization strategy + // Set optimization strategy (consolidated to 3 options) String strategyStr = strategyComboBox.getValue(); - ScheduleConfiguration.OptimizationStrategy strategy = ScheduleConfiguration.OptimizationStrategy.DEFAULT; + ScheduleConfiguration.OptimizationStrategy strategy = ScheduleConfiguration.OptimizationStrategy.STUDENT_FRIENDLY; if (strategyStr != null) { if (strategyStr.contains("Minimize Days")) { strategy = ScheduleConfiguration.OptimizationStrategy.MINIMIZE_DAYS; - } else if (strategyStr.contains("Balanced Distribution")) { - strategy = ScheduleConfiguration.OptimizationStrategy.BALANCED_DISTRIBUTION; } else if (strategyStr.contains("Minimize Classrooms")) { strategy = ScheduleConfiguration.OptimizationStrategy.MINIMIZE_CLASSROOMS; - } else if (strategyStr.contains("Balance Classrooms")) { - strategy = ScheduleConfiguration.OptimizationStrategy.BALANCE_CLASSROOMS; - } else if (strategyStr.contains("Student Friendly")) { - strategy = ScheduleConfiguration.OptimizationStrategy.STUDENT_FRIENDLY; } + // STUDENT_FRIENDLY is default, no explicit check needed } config.setOptimizationStrategy(strategy); diff --git a/src/main/java/org/example/se302/model/ScheduleConfiguration.java b/src/main/java/org/example/se302/model/ScheduleConfiguration.java index 0110dac..bc235cd 100644 --- a/src/main/java/org/example/se302/model/ScheduleConfiguration.java +++ b/src/main/java/org/example/se302/model/ScheduleConfiguration.java @@ -26,24 +26,36 @@ public class ScheduleConfiguration { /** * Optimization strategies for the scheduling algorithm. + * + *

Active Strategies:

+ *
    + *
  • STUDENT_FRIENDLY - Minimize gaps, enforce constraints (DEFAULT)
  • + *
  • MINIMIZE_DAYS - Pack exams into fewest days
  • + *
  • MINIMIZE_CLASSROOMS - Use fewest classrooms
  • + *
+ * + *

Deprecated strategies are maintained for backward compatibility only.

*/ public enum OptimizationStrategy { - /** Minimize total number of days used - pack exams into fewest days */ + /** Pack exams into fewest days possible */ MINIMIZE_DAYS, - /** Spread exams evenly across days - balanced distribution */ - BALANCED_DISTRIBUTION, - - /** Minimize number of classrooms used - reuse same classrooms */ + /** Use fewest classrooms possible */ MINIMIZE_CLASSROOMS, - /** Balance classroom usage across days - even distribution */ - BALANCE_CLASSROOMS, - - /** Minimize consecutive exams for students (bonus strategy) */ + /** Minimize gaps, enforce student-friendly constraints (DEFAULT) */ STUDENT_FRIENDLY, - /** Default balanced approach */ + /** @deprecated Use STUDENT_FRIENDLY instead. Balanced distribution conflicts with minimize days. */ + @Deprecated + BALANCED_DISTRIBUTION, + + /** @deprecated Use MINIMIZE_CLASSROOMS instead. Balance classrooms conflicts with minimize classrooms. */ + @Deprecated + BALANCE_CLASSROOMS, + + /** @deprecated Use STUDENT_FRIENDLY instead. This is now the default behavior. */ + @Deprecated DEFAULT } @@ -76,9 +88,9 @@ public class ScheduleConfiguration { this.slotDurationMinutes = 120; // 2 hours this.dayStartTime = LocalTime.of(9, 0); // 09:00 this.breakBetweenSlotsMinutes = 30; // 30 minute break - this.optimizationStrategy = OptimizationStrategy.DEFAULT; - this.allowBackToBackExams = true; - this.maxExamsPerDay = 0; // 0 = no limit + this.optimizationStrategy = OptimizationStrategy.STUDENT_FRIENDLY; // Student-friendly by default + this.allowBackToBackExams = false; // Enforce no consecutive exams by default + this.maxExamsPerDay = 2; // Enforce max 2 exams per day by default this.timeoutMs = 60000; // 60 seconds this.useHeuristics = true; } diff --git a/src/main/java/org/example/se302/service/ScheduleGeneratorService.java b/src/main/java/org/example/se302/service/ScheduleGeneratorService.java index c36df21..e5fba48 100644 --- a/src/main/java/org/example/se302/service/ScheduleGeneratorService.java +++ b/src/main/java/org/example/se302/service/ScheduleGeneratorService.java @@ -25,6 +25,7 @@ public class ScheduleGeneratorService { /** * Generate an exam schedule based on configuration. + * Uses multi-restart optimization to find the best schedule among multiple attempts. */ public ScheduleResult generateSchedule(ScheduleConfiguration config) { cancelled.set(false); @@ -38,28 +39,71 @@ public class ScheduleGeneratorService { return ScheduleResult.failure("No classrooms available"); } - // Create schedule state - ScheduleState scheduleState = initializeScheduleState(config); - // Get courses ordered by MRV heuristic List coursesToSchedule = getCoursesOrderedByMRV(); - // Report progress - updateProgress(0, coursesToSchedule.size(), "Starting schedule generation..."); + // Multi-restart optimization: try multiple schedules and pick the best + int numRestarts = 5; // Generate 5 different schedules + ScheduleState bestSchedule = null; + double bestScore = Double.MAX_VALUE; + int successfulAttempts = 0; - // Start backtracking - boolean success = backtrack(scheduleState, coursesToSchedule, 0, config); + updateProgress(0, numRestarts, "Generating schedules (multi-restart optimization)..."); + + for (int attempt = 0; attempt < numRestarts; attempt++) { + if (cancelled.get()) { + return ScheduleResult.cancelled(); + } + + // Create fresh schedule state for this attempt + ScheduleState scheduleState = initializeScheduleState(config); + + updateProgress(attempt, numRestarts, + String.format("Attempt %d/%d: Starting backtracking...", attempt + 1, numRestarts)); + + // Randomize search order slightly for diversity + List shuffledCourses = new ArrayList<>(coursesToSchedule); + if (attempt > 0) { + // Keep MRV heuristic but add small random perturbation + shuffledCourses = perturbCourseOrder(shuffledCourses, attempt); + } + + // Run backtracking + boolean success = backtrack(scheduleState, shuffledCourses, 0, config); + + if (success) { + successfulAttempts++; + + // Evaluate this schedule using objective function + double score = ScheduleObjective.evaluateSchedule(scheduleState, config, dataManager); + + updateProgress(attempt + 1, numRestarts, + String.format("Attempt %d/%d: Found schedule (score: %.1f, best: %.1f)", + attempt + 1, numRestarts, score, bestScore)); + + if (score < bestScore) { + bestScore = score; + bestSchedule = scheduleState.copy(); + } + } else { + updateProgress(attempt + 1, numRestarts, + String.format("Attempt %d/%d: No valid schedule", attempt + 1, numRestarts)); + } + } if (cancelled.get()) { return ScheduleResult.cancelled(); } - if (success) { - updateProgress(coursesToSchedule.size(), coursesToSchedule.size(), "Schedule generated successfully!"); - return ScheduleResult.success(scheduleState); + if (bestSchedule != null) { + updateProgress(numRestarts, numRestarts, + String.format("Complete! Best schedule: score %.1f (%d/%d attempts succeeded)", + bestScore, successfulAttempts, numRestarts)); + return ScheduleResult.success(bestSchedule); } else { - return ScheduleResult - .failure("No valid schedule found. Try increasing days/slots or relaxing constraints."); + return ScheduleResult.failure( + "No valid schedule found in " + numRestarts + " attempts. " + + "Try increasing days/slots or relaxing constraints."); } } @@ -69,9 +113,15 @@ public class ScheduleGeneratorService { private ScheduleState initializeScheduleState(ScheduleConfiguration config) { ScheduleState state = new ScheduleState(); + // CRITICAL: Set configuration so ScheduleState knows about days/slots + state.setConfiguration(config); + // Set available classrooms state.setAvailableClassrooms(new ArrayList<>(dataManager.getClassrooms())); + // Set available time slots based on configuration + state.setAvailableTimeSlots(config.generateTimeSlots()); + // Initialize exam assignments for all courses (unassigned) for (Course course : dataManager.getCourses()) { ExamAssignment assignment = new ExamAssignment(course.getCourseCode()); @@ -162,6 +212,29 @@ public class ScheduleGeneratorService { return courses; } + /** + * Add small random perturbation to course order while preserving MRV bias. + * This creates diverse schedules for multi-restart optimization. + * + * @param courses Original course list + * @param seed Seed for deterministic randomization + * @return Perturbed course list + */ + private List perturbCourseOrder(List courses, int seed) { + Random random = new Random(seed * 1000L); // Deterministic seed for reproducibility + List perturbed = new ArrayList<>(courses); + + // Swap 2-3 random pairs (limited perturbation maintains MRV benefits) + int swaps = 2 + random.nextInt(2); + for (int i = 0; i < swaps && perturbed.size() > 1; i++) { + int idx1 = random.nextInt(perturbed.size()); + int idx2 = random.nextInt(perturbed.size()); + Collections.swap(perturbed, idx1, idx2); + } + + return perturbed; + } + /** * Get time slots ordered by optimization strategy. */ @@ -175,24 +248,30 @@ public class ScheduleGeneratorService { } } - // Order based on strategy + // Order based on strategy (deprecated strategies handled in ScheduleObjective) switch (config.getOptimizationStrategy()) { case MINIMIZE_DAYS: // Already in order (day 0 slot 0, day 0 slot 1, ... day 1 slot 0, ...) // This fills earlier days first break; - case BALANCED_DISTRIBUTION: - // Round-robin across days: day 0 slot 0, day 1 slot 0, day 2 slot 0, ... day 0 - // slot 1, ... - timeSlots.sort(Comparator.comparingInt((DaySlotPair p) -> p.slot) - .thenComparingInt(p -> p.day)); - break; - case STUDENT_FRIENDLY: - // Try to space out exams - prefer later slots on same day to avoid consecutive - // (This is a simple heuristic - more sophisticated would track student - // conflicts) + // Fill each day completely before moving to next day + // Within each day, prefer middle slots (avoid early morning) + // Priority: slots 1 and 2 (middle of day) over slots 0 (early) and 3 (late) + timeSlots.sort((p1, p2) -> { + // Primary: sort by day (fill Day 0 first, then Day 1, etc.) + int dayCompare = Integer.compare(p1.day, p2.day); + if (dayCompare != 0) + return dayCompare; + + // Secondary: within same day, prefer middle slots + // Slot priority order: 1 (best), 2 (good), 0 (early morning), 3 (late afternoon) + int[] slotPriority = { 2, 0, 1, 3 }; // Maps slot index to priority (lower is better) + int priority1 = p1.slot < slotPriority.length ? slotPriority[p1.slot] : p1.slot; + int priority2 = p2.slot < slotPriority.length ? slotPriority[p2.slot] : p2.slot; + return Integer.compare(priority1, priority2); + }); break; default: @@ -237,7 +316,7 @@ public class ScheduleGeneratorService { } } - // Order based on strategy + // Order based on strategy (deprecated strategies handled in ScheduleObjective) switch (config.getOptimizationStrategy()) { case MINIMIZE_CLASSROOMS: // Prefer classrooms that are already in use (reuse same classrooms) @@ -249,19 +328,26 @@ public class ScheduleGeneratorService { }); break; - case BALANCE_CLASSROOMS: - // Prefer classrooms that are least used + default: + // DEFAULT: Use round-robin to force distribution across classrooms + // Always prefer least-used classrooms to ensure multiple classrooms get used suitable.sort((c1, c2) -> { int usage1 = getClassroomUsageCount(c1.getClassroomId(), scheduleState); int usage2 = getClassroomUsageCount(c2.getClassroomId(), scheduleState); - // Sort ascending (least used first) - return Integer.compare(usage1, usage2); - }); - break; - default: - // DEFAULT or others: prefer smaller classrooms that fit (efficient space usage) - suitable.sort(Comparator.comparingInt(Classroom::getCapacity)); + // Primary: prefer least-used classrooms + int usageCompare = Integer.compare(usage1, usage2); + if (usageCompare != 0) + return usageCompare; + + // Tiebreaker: prefer smaller capacity (efficient space usage) + int capacityCompare = Integer.compare(c1.getCapacity(), c2.getCapacity()); + if (capacityCompare != 0) + return capacityCompare; + + // Final tiebreaker: classroom ID (deterministic) + return c1.getClassroomId().compareTo(c2.getClassroomId()); + }); break; } diff --git a/src/main/java/org/example/se302/service/ScheduleObjective.java b/src/main/java/org/example/se302/service/ScheduleObjective.java new file mode 100644 index 0000000..cba9ed0 --- /dev/null +++ b/src/main/java/org/example/se302/service/ScheduleObjective.java @@ -0,0 +1,267 @@ +package org.example.se302.service; + +import org.example.se302.model.*; + +import java.util.*; + +/** + * Calculates quality scores for exam schedules to enable optimization. + * Lower score = better schedule. + * + * Each optimization strategy has its own scoring function that quantifies + * how well a schedule meets the strategy's goals. + */ +public class ScheduleObjective { + + /** + * Evaluates a schedule based on the selected optimization strategy. + * + * @param state The schedule state to evaluate + * @param config The configuration containing the optimization strategy + * @param dataManager Data manager for accessing student and course data + * @return Quality score (lower is better) + */ + public static double evaluateSchedule( + ScheduleState state, + ScheduleConfiguration config, + DataManager dataManager) { + + // Normalize strategy in case deprecated ones are used + ScheduleConfiguration.OptimizationStrategy strategy = config.getOptimizationStrategy(); + + // Map deprecated strategies to their replacements + switch (strategy) { + case BALANCED_DISTRIBUTION: + case DEFAULT: + strategy = ScheduleConfiguration.OptimizationStrategy.STUDENT_FRIENDLY; + break; + case BALANCE_CLASSROOMS: + strategy = ScheduleConfiguration.OptimizationStrategy.MINIMIZE_CLASSROOMS; + break; + default: + break; + } + + // Calculate score based on strategy + switch (strategy) { + case MINIMIZE_DAYS: + return scoreMinimizeDays(state, config); + + case MINIMIZE_CLASSROOMS: + return scoreMinimizeClassrooms(state); + + case STUDENT_FRIENDLY: + return scoreStudentFriendly(state, config, dataManager); + + default: + return scoreStudentFriendly(state, config, dataManager); + } + } + + /** + * Score for MINIMIZE_DAYS strategy. + * Goal: Pack exams into as few days as possible. + * + * @param state The schedule state + * @param config The configuration + * @return Score (lower = fewer days used) + */ + private static double scoreMinimizeDays(ScheduleState state, ScheduleConfiguration config) { + // Count number of unique days actually used + Set usedDays = new HashSet<>(); + for (ExamAssignment assignment : state.getAssignments().values()) { + if (assignment.isAssigned()) { + usedDays.add(assignment.getDay()); + } + } + + // Primary objective: minimize number of days + // Penalty: 1000 points per day used + double score = usedDays.size() * 1000.0; + + // Secondary objective: prefer earlier days (Day 0 better than Day 4) + // This is a tiebreaker when two schedules use the same number of days + double avgDay = state.getAssignedCoursesList().stream() + .mapToInt(ExamAssignment::getDay) + .average() + .orElse(0.0); + score += avgDay * 10; + + // Tertiary objective: within each day, fill earlier slots first + double avgSlot = state.getAssignedCoursesList().stream() + .mapToInt(ExamAssignment::getTimeSlotIndex) + .average() + .orElse(0.0); + score += avgSlot * 1; + + return score; + } + + /** + * Score for MINIMIZE_CLASSROOMS strategy. + * Goal: Use as few different classrooms as possible. + * + * @param state The schedule state + * @return Score (lower = fewer classrooms used) + */ + private static double scoreMinimizeClassrooms(ScheduleState state) { + // Count unique classrooms used + Set usedClassrooms = new HashSet<>(); + for (ExamAssignment assignment : state.getAssignments().values()) { + if (assignment.isAssigned() && assignment.getClassroomId() != null) { + usedClassrooms.add(assignment.getClassroomId()); + } + } + + // Primary objective: minimize number of classrooms + // Penalty: 1000 points per classroom used + double score = usedClassrooms.size() * 1000.0; + + // Secondary objective: prefer classrooms with lower IDs + // (e.g., Classroom_01 better than Classroom_10) + // This is a tiebreaker when two schedules use the same number of classrooms + for (ExamAssignment assignment : state.getAssignments().values()) { + if (assignment.isAssigned() && assignment.getClassroomId() != null) { + String classroomId = assignment.getClassroomId(); + // Extract numeric part (e.g., "Classroom_01" -> 1) + try { + int num = Integer.parseInt(classroomId.replaceAll("[^0-9]", "")); + score += num * 0.1; // Small penalty for higher-numbered classrooms + } catch (NumberFormatException e) { + // If classroom ID doesn't contain number, ignore + } + } + } + + return score; + } + + /** + * Score for STUDENT_FRIENDLY strategy. + * Goal: Minimize gaps in student schedules and create a comfortable exam experience. + * + * @param state The schedule state + * @param config The configuration + * @param dataManager Data manager for accessing student data + * @return Score (lower = more student-friendly) + */ + private static double scoreStudentFriendly( + ScheduleState state, + ScheduleConfiguration config, + DataManager dataManager) { + + double score = 0.0; + + // Calculate total gaps in student schedules + for (Student student : dataManager.getStudents()) { + List studentExams = getStudentExams(student, state, dataManager); + + if (studentExams.isEmpty()) { + continue; + } + + // Sort exams by day, then by time slot + studentExams.sort(Comparator + .comparingInt(ExamAssignment::getDay) + .thenComparingInt(ExamAssignment::getTimeSlotIndex)); + + // Calculate gaps (empty slots between exams on the same day) + for (int i = 1; i < studentExams.size(); i++) { + ExamAssignment prev = studentExams.get(i - 1); + ExamAssignment curr = studentExams.get(i); + + if (prev.getDay() == curr.getDay()) { + int gap = curr.getTimeSlotIndex() - prev.getTimeSlotIndex() - 1; + if (gap > 0) { + // Penalty: 10 points per gap slot + // Example: Exam at slot 0 and slot 3 = 2 gap slots = 20 points penalty + score += gap * 10.0; + } + } + } + + // Penalty for early-morning exams (first slot of the day) + // Students prefer later exam times + for (ExamAssignment exam : studentExams) { + if (exam.getTimeSlotIndex() == 0) { + score += 5.0; // Small penalty for first slot (typically 8-9am) + } + } + + // Penalty for late-afternoon exams (last slot of the day) + int lastSlot = config.getSlotsPerDay() - 1; + for (ExamAssignment exam : studentExams) { + if (exam.getTimeSlotIndex() == lastSlot) { + score += 3.0; // Smaller penalty for last slot + } + } + + // Bonus for clustered exam days (exams on consecutive days are harder) + // Prefer spreading exams across non-consecutive days when possible + Set examDays = new HashSet<>(); + for (ExamAssignment exam : studentExams) { + examDays.add(exam.getDay()); + } + List sortedDays = new ArrayList<>(examDays); + Collections.sort(sortedDays); + + for (int i = 1; i < sortedDays.size(); i++) { + if (sortedDays.get(i) - sortedDays.get(i - 1) == 1) { + // Consecutive days + score += 2.0; // Small penalty for exams on consecutive days + } + } + } + + // Secondary objective: balance classroom usage + // Prefer schedules that distribute exams across classrooms + Map classroomUsage = new HashMap<>(); + for (ExamAssignment assignment : state.getAssignments().values()) { + if (assignment.isAssigned()) { + classroomUsage.merge(assignment.getClassroomId(), 1, Integer::sum); + } + } + + // Calculate standard deviation of classroom usage + if (!classroomUsage.isEmpty()) { + double avgUsage = classroomUsage.values().stream() + .mapToInt(Integer::intValue) + .average() + .orElse(0.0); + + double variance = classroomUsage.values().stream() + .mapToDouble(usage -> Math.pow(usage - avgUsage, 2)) + .average() + .orElse(0.0); + + double stdDev = Math.sqrt(variance); + // Penalty for unbalanced classroom usage + score += stdDev * 2.0; + } + + return score; + } + + /** + * Gets all exam assignments for a specific student. + * + * @param student The student + * @param state The schedule state + * @param dataManager Data manager for course lookup + * @return List of exam assignments for this student + */ + private static List getStudentExams( + Student student, + ScheduleState state, + DataManager dataManager) { + + List exams = new ArrayList<>(); + for (String courseCode : student.getEnrolledCourses()) { + ExamAssignment assignment = state.getAssignment(courseCode); + if (assignment != null && assignment.isAssigned()) { + exams.add(assignment); + } + } + return exams; + } +}