java - Timefold load balancing problem unfairness is always 0 - Stack Overflow

admin2025-04-17  3

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:

  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 plement() 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");
    }
}

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:

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

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");
    }
}
Share Improve this question edited Jan 31 at 0:31 Aiman Daniel asked Jan 30 at 18:59 Aiman DanielAiman Daniel 1691 silver badge10 bronze badges 3
  • Hello Aiman, your class LecturerCourseBalancing, has an attribute HardSoftBigDecimalScore score, which from what you show is never instantiated... if not, you could add the code where this is done?. – Marce Puente Commented Jan 30 at 22:45
  • @MarcePuente I believe that property will be set by Timefold automatically – Aiman Daniel Commented Jan 31 at 0:12
  • Score will indeed be set by Timefold Solver. – Tom Cools Commented Jan 31 at 12:07
Add a comment  | 

1 Answer 1

Reset to default 4

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]]
转载请注明原文地址:http://anycun.com/QandA/1744896469a89158.html