I am learning to work with optaplanner. I need to spread exams of one student. The less time between two exams of one student, the more penalty I give.
I need the Integer List ExamIds of my Student class because there are all the exams for that one student.
Then I need to check all these Exams planned Timeslots with eachother to give them more time between.
What i tried is following code:
`
Constraint spaceBetweenExams(ConstraintFactory constraintFactory) {
return constraintFactory.forEach(Student.class)
.join(Exam.class)
.join(Exam.class)
.filter((student, exam1, exam2) ->{
if(student.getExamIds().contains(exam1.getID()) && student.getExamIds().contains(exam2.getID())){
if(exam1.getID() < exam2.getID()){
int timeDifference = Math.abs(exam1.getTimeslot().getID() - exam2.getTimeslot().getID());
if (timeDifference == 1) {
penalty = 16;
} else if (timeDifference == 2) {
penalty = 8;
} else if (timeDifference == 3) {
penalty = 4;
} else if (timeDifference == 4) {
penalty = 2;
} else if (timeDifference == 5) {
penalty = 1;
}
return true;
}
}
return false;
})
.penalize("Max time between exams", HardSoftScore.ofSoft(penalty));
`
The result I get is 24645 soft penalties but optaplanner doesn't even try to fix them. I think that the way I check the exams in the code above is not fully correct.
This is my constraint class :
public class ExamTableConstraintProvider implements ConstraintProvider {
int penalty = 0;
List<Integer> studentExamIds;
@Override
public Constraint[] defineConstraints(ConstraintFactory constraintFactory) {
return new Constraint[]{
// Hard constraints
twoExamForStudentConflict(constraintFactory),
// Soft constraints
spaceBetweenExams(constraintFactory)
};
}
private Constraint twoExamForStudentConflict(ConstraintFactory constraintFactory) {
return constraintFactory.forEach(Exam.class)
.join(Exam.class,
Joiners.equal(Exam::getTimeslot),
Joiners.lessThan(Exam::getID))
.filter((exam1, exam2) -> {
List<Integer> result = new ArrayList<>(exam1.getSID());
result.retainAll(exam2.getSID());
return result.size() > 0;
})
.penalize("Student conflict", HardSoftScore.ONE_HARD);
}
Constraint spaceBetweenExams(ConstraintFactory constraintFactory) {
return constraintFactory.forEach(Student.class)
.join(Exam.class)
.join(Exam.class)
.filter((student, exam1, exam2) ->{
if(student.getExamIds().contains(exam1.getID()) && student.getExamIds().contains(exam2.getID())){
if(exam1.getID() < exam2.getID()){
int timeDifference = Math.abs(exam1.getTimeslot().getID() - exam2.getTimeslot().getID());
if (timeDifference == 1) {
penalty = 16;
} else if (timeDifference == 2) {
penalty = 8;
} else if (timeDifference == 3) {
penalty = 4;
} else if (timeDifference == 4) {
penalty = 2;
} else if (timeDifference == 5) {
penalty = 1;
}
return true;
}
}
return false;
})
.penalize("Max time between exams", HardSoftScore.ofSoft(penalty));
}
/*Constraint spaceBetweenExams(ConstraintFactory constraintFactory) {
penalty = 0;
return constraintFactory.forEach(Student.class)
.join(Exam.class,
equal(Student::getExamIds, Exam::getID),
filtering((student, exam1) -> exam1.getTimeslot() != null))
.join(Exam.class,
equal((student, exam1) -> student.getExamIds(), Exam::getID),
equal((student, exam1) -> exam1.getTimeslot(), Exam::getTimeslot),
filtering((student, exam1, exam2) -> {
int timeDifference = getPeriodBetweenExams(exam1, exam2);
if (timeDifference == 1) {
penalty = 16;
} else if (timeDifference == 2) {
penalty = 8;
} else if (timeDifference == 3) {
penalty = 4;
} else if (timeDifference == 4) {
penalty = 2;
} else if (timeDifference == 5) {
penalty = 1;
}
if(penalty == 0){
return false;
}
return true;
}))
.penalize("Max time between exams", HardSoftScore.ONE_SOFT);
}*/
}
And this is the class where I start the program:
public class ExamTableApp {
private static final Logger LOGGER = LoggerFactory.getLogger(ExamTableApp.class);
public static void main(String[] args) {
SolverFactory<ExamTable> solverFactory = SolverFactory.create(new SolverConfig()
.withSolutionClass(ExamTable.class)
.withEntityClasses(Exam.class)
.withConstraintProviderClass(ExamTableConstraintProvider.class)
// The solver runs only for 5 seconds on this small dataset.
// It's recommended to run for at least 5 minutes ("5m") otherwise.
.withTerminationSpentLimit(Duration.ofSeconds(30)));
// Load the problem
ExamTable problem = getData();
// Solve the problem
Solver<ExamTable> solver = solverFactory.buildSolver();
ExamTable solution = solver.solve(problem);
// Visualize the solution
printTimetable(solution);
}
public static ExamTable getData(){
DataReader parser = new DataReader("benchmarks/sta-f-83.crs", "benchmarks/sta-f-83.stu");
List<Room> roomList = new ArrayList<>(1);
roomList.add(new Room(1,"Room A"));
// roomList.add(new Room(2,"Room B"));
// roomList.add(new Room(3,"Room C"));
List<Exam> examList = new ArrayList<>();
HashMap<Integer, Exam> exams = parser.getExams();
Set<Integer> keys = exams.keySet();
for (Integer i : keys) {
Exam exam = exams.get(i);
examList.add(new Exam(exam.getID(), exam.getSID()));
}
List<Student> studentList = new ArrayList<>();
HashMap<Integer, Student> students = parser.getStudents();
Set<Integer> keys2 = students.keySet();
for (Integer i : keys2) {
Student student = students.get(i);
studentList.add(new Student(student.getID(), student.getExamIds()));
}
return new ExamTable(parser.getTimeslots(), roomList, examList, studentList);
}
private static void printTimetable(ExamTable examTable) {
LOGGER.info("");
List<Room> roomList = examTable.getRoomList();
List<Exam> examList = examTable.getExamList();
Map<TimeSlot, Map<Room, List<Exam>>> examMap = examList.stream()
.filter(exam -> exam.getTimeslot() != null && exam.getRoom() != null)
.collect(Collectors.groupingBy(Exam::getTimeslot, Collectors.groupingBy(Exam::getRoom)));
LOGGER.info("| | " roomList.stream()
.map(room -> String.format("%-10s", room.getName())).collect(Collectors.joining(" | ")) " |");
LOGGER.info("|" "------------|".repeat(roomList.size() 1));
for (TimeSlot timeslot : examTable.getTimeslotList()) {
List<List<Exam>> cellList = roomList.stream()
.map(room -> {
Map<Room, List<Exam>> byRoomMap = examMap.get(timeslot);
if (byRoomMap == null) {
return Collections.<Exam>emptyList();
}
List<Exam> cellLessonList = byRoomMap.get(room);
if (cellLessonList == null) {
return Collections.<Exam>emptyList();
}
return cellLessonList;
})
.collect(Collectors.toList());
LOGGER.info("| " String.format("%-10s",
timeslot.getID() " " " | "
cellList.stream().map(cellLessonList -> String.format("%-10s",
cellLessonList.stream().map(Exam::getName).collect(Collectors.joining(", "))))
.collect(Collectors.joining(" | "))
" |"));
LOGGER.info("|" "------------|".repeat(roomList.size() 1));
}
List<Exam> unassignedExams = examList.stream()
.filter(exam -> exam.getTimeslot() == null || exam.getRoom() == null)
.collect(Collectors.toList());
if (!unassignedExams.isEmpty()) {
LOGGER.info("");
LOGGER.info("Unassigned lessons");
for (Exam exam : unassignedExams) {
LOGGER.info(" " exam.getName() " - " exam.getNumberOfStudents() " - " exam.getSID());
}
}
}
}
Could someone help me out with this?
Thanks in advance.
CodePudding user response:
The code you provided shows one major misunderstanding of how Constraint Streams work, which in turn makes it fundamentally broken.
Any instance of ConstraintProvider
must be stateless. Even though you technically can have fields in that class, there is no use for it, as the constraints - when run - need to be without side effects. This way, you have introduced score corruptions and probably have not even noticed.
Also, the constraint weight HardScore.ofSoft(...)
is only computed once during constraint creation, not at solver runtime, therefore defining it like you did would have been pointless anyway. (Its value will be whatever penalty
was at instantiation, therefore 0
.) What you need to use instead is a match weight, which is computed at runtime. Making just that modification, the constraint in question would look like this:
Constraint spaceBetweenExams(ConstraintFactory constraintFactory) {
return constraintFactory.forEach(Student.class)
.join(Exam.class)
.join(Exam.class)
.filter((student, exam1, exam2) -> {
if(student.getExamIds().contains(exam1.getID()) && student.getExamIds().contains(exam2.getID())){
if(exam1.getID() < exam2.getID()){
return true;
}
}
return false;
})
.penalize("Max time between exams", HardSoftScore.ONE_SOFT,
(student, exam1, exam2) ->{
int timeDifference = Math.abs(exam1.getTimeslot().getID() - exam2.getTimeslot().getID());
if (timeDifference == 1) {
return 16;
} else if (timeDifference == 2) {
return 8;
} else if (timeDifference == 3) {
return 4;
} else if (timeDifference == 4) {
return 2;
} else {
return 1;
}
}
});
}
The above code will at least make this code run properly, but it will likely be slow. See my other recent answer for inspiration on how to change your domain model to improve performance of your constraints. It may not fit exactly, but the idea seems valid to your use case as well.