optaplannervehicle-routingtimefold

SolverConfig not able to find multiple entity classes - Error "there are multiple in the entityClassSet"


Trying to solve a clincian/provider's visit scheduling with time slots constraints, having issues in configuration with multiple planning entities. A provider will take many visits in a day based on provider's slots and member's availability and a visit can only have one provider. I have tried everything available but not able to find solution. Really appreciate any help.

Use case :

Clinician/Provider has avaialbility in a day and member also has avaialabilty, providers slot can be broken up into 1 hour slots. Provider will start from home and perform visits. We need to find optimum viists for provider. Below is example. It is Vehicle Routing Problem time windowed with additional constraints.

Now instead of distance we need to check driving time and 1 hour visit slot, so lets say provider slots are from 8-11am and an average visit is 55 min and visit window for member is 8-10am, then provider should reach member location in between 8-10am and also complete 55 min visit within that time frame, so basically it is vehicle routing problem with time window of both provider and member and considering driving time and visit time. so instead of haversine distance we will use mapbox api to calculate drive time and assume visit time to be 55 mins.

below example route for provider to clarify more:

provider slot : 8-11am

visit 1(time window 8-10am) : drive time 20 mins leave home at 7:40am reach at 8am and do visit for 55 mins , that is till 8:55am (within visit time window) assigned provider slot : 8-9am

visit 1 to visit 2(time window 9-11am) : drive time 20 mins reach at 9:15am and do visit for 55 mins , that is till 10:05am (within visit time window) assigned provider slot : 9-10am

visit 2 to visit 3(time window 10-11am) : drive time 20 mins reach at 10:25am and do visit for 55 mins , that is till 11:20am (within visit time window) , so this visit should not be assigned to the provider No provider slot assigned

Below is my entity classes :

Planning Entity 1

@PlanningEntity
@Data
public class Visits {
    private String _id;
    private Double matrixVisitId;
    private Long clientId;
    private VisitMsa msa;
    private Date[] slotPreferences;
    private String[] products;
    private VisitsMember member;
    private String slotType;
    private boolean pinned;
    private TimeWindow timeWindow;

    @PlanningVariable(valueRangeProviderRefs = "timeBlockRangeForVisit", nullable = true)
    private ClinicianSlot assignedSlot;

    private Providers assignedClinician;
    private Visits previousVisit;
    private Visits nextVisit;

    private LocalDateTime previousVisitEndTime;

    @ValueRangeProvider(id = "timeBlockRangeForVisit")
    public List<ClinicianSlot> generatePossibleTimeBlocks() {
        if (this.timeWindow == null) {
            return Collections.emptyList();
        }
        List<ClinicianSlot> possibleTimeBlocks = new ArrayList<>();
        LocalDateTime start = timeWindow.getStart();
        while (!start.plusHours(1).isAfter(timeWindow.getEnd())) {
            possibleTimeBlocks.add(new ClinicianSlot(start, start.plusHours(1)));
            start = start.plusHours(1);
        }
        return possibleTimeBlocks;
    }

    @PlanningPin
    public boolean isPinned() {
        return pinned;
    }

    @PreviousElementShadowVariable(sourceVariableName = "visits")
    public Visits getPreviousVisit() {
        return previousVisit;
    }

    public void setPreviousVisit(Visits previousVisit) {
        this.previousVisit = previousVisit;
    }

    @NextElementShadowVariable(sourceVariableName = "visits")
    public Visits getNextVisit() {
        return nextVisit;
    }

    public void setNextVisit(Visits nextVisit) {
        this.nextVisit = nextVisit;
    }

    @InverseRelationShadowVariable(sourceVariableName = "visits")
    public Providers getAssignedClinician() {
        return assignedClinician;
    }

    public void setVehicle(Providers assignedClinician) {
        this.assignedClinician = assignedClinician;
    }

    @ShadowVariable(variableListenerClass = VisitChainVariableListener.class, sourceVariableName = "previousVisit")
    public LocalDateTime getPreviousVisitEndTime() {
        return previousVisitEndTime;
    }

    public void setPreviousVisitEndTime(LocalDateTime previousVisitEndTime) {
        this.previousVisitEndTime = previousVisitEndTime;
    }
}

Planning Entity 2

@Data
@PlanningEntity
@AllArgsConstructor
public class Providers {
    private Integer staffResourceId;
    private String providerId;
    private ProviderMsa[] msa;
    private Client[] clients;
    private String[] products;
    private List<ClinicianSlot> availableSlots;

    @PlanningListVariable(valueRangeProviderRefs = "visitRange")
    private List<Visits> visits;

    @GeoSpatialIndexed(type = GeoSpatialIndexType.GEO_2DSPHERE)
    private Location location;
}

Solution class

@PlanningSolution
@Data
public class ProviderRouteSolution {

    private List<Visits> visitList;

    @PlanningEntityCollectionProperty
    @ValueRangeProvider(id = "visitRange")
    public List<Visits> getVisitList() {
        return visitList;
    }

    private List<Providers> clinicianList;
    
    @PlanningScore
    private HardSoftScore score;

    @PlanningEntityCollectionProperty
    public List<Providers> getClinicianList() {
        return clinicianList;
    }
}

Below is my service class where i am initialising Solver :

@Service
public class ProviderRouteService {

@Autowired
    public ProviderRouteService(
            ProvidersRepository providersRepository, VisitsRepository visitsRepository, ProviderSlotsRepository providerSlotsRepository, RoutesRepository routesRepository
    ) {
        this.providersRepository = providersRepository;
        this.visitsRepository = visitsRepository;
        this.providerSlotsRepository = providerSlotsRepository;
        this.routesRepository = routesRepository;
    }

    @PostConstruct
    public void init() {
        SolverConfig template = SolverConfig.createFromXmlResource("vehicleRoutingSolverConfig.xml");
        SolverFactory<ProviderRouteSolution> solverFactory = SolverFactory.create(template);
        this.solver = solverFactory.buildSolver();
    }

...additional code here

}

Here is my vehicleRoutingSolverConfig.xml :

<?xml version="1.0" encoding="UTF-8"?>

<solver xmlns="https://www.optaplanner.org/xsd/solver" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="https://www.optaplanner.org/xsd/solver https://www.optaplanner.org/xsd/solver/solver.xsd">

    <solutionClass>com.optaplanner.dto.ProviderRouteSolution</solutionClass>
    <entityClass>com.optaplanner.model.Visits</entityClass>
    <entityClass>com.optaplanner.model.Providers</entityClass>

    <scoreDirectorFactory>
        <constraintProviderClass>com.optaplanner.service.impl.ProviderSchedulingConstraintProvider
        </constraintProviderClass>
        <initializingScoreTrend>ONLY_DOWN</initializingScoreTrend>
    </scoreDirectorFactory>

    <termination>
        <minutesSpentLimit>5</minutesSpentLimit>
    </termination>
    <constructionHeuristic>
    </constructionHeuristic>
    <localSearch>
        <unionMoveSelector>
            <listChangeMoveSelector>
                <valueSelector id="1"/>
                <destinationSelector>
                    <entitySelector>
                        <entityClass>com.optaplanner.model.Providers</entityClass>
                    </entitySelector>
                    <valueSelector variableName="visits"/>
                </destinationSelector>
            </listChangeMoveSelector>
        </unionMoveSelector>
        <acceptor>
            <lateAcceptanceSize>200</lateAcceptanceSize>
        </acceptor>
        <forager>
            <acceptedCountLimit>1</acceptedCountLimit>
        </forager>
    </localSearch>
</solver>

When i run my sprint boot app i encounter below error :

Caused by: java.lang.IllegalArgumentException: The config (QueuedValuePlacerConfig(ValueSelectorConfig(visits), ListChangeMoveSelectorConfig(ValueSelectorConfig(null), null))) has no entityClass configured and because there are multiple in the entityClassSet ([class com.optaplanner.model.Visits, class com.optaplanner.model.Providers]), it cannot be deduced automatically.
    at org.optaplanner.core.impl.AbstractFromConfigFactory.getTheOnlyEntityDescriptor(AbstractFromConfigFactory.java:67) ~[optaplanner-core-impl-9.44.0.Final.jar:9.44.0.Final]
    at org.optaplanner.core.impl.AbstractFromConfigFactory.deduceEntityDescriptor(AbstractFromConfigFactory.java:44) ~[optaplanner-core-impl-9.44.0.Final.jar:9.44.0.Final]
    at org.optaplanner.core.impl.constructionheuristic.placer.QueuedValuePlacerFactory.buildEntityPlacer(QueuedValuePlacerFactory.java:34) ~[optaplanner-core-impl-9.44.0.Final.jar:9.44.0.Final]
    at org.optaplanner.core.impl.constructionheuristic.placer.QueuedValuePlacerFactory.buildEntityPlacer(QueuedValuePlacerFactory.java:20) ~[optaplanner-core-impl-9.44.0.Final.jar:9.44.0.Final]
    at org.optaplanner.core.impl.constructionheuristic.DefaultConstructionHeuristicPhaseFactory.buildPhase(DefaultConstructionHeuristicPhaseFactory.java:73) ~[optaplanner-core-impl-9.44.0.Final.jar:9.44.0.Final]
    at org.optaplanner.core.impl.constructionheuristic.DefaultConstructionHeuristicPhaseFactory.buildPhase(DefaultConstructionHeuristicPhaseFactory.java:43) ~[optaplanner-core-impl-9.44.0.Final.jar:9.44.0.Final]
    at org.optaplanner.core.impl.phase.PhaseFactory.buildPhases(PhaseFactory.java:59) ~[optaplanner-core-impl-9.44.0.Final.jar:9.44.0.Final]
    at org.optaplanner.core.impl.solver.DefaultSolverFactory.buildPhaseList(DefaultSolverFactory.java:233) ~[optaplanner-core-impl-9.44.0.Final.jar:9.44.0.Final]
    at org.optaplanner.core.impl.solver.DefaultSolverFactory.buildSolver(DefaultSolverFactory.java:126) ~[optaplanner-core-impl-9.44.0.Final.jar:9.44.0.Final]
    at com.optaplanner.service.impl.ProviderRouteService.init(ProviderRouteService.java:69) ~[classes/:na]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
    at java.base/java.lang.reflect.Method.invoke(Method.java:566) ~[na:na]

Solution

  • Having both @PlanningListVariable and @PlanningVariable is currently not supported; see the warning in the docs or this issue for details. You might want to use chained variables, which do not have this limitation.