Aiman Daniel
Aiman Daniel

Reputation: 169

Timefold load balancing problem unfairness is always 0

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:

enter image description here

In this problem, I'm getting an unfairness value of 0 even if all courses are assigned to Faizal or Ahmad.

My questions are:

  1. Why does LoadBalance::unfairness() always return 0? Do i need to implement my own unfairness function?

  2. 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))
        );

enter image description here

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

enter image description here


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

Answers (1)

Aiman Daniel
Aiman Daniel

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 Lecturers 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

Related Questions