Reputation: 169
My objective is to distribute the courses to the lecturers fairly. The balanced entity is Lecturer
, and the load value is the total duration of the courses assigned to the Lecturer
This is the output of the solver when it calls penalizeBigDecimal
:
In this problem, I'm getting an unfairness value of 0 even if all courses are assigned to Faizal or Ahmad.
My questions are:
Why does LoadBalance::unfairness()
always return 0? Do i need to implement my own unfairness function?
How do I ensure all lecturers are present in LoadBalance::loads()
? If I do need to implement a fairness function, say, average duration per lecturer, I assume I need all lecturers to present in LoadBalance::loads()
to calculate the average
I tried chaining .complement()
after .groupBy()
but it seems to be asking a Class<LoadBalance<Lecturer>>
as parameter, which I'm not exactly sure how to pass.
EDIT:
Upon changing one of the course duration to 70, the solver gives a non-zero unfairness
List<Course> courses = List.of(
new Course(String.valueOf(courseId++), "Fizik 1", 70),
new Course(String.valueOf(courseId++), "Chem 1", 60)
);
List<LecturerCourseAssignment> assignments = List.of(
new LecturerCourseAssignment(courses.get(0)),
new LecturerCourseAssignment(courses.get(1))
);
I believe the unfairness is 0 due to the absence of Faizal in LoadBalance::loads()
However, when I added 2 more LecturerCourseAssignments
with duration of 60, it still gives an unbalanced solution
List<Course> courses = List.of(
new Course(String.valueOf(courseId++), "Fizik 1", 60),
new Course(String.valueOf(courseId++), "Chem 1", 60),
new Course(String.valueOf(courseId++), "Math 1", 60),
new Course(String.valueOf(courseId++), "Programming 1", 60)
);
List<LecturerCourseAssignment> assignments = List.of(
new LecturerCourseAssignment(courses.get(0)),
new LecturerCourseAssignment(courses.get(1)),
new LecturerCourseAssignment(courses.get(2)),
new LecturerCourseAssignment(courses.get(3))
);
PlanningSolution class:
@PlanningSolution
public class LecturerCourseBalancing {
@ProblemFactCollectionProperty
@ValueRangeProvider
List<Lecturer> lecturers;
@PlanningEntityCollectionProperty
List<LecturerCourseAssignment> courses;
@PlanningScore
HardSoftBigDecimalScore score;
public LecturerCourseBalancing() {}
public LecturerCourseBalancing(List<Lecturer> lecturers, List<LecturerCourseAssignment> courses) {
this.lecturers = lecturers;
this.courses = courses;
}
public List<Lecturer> getLecturers() {
return lecturers;
}
public List<LecturerCourseAssignment> getCourses() {
return courses;
}
public HardSoftBigDecimalScore getScore() {
return score;
}
}
LecturerCourseAssignment (PlanningEntity):
@PlanningEntity
public class LecturerCourseAssignment {
@PlanningVariable
Lecturer lecturer;
Course course;
public LecturerCourseAssignment() {
}
public LecturerCourseAssignment(Course course) {
this.course = course;
}
public void setLecturer(Lecturer lecturer) {
this.lecturer = lecturer;
}
public Lecturer getLecturer() {
return lecturer;
}
public Course getCourse() {
return course;
}
@Override
public String toString() {
return "LecturerCourseAssignment [lecturer=" + lecturer + ", course=" + course + "]";
}
}
Lecturer.java (ProblemFact):
public class Lecturer {
@PlanningId
String id;
String name;
public Lecturer(String id, String name) {
this.id = id;
this.name = name;
}
public String getId() {
return id;
}
public String getName() {
return name;
}
@Override
public String toString() {
return "Lecturer [id=" + id + ", name=" + name + "]";
}
}
Course.java:
public class Course {
@PlanningId
String id;
String name;
int duration;
public Course() {}
public Course(String id, String name, int duration) {
this.id = id;
this.name = name;
this.duration = duration;
}
public String getId() {
return id;
}
public int getDuration() {
return duration;
}
public String getName() {
return name;
}
@Override
public String toString() {
return "Course [id=" + id + ", name=" + name + ", duration=" + duration + "]";
}
}
Constraint Provider:
public class LecturerCourseBalancingConstraintProvider implements ConstraintProvider {
@Override
public Constraint @NonNull [] defineConstraints(@NonNull ConstraintFactory constraintFactory) {
return new Constraint[] {
fairLecturerCourseAssignment(constraintFactory),
};
}
Constraint fairLecturerCourseAssignment(ConstraintFactory constraintFactory) {
return constraintFactory.forEach(LecturerCourseAssignment.class)
.groupBy(ConstraintCollectors.loadBalance(lca -> lca.getLecturer(), lca -> lca.getCourse().getDuration()))
.penalizeBigDecimal(HardSoftBigDecimalScore.ONE_SOFT, (loadBalance) -> {
System.out.println(loadBalance.loads());
System.out.println(loadBalance.unfairness());
return loadBalance.unfairness();
})
.asConstraint("fairLecturerCourseAssignment");
}
}
Upvotes: 0
Views: 62
Reputation: 169
After reading the documentation closely, I managed to find a solution.
First, we need to sum the Course
duration, grouped by Lecturer
. Then, complement the solution with 0
for Lecturer
s that are not assigned to any courses. This way, when we perform a groupBy with ConstraintCollectors.loadBalance()
, all Lecturer
will be present during the unfairness calculation.
Constraint Provider:
public class LecturerCourseBalancingConstraintProvider implements ConstraintProvider {
@Override
public Constraint @NonNull [] defineConstraints(@NonNull ConstraintFactory constraintFactory) {
return new Constraint[] {
fairLecturerCourseAssignment(constraintFactory),
};
}
Constraint fairLecturerCourseAssignment(ConstraintFactory constraintFactory) {
return constraintFactory.forEach(LecturerCourseAssignment.class)
.groupBy(LecturerCourseAssignment::getLecturer, ConstraintCollectors.sum(x -> x.getCourse().getDuration()))
.complement(Lecturer.class, t -> 0)
.groupBy(ConstraintCollectors.loadBalance((lecturer, totalDuration) -> lecturer, (lecturer, totalDuration) -> totalDuration))
.penalizeBigDecimal(HardSoftBigDecimalScore.ONE_SOFT, LoadBalance::unfairness)
.asConstraint("fairLecturerCourseAssignment");
}
}
Solver output:
2025-01-31 11:06:18,752 INFO [ai.tim.sol.cor.imp.sol.DefaultSolver] (pool-19-thread-1) Solving started: time spent (1), best score (-4init/0hard/0soft), environment mode (FULL_ASSERT), move thread count (NONE), random (JDK with seed 0).
2025-01-31 11:06:18,752 INFO [ai.tim.sol.cor.imp.sol.DefaultSolver] (pool-19-thread-1) Problem scale: entity count (4), variable count (4), approximate value count (2), approximate problem scale (16).
2025-01-31 11:06:18,756 INFO [ai.tim.sol.cor.imp.con.DefaultConstructionHeuristicPhase] (pool-19-thread-1) Construction Heuristic phase (0) ended: time spent (5), best score (0hard/-7.07107soft), move evaluation speed (2666/sec), step total (4).
2025-01-31 11:06:18,756 INFO [com.aim.SolverResource] (pool-20-thread-1) Score: 0hard/-7.07107soft
2025-01-31 11:06:18,756 INFO [com.aim.SolverResource] (pool-20-thread-1) Courses:
2025-01-31 11:06:18,757 INFO [com.aim.SolverResource] (pool-20-thread-1) LecturerCourseAssignment [lecturer=Lecturer [id=1, name=Ahmad], course=Course [id=1, name=Fizik 1, duration=70]]
2025-01-31 11:06:18,757 INFO [com.aim.SolverResource] (pool-20-thread-1) LecturerCourseAssignment [lecturer=Lecturer [id=2, name=Faizal], course=Course [id=2, name=Chem 1, duration=60]]
2025-01-31 11:06:18,757 INFO [com.aim.SolverResource] (pool-20-thread-1) LecturerCourseAssignment [lecturer=Lecturer [id=2, name=Faizal], course=Course [id=3, name=Math 1, duration=60]]
2025-01-31 11:06:18,757 INFO [com.aim.SolverResource] (pool-20-thread-1) LecturerCourseAssignment [lecturer=Lecturer [id=1, name=Ahmad], course=Course [id=4, name=Programming 1, duration=60]]
2025-01-31 11:06:28,751 INFO [ai.tim.sol.cor.imp.loc.DefaultLocalSearchPhase] (pool-19-thread-1) Local Search phase (1) ended: time spent (10000), best score (0hard/-7.07107soft), move evaluation speed (7051/sec), step total (35182).
2025-01-31 11:06:28,754 INFO [ai.tim.sol.cor.imp.sol.DefaultSolver] (pool-19-thread-1) Solving ended: time spent (10002), best score (0hard/-7.07107soft), move evaluation speed (7047/sec), phase total (2), environment mode (FULL_ASSERT), move thread count (NONE).
2025-01-31 11:06:28,754 INFO [com.aim.SolverResource] (pool-20-thread-1) Found best solution for job 53d7ba83-4677-4e0c-9e31-3eb5f5d15263
2025-01-31 11:06:28,755 INFO [com.aim.SolverResource] (pool-20-thread-1) Best solution: com.aimandaniel.LecturerCourseBalancing@36282a99
2025-01-31 11:06:28,755 INFO [com.aim.SolverResource] (pool-20-thread-1) Score: 0hard/-7.07107soft
2025-01-31 11:06:28,756 INFO [com.aim.SolverResource] (pool-20-thread-1) Courses:
2025-01-31 11:06:28,756 INFO [com.aim.SolverResource] (pool-20-thread-1) LecturerCourseAssignment [lecturer=Lecturer [id=1, name=Ahmad], course=Course [id=1, name=Fizik 1, duration=70]]
2025-01-31 11:06:28,757 INFO [com.aim.SolverResource] (pool-20-thread-1) LecturerCourseAssignment [lecturer=Lecturer [id=2, name=Faizal], course=Course [id=2, name=Chem 1, duration=60]]
2025-01-31 11:06:28,757 INFO [com.aim.SolverResource] (pool-20-thread-1) LecturerCourseAssignment [lecturer=Lecturer [id=2, name=Faizal], course=Course [id=3, name=Math 1, duration=60]]
2025-01-31 11:06:28,757 INFO [com.aim.SolverResource] (pool-20-thread-1) LecturerCourseAssignment [lecturer=Lecturer [id=1, name=Ahmad], course=Course [id=4, name=Programming 1, duration=60]]
Upvotes: 4