TimefoldAI / timefold-solver

The open source Solver AI for Java, Python and Kotlin to optimize scheduling and routing. Solve the vehicle routing problem, employee rostering, task assignment, maintenance scheduling and other planning problems.
https://timefold.ai
Apache License 2.0
940 stars 81 forks source link

Chained model with additional planning variable is no longer working #931

Closed simontiffert closed 2 months ago

simontiffert commented 3 months ago

Describe the bug Chained model and one additional planning variable is no longer working with Timefold versions > 1.7.0

Expected behavior Auto-configuration working for PlanningVariableGraphType.CHAINED with additional PlanningVariables.

Actual behavior Error message after start of solving:

2024-07-03 08:35:12,652 ERROR [io.qua.ver.htt.run.QuarkusErrorHandler] (executor-thread-1) HTTP Request to /route-plans failed, error id: 1f3b37cd-2730-4a3c-b613-e77be28d6b25-1: org.jboss.resteasy.spi.UnhandledException: java.lang.IllegalArgumentException: The config (ValueSelectorConfig(null)) has no configured variableName for entityClass (class org.acme.vehiclerouting.domain.Visit) and because there are multiple variableNames ([count, previousVisit]), it cannot be deduced automatically. at org.jboss.resteasy.core.ExceptionHandler.handleException(ExceptionHandler.java:357) at org.jboss.resteasy.core.SynchronousDispatcher.writeException(SynchronousDispatcher.java:205) at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:452) at org.jboss.resteasy.core.SynchronousDispatcher.lambda$invokePropagateNotFound$6(SynchronousDispatcher.java:275) at org.jboss.resteasy.core.SynchronousDispatcher.lambda$preprocess$0(SynchronousDispatcher.java:154) at org.jboss.resteasy.core.interception.jaxrs.PreMatchContainerRequestContext.filter(PreMatchContainerRequestContext.java:321) at org.jboss.resteasy.core.SynchronousDispatcher.preprocess(SynchronousDispatcher.java:157) at org.jboss.resteasy.core.SynchronousDispatcher.invokePropagateNotFound(SynchronousDispatcher.java:260) at io.quarkus.resteasy.runtime.standalone.RequestDispatcher.service(RequestDispatcher.java:84) at io.quarkus.resteasy.runtime.standalone.VertxRequestHandler.dispatch(VertxRequestHandler.java:151) at io.quarkus.resteasy.runtime.standalone.VertxRequestHandler$1.run(VertxRequestHandler.java:97) at io.quarkus.vertx.core.runtime.VertxCoreRecorder$14.runWith(VertxCoreRecorder.java:599) at org.jboss.threads.EnhancedQueueExecutor$Task.doRunWith(EnhancedQueueExecutor.java:2516) at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2495) at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1521) at org.jboss.threads.DelegatingRunnable.run(DelegatingRunnable.java:11) at org.jboss.threads.ThreadLocalResettingRunnable.run(ThreadLocalResettingRunnable.java:11) at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) at java.base/java.lang.Thread.run(Thread.java:1583) Caused by: java.lang.IllegalArgumentException: The config (ValueSelectorConfig(null)) has no configured variableName for entityClass (class org.acme.vehiclerouting.domain.Visit) and because there are multiple variableNames ([count, previousVisit]), it cannot be deduced automatically. at ai.timefold.solver.core.impl.AbstractFromConfigFactory.getTheOnlyVariableDescriptor(AbstractFromConfigFactory.java:100) at ai.timefold.solver.core.impl.AbstractFromConfigFactory.deduceGenuineVariableDescriptor(AbstractFromConfigFactory.java:76) at ai.timefold.solver.core.impl.heuristic.selector.value.ValueSelectorFactory.buildValueSelector(ValueSelectorFactory.java:89) at ai.timefold.solver.core.impl.heuristic.selector.value.ValueSelectorFactory.buildValueSelector(ValueSelectorFactory.java:81) at ai.timefold.solver.core.impl.heuristic.selector.move.generic.chained.TailChainSwapMoveSelectorFactory.buildBaseMoveSelector(TailChainSwapMoveSelectorFactory.java:36) at ai.timefold.solver.core.impl.heuristic.selector.move.AbstractMoveSelectorFactory.buildMoveSelector(AbstractMoveSelectorFactory.java:70) at ai.timefold.solver.core.impl.heuristic.selector.move.composite.AbstractCompositeMoveSelectorFactory.lambda$buildInnerMoveSelectors$0(AbstractCompositeMoveSelectorFactory.java:28) at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197) at java.base/java.util.LinkedList$LLSpliterator.forEachRemaining(LinkedList.java:1249) at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509) at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499) at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921) at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:682) at ai.timefold.solver.core.impl.heuristic.selector.move.composite.AbstractCompositeMoveSelectorFactory.buildInnerMoveSelectors(AbstractCompositeMoveSelectorFactory.java:29) at ai.timefold.solver.core.impl.heuristic.selector.move.composite.UnionMoveSelectorFactory.buildBaseMoveSelector(UnionMoveSelectorFactory.java:46) at ai.timefold.solver.core.impl.heuristic.selector.move.AbstractMoveSelectorFactory.buildMoveSelector(AbstractMoveSelectorFactory.java:70) at ai.timefold.solver.core.impl.localsearch.DefaultLocalSearchPhaseFactory.buildMoveSelector(DefaultLocalSearchPhaseFactory.java:184) at ai.timefold.solver.core.impl.localsearch.DefaultLocalSearchPhaseFactory.buildDecider(DefaultLocalSearchPhaseFactory.java:67) at ai.timefold.solver.core.impl.localsearch.DefaultLocalSearchPhaseFactory.buildPhase(DefaultLocalSearchPhaseFactory.java:53) at ai.timefold.solver.core.impl.localsearch.DefaultLocalSearchPhaseFactory.buildPhase(DefaultLocalSearchPhaseFactory.java:40) at ai.timefold.solver.core.impl.phase.PhaseFactory.buildPhases(PhaseFactory.java:59) at ai.timefold.solver.core.impl.solver.DefaultSolverFactory.buildPhaseList(DefaultSolverFactory.java:249) at ai.timefold.solver.core.impl.solver.DefaultSolverFactory.buildSolver(DefaultSolverFactory.java:125) at ai.timefold.solver.core.api.solver.SolverFactory.buildSolver(SolverFactory.java:119) at ai.timefold.solver.core.impl.solver.DefaultSolverManager.validateSolverFactory(DefaultSolverManager.java:65) at ai.timefold.solver.core.impl.solver.DefaultSolverManager.<init>(DefaultSolverManager.java:49) at ai.timefold.solver.core.api.solver.SolverManager.create(SolverManager.java:98) at ai.timefold.solver.quarkus.bean.DefaultTimefoldBeanProvider.solverManager(DefaultTimefoldBeanProvider.java:62) at ai.timefold.solver.quarkus.bean.DefaultTimefoldBeanProvider_ProducerMethod_solverManager_T5OWUir0G727pboehAmaToghWS8_Bean.doCreate(Unknown Source) at ai.timefold.solver.quarkus.bean.DefaultTimefoldBeanProvider_ProducerMethod_solverManager_T5OWUir0G727pboehAmaToghWS8_Bean.create(Unknown Source) at ai.timefold.solver.quarkus.bean.DefaultTimefoldBeanProvider_ProducerMethod_solverManager_T5OWUir0G727pboehAmaToghWS8_Bean.get(Unknown Source) at ai.timefold.solver.quarkus.bean.DefaultTimefoldBeanProvider_ProducerMethod_solverManager_T5OWUir0G727pboehAmaToghWS8_Bean.get(Unknown Source) at org.acme.vehiclerouting.rest.VehicleRoutePlanResource_Bean.doCreate(Unknown Source) at org.acme.vehiclerouting.rest.VehicleRoutePlanResource_Bean.create(Unknown Source) at org.acme.vehiclerouting.rest.VehicleRoutePlanResource_Bean.create(Unknown Source) at io.quarkus.arc.impl.AbstractSharedContext.createInstanceHandle(AbstractSharedContext.java:119) at io.quarkus.arc.impl.AbstractSharedContext$1.get(AbstractSharedContext.java:38) at io.quarkus.arc.impl.AbstractSharedContext$1.get(AbstractSharedContext.java:35) at io.quarkus.arc.impl.LazyValue.get(LazyValue.java:32) at io.quarkus.arc.impl.ComputingCache.computeIfAbsent(ComputingCache.java:69) at io.quarkus.arc.impl.ComputingCacheContextInstances.computeIfAbsent(ComputingCacheContextInstances.java:19) at io.quarkus.arc.impl.AbstractSharedContext.get(AbstractSharedContext.java:35) at org.acme.vehiclerouting.rest.VehicleRoutePlanResource_Bean.get(Unknown Source) at org.acme.vehiclerouting.rest.VehicleRoutePlanResource_Bean.get(Unknown Source) at io.quarkus.arc.impl.ArcContainerImpl.beanInstanceHandle(ArcContainerImpl.java:559) at io.quarkus.arc.impl.ArcContainerImpl.beanInstanceHandle(ArcContainerImpl.java:539) at io.quarkus.arc.impl.ArcContainerImpl.beanInstanceHandle(ArcContainerImpl.java:572) at io.quarkus.arc.impl.ArcContainerImpl$3.get(ArcContainerImpl.java:331) at io.quarkus.arc.impl.ArcContainerImpl$3.get(ArcContainerImpl.java:328) at io.quarkus.resteasy.common.runtime.QuarkusConstructorInjectoruct(QuarkusConstructorInjector.java:52) at org.jboss.resteasy.plugins.server.resourcefactory.POJOResourceFactory.createResource(POJOResourceFactory.java:64) at org.jboss.resteasy.core.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:349) at org.jboss.resteasy.core.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:70) at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:429) ... 16 more

To Reproduce Go to branch https://github.com/simontiffert/timefold-quickstarts/tree/chainedMultiVariable and run it (start planning in the UI) with Timefold 1.11.0 (default) or Timefold 1.7.0. There is one additional planning variable count in the class Visit. Please note that the branch is just used to show case the error and is not meant to be a full working example.

Environment

Timefold Solver Version or Git ref: 1.11.0 (but can reproduce in 1.8.0 as well)

Output of java -version: openjdk version "21.0.1" 2023-10-17 OpenJDK Runtime Environment (build 21.0.1+12-29) OpenJDK 64-Bit Server VM (build 21.0.1+12-29, mixed mode, sharing)

triceo commented 3 months ago

@simontiffert Thanks for reporting! The behavior is correct; we can not auto-configure the selectors in cases with more than one planning variable. I'm not exactly sure why it only started happening recently for you; as far as I know, this has always been the case.

Also, your report mentions 1.7.0 in places, and 1.8.0 in others. Which is the first release where you see this break? If 1.7.0, it is even stranger; that was a small release, and we didn't really touch anything that could do this. 1.8.0, on the other hand, was large and we were touching related code.

I'm assuming from the quickstart that you're not using Enterprise edition, and therefore not using Nearby selection?

(Side note: any reason why you're using chains as opposed to list variable? Chains will one day be deprecated. I'm assuming it's precisely because you need two vars.)

simontiffert commented 3 months ago

@triceo It is working with releases <= 1.7.0 and starts to throw the error message starting with version >= 1.8.0.

Yes, you are right, I have multiple planning variables which is not supported for list variables. Next to the assignment of visits to a "vehicle" - the duration of my visits is flexible, but there is a constraint for the overall duration of visits of a specific kind per week.

What would be the work around? Defining the selectors manually in the configuration?

triceo commented 3 months ago

What would be the work around? Defining the selectors manually in the configuration?

Exactly. As far as I know, we've always required it for situations with more than 1 variable. For example, the documentation for swap moves shows how to configure the move for two variables.

When time permits, we'll look into why it started happening suddenly in this case.

triceo commented 3 months ago

Hypothesis: it's happening because we've added TailChainSwapMove to the list of default moves. Assuming it wasn't there before, this could explain the sudden change.

To prove or disprove, configure your move selectors to only include change and swap, without any additional selector configuration. (So, do not rely on the default config.) If it works without an exception, then the hypothesis is correct; it's failing now, because it has one more default move now, and out of the box that move doesn't like two variables.

simontiffert commented 3 months ago

@triceo The manual configuration seems to work:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<solver xmlns="https://timefold.ai/xsd/solver">
    <solutionClass>org.acme.vehiclerouting.domain.VehicleRoutePlan</solutionClass>
    <entityClass>org.acme.vehiclerouting.domain.Visit</entityClass>
    <entityClass>org.acme.vehiclerouting.domain.Standstill</entityClass>
    <constructionHeuristic/>
    <localSearch>
        <unionMoveSelector>
            <changeMoveSelector/>
            <swapMoveSelector/>
            <tailChainSwapMoveSelector>
                <entitySelector>
                    <entityClass>org.acme.vehiclerouting.domain.Visit</entityClass>
                </entitySelector>
                <valueSelector variableName="previousVisit">
                </valueSelector>
            </tailChainSwapMoveSelector>
        </unionMoveSelector>
    </localSearch>
</solver>
triceo commented 3 months ago

This confirms my hypothesis that the problem is caused by us introducing tailChainSwapMove without realizing it will blow up in situations with more than 1 variable.