Static Classroum bug fixed. Now codes can assign different classroum on scheduling.

This commit is contained in:
sabazadam
2025-12-18 19:12:51 +03:00
parent fcbd05edec
commit 56e2bf509b
5 changed files with 421 additions and 64 deletions

2
.idea/misc.xml generated
View File

@@ -9,7 +9,7 @@
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_23" default="true" project-jdk-name="ms-21" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="ms-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

View File

@@ -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<String> 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<String> 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);

View File

@@ -26,24 +26,36 @@ public class ScheduleConfiguration {
/**
* Optimization strategies for the scheduling algorithm.
*
* <p>Active Strategies:</p>
* <ul>
* <li>STUDENT_FRIENDLY - Minimize gaps, enforce constraints (DEFAULT)</li>
* <li>MINIMIZE_DAYS - Pack exams into fewest days</li>
* <li>MINIMIZE_CLASSROOMS - Use fewest classrooms</li>
* </ul>
*
* <p>Deprecated strategies are maintained for backward compatibility only.</p>
*/
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;
}

View File

@@ -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<Course> 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<Course> 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<Course> perturbCourseOrder(List<Course> courses, int seed) {
Random random = new Random(seed * 1000L); // Deterministic seed for reproducibility
List<Course> 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;
}

View File

@@ -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<Integer> 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<String> 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<ExamAssignment> 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<Integer> examDays = new HashSet<>();
for (ExamAssignment exam : studentExams) {
examDays.add(exam.getDay());
}
List<Integer> 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<String, Integer> 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<ExamAssignment> getStudentExams(
Student student,
ScheduleState state,
DataManager dataManager) {
List<ExamAssignment> exams = new ArrayList<>();
for (String courseCode : student.getEnrolledCourses()) {
ExamAssignment assignment = state.getAssignment(courseCode);
if (assignment != null && assignment.isAssigned()) {
exams.add(assignment);
}
}
return exams;
}
}