Search code examples
javaspring-bootconstraintsoptaplannertimefold

How to choose dynamically which constraints should be applied on the optimization problem based on the frontend input in Timefold Spring Boot?


Let's assume that we have a fullstack application that has a page in the frontend side where we can select the constraints that we should apply to a specific problem. The list of those constraints will be sent to the backend side when we run the Timefold Solver for that specific problem.

How can I make sure that the Timefold Solver will apply just the constraints I chose from the frontend side? How can I modify the TimetableConstraintProvider (for example) to achieve the mentioned functionality.

public class TimetableConstraintProvider implements ConstraintProvider {

    @Override
    public Constraint[] defineConstraints(ConstraintFactory constraintFactory) {
        return new Constraint[] {
                // Hard constraints
                roomConflict(constraintFactory),
                teacherConflict(constraintFactory),
                studentGroupConflict(constraintFactory),
                // Soft constraints
                teacherRoomStability(constraintFactory),
                teacherTimeEfficiency(constraintFactory),
                studentGroupSubjectVariety(constraintFactory)
        };
    }
                //implementation of the constraints
}

I assume that, first, we should have an POST/GET endpoints for the selected constraints. After that what are the next steps? Any help is welcomed. Thank you!


Solution

  • Based on the documentation about Constraint Configuration (https://timefold.ai/docs/timefold-solver/latest/constraints-and-score/constraint-configuration#constraintWeight) I managed to build dynamic constraints by extending the timetable project (https://github.com/TimefoldAI/timefold-quickstarts/tree/stable/technology/java-spring-boot). Please note that the solution is not using Spring Data JPA for the simplicity:

    So, first, you need a Constraint Configuration Bean where you input all the constraints' weights (ZERO means that the certain constraint will not be taken into account by the Solver):

    import ai.timefold.solver.core.api.domain.constraintweight.ConstraintConfiguration;
    import ai.timefold.solver.core.api.domain.constraintweight.ConstraintWeight;
    import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore;
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    import java.util.List;
    
    //constraintPackage = "the package where you can find your ConstraintProvider"
    @ConstraintConfiguration(constraintPackage = "com.timetablealgo.testingtimetablealgo.solver")
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class TimetableConstraintConfiguration {
    
        @ConstraintWeight("roomConflict")
        private HardSoftScore roomConflict = HardSoftScore.ZERO; //hard
        @ConstraintWeight("teacherConflict")
        private HardSoftScore teacherConflict = HardSoftScore.ZERO; //hard
        @ConstraintWeight("studentGroupConflict")
        private HardSoftScore studentGroupConflict= HardSoftScore.ZERO; //hard
        @ConstraintWeight("teacherRoomStability")
        private HardSoftScore teacherRoomStability= HardSoftScore.ZERO; //soft
        @ConstraintWeight("teacherTimeEfficiency")
        private HardSoftScore teacherTimeEfficiency = HardSoftScore.ZERO; //soft
        @ConstraintWeight("studentGroupVariety")
        private HardSoftScore studentGroupVariety = HardSoftScore.ZERO; //soft
    
        public TimetableConstraintConfiguration(List<String> constraints){
           constraints.forEach(element -> {
                switch (element) {
                    case "roomConflict" -> roomConflict = HardSoftScore.ONE_HARD;
                    case "teacherConflict" -> teacherConflict = HardSoftScore.ONE_HARD;
                    case "studentGroupConflict" -> studentGroupConflict = HardSoftScore.ONE_HARD;
                    case "teacherRoomStability" -> teacherRoomStability = HardSoftScore.ONE_SOFT;
                    case "teacherTimeEfficiency" -> teacherTimeEfficiency = HardSoftScore.ONE_SOFT;
                    case "studentGroupVariety" -> studentGroupVariety = HardSoftScore.ONE_SOFT;
                    // Add more cases for other constraints if needed
                }
            });
        }
    }
    

    Add the Constraint Configuration into your @PlanningSolution Bean:

    @PlanningSolution
    public class Timetable {
    
        @ConstraintConfigurationProvider
        private TimetableConstraintConfiguration timetableConstraintConfiguration;
    ...
    
    public Timetable(String name, List<Timeslot> timeslots, List<Room> rooms, List<Lesson> lessons, TimetableConstraintConfiguration timetableConstraintConfiguration) {
            this.name = name;
            this.timeslots = timeslots;
            this.rooms = rooms;
            this.lessons = lessons;
            this.timetableConstraintConfiguration = timetableConstraintConfiguration;
        }
    ...
        }
    

    Rewrite every constraint in the ConstraintProvider Bean using penalizeConfigurable() or rewardConfigurable() methods:

    Constraint roomConflict(ConstraintFactory constraintFactory) {
            // A room can accommodate at most one lesson at the same time.
            return constraintFactory
                    // Select each pair of 2 different lessons ...
                    .forEachUniquePair(Lesson.class,
                            // ... in the same timeslot ...
                            Joiners.equal(Lesson::getTimeslot),
                            // ... in the same room ...
                            Joiners.equal(Lesson::getRoom))
                    //.penalize(HardSoftScore.ONE_HARD)
                   // you can also add a lambda function to multiply
                   // the score penalty: (l1,l2) -> return a number 
                    .penalizeConfigurable()
                    .justifyWith((lesson1, lesson2, score) -> new RoomConflictJustification(lesson1.getRoom(), lesson1, lesson2))
                    .asConstraint("roomConflict");
        }
    

    In this case, this constraint will search in the ConstraintConfiguration Bean for a constraint with the value "roomConflict" to apply the score penalty.

    Then, in the DemoDataController Bean add a list of constraints to be taken into account by the Timefold Solver:

    //constraints
    // if you want to use Spring Jpa, it would be 
    // List<String> constraints = constraintsRepo.findAll();
            List<String> constraints = List.of("roomConflict", "teacherConflict", "studentGroupConflict",
                    "teacherRoomStability", "teacherTimeEfficiency", "studentGroupVariety");
            TimetableConstraintConfiguration timetableConstraintConfiguration = new TimetableConstraintConfiguration(constraints);
    

    and return the Timetable with the timetableConstraintConfiguration included:

    return ResponseEntity.ok(new Timetable(demoData.name(), timeslots, rooms, lessons, timetableConstraintConfiguration));
    

    And that's it. It should work. Please let me know if it worked.

    PS: if you want to use Spring Data JPA, you would want to create a CRUD application where you can post and get all the data needed for the Timetable (there is in the Timefold documentation how you use Spring JPA with Timefold Solver). So, before you initialize the Timetable object that you want to be solved, make sure that you use the Repository to get all the data from your domain (Room, Timeslot, Lesson, Constraints) and let Timefold do the dirty work for you. In short, to make an application that also uses JPA practically you just need to adapt the code from GitHub to a CRUD environment.

    PPS: Thank you Timefold team for your support!