Search code examples
javaoptaplanneroptaweb-employee-rosteringoptaweb-vehicle-routing

Optaplanner assigning incorrect planning entity during planning


I have a use case where I want to assign a salesperson to a list of appointments. Now, these salespeople have to travel from one point to another to reach the appointment location. I am using Optaplanner to schedule a list of salesperson to a bunch of appointments. I have a constraint defined:

Constraint repConflict(ConstraintFactory constraintFactory) {
        // A sales-rep can accommodate at most one appointment at the same time.
        return constraintFactory
                // Select each pair of 2 different appointments ...
                .forEachUniquePair(Appointment.class,
                        Joiners.equal(Appointment::getRepUuid))
                .filter((appt1, appt2) ->{
                    if(appt1.getStartTime().before(appt2.getStartTime()) &&
                            appt1.getEndTime().before(appt2.getStartTime()) &&
                            appt1.getStartTime().before(appt2.getEndTime()) &&
                            appt1.getEndTime().before(appt2.getEndTime())) {
                        return false;
                    }
                    return true;
                })
                // ... and penalize each pair with a hard weight.
                .penalize(HardSoftScore.ONE_HARD)
                .asConstraint("SalesRep conflict");
    }

This constraint checks if a sales rep is assigned to at most one appointment at any instance The constraint works fine but the planner assigns some random sales rep for appointments with no feasible solution. This completely makes the final solution unusable. Our requirement is if the solution is not feasible(no sales rep can be assigned) then don't assign anybody to the appointment.

I looked into the documentation and changed the SolverFactory with the following configuration but still no progress

SolverFactory<RepRoutingSolution> solverFactory = SolverFactory.create(new SolverConfig()
                .withSolutionClass(RepRoutingSolution.class)
                .withEntityClasses(Appointment.class)
                .withConstraintProviderClass(RepSchedulerConstraintProvider.class)
                .withTerminationConfig(new TerminationConfig()
                        .withBestScoreFeasible(true)
                )
                // 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(5)));


        // Load the problem
        RepRoutingSolution problem = generateDemoData();

        // Solve the problem
        Solver<RepRoutingSolution> solver = solverFactory.buildSolver();
        RepRoutingSolution solution = solver.solve(problem);

        // Visualize the solution
        printRepVisits(solution);

Edit 1: Newly added constraint to reward for successfully assigning sales rep. This reward based constraint fixed the issue in my case

Constraint repRewardForAppointment(ConstraintFactory constraintFactory) {
        
        return constraintFactory
                // Select each pair of 2 different appointments ...
                .forEachUniquePair(Appointment.class,
                        Joiners.equal(Appointment::getRepUuid))
                .filter((appt1, appt2) -> {
                    if (appt1.getStartTime().before(appt2.getStartTime()) &&
                            appt1.getEndTime().before(appt2.getStartTime()) &&
                            appt1.getStartTime().before(appt2.getEndTime()) &&
                            appt1.getEndTime().before(appt2.getEndTime())) {
                        return true;
                    }
                    return false;
                })
                // ... and penalize each pair with a hard weight.
                .reward(HardMediumSoftScore.ONE_MEDIUM)
                .asConstraint("SalesRep reward for Appointments");
    }

Solution

  • What you're likely looking for is called overconstrained planning. You'll need a nullable planning variable.