3

I'm trying to solve vehicle routing where each customer has multiple locations, of which exactly one needs to be visited. I obtained optaplanner-master and modified the vehiclerouting example in the following manner:

Customer.java:

/*
 * Copyright 2012 Red Hat, Inc. and/or its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.optaplanner.examples.vehiclerouting.domain;

import com.thoughtworks.xstream.annotations.XStreamAlias;
import com.thoughtworks.xstream.annotations.XStreamInclude;
import org.optaplanner.core.api.domain.entity.PlanningEntity;
import org.optaplanner.core.api.domain.valuerange.CountableValueRange;
import org.optaplanner.core.api.domain.valuerange.ValueRangeFactory;
import org.optaplanner.core.api.domain.valuerange.ValueRangeProvider;
import org.optaplanner.core.api.domain.variable.AnchorShadowVariable;
import org.optaplanner.core.api.domain.variable.PlanningVariable;
import org.optaplanner.core.api.domain.variable.PlanningVariableGraphType;
import org.optaplanner.examples.common.domain.AbstractPersistable;
import org.optaplanner.examples.vehiclerouting.domain.location.Location;
import org.optaplanner.examples.vehiclerouting.domain.solver.DepotAngleCustomerDifficultyWeightFactory;
import org.optaplanner.examples.vehiclerouting.domain.timewindowed.TimeWindowedCustomer;

import java.util.List;

@PlanningEntity(difficultyWeightFactoryClass = DepotAngleCustomerDifficultyWeightFactory.class)
@XStreamAlias("VrpCustomer")
@XStreamInclude({
        TimeWindowedCustomer.class
})
public class Customer extends AbstractPersistable implements Standstill {

    protected int demand;

    // Planning variables: changes during planning, between score calculations.
    protected Standstill previousStandstill;

    // Shadow variables
    protected Customer nextCustomer;
    protected Vehicle vehicle;

    protected List<Location> locations;
    protected Integer selectedLocation = 0;

    public void setSelectedLocation(Integer selectedLocation){
        this.selectedLocation = selectedLocation;
    }

    @PlanningVariable(valueRangeProviderRefs = {"selectedLocation"})
    public Integer getSelectedLocation(){
        return selectedLocation;
    }

    @ValueRangeProvider(id = "selectedLocation")
    public CountableValueRange<Integer> getSelectableLocations(){
        return ValueRangeFactory.createIntValueRange(0, locations.size());
    }

    public void setLocations(List<Location> locations) {
        this.locations = locations;
    }

    public List<Location> getLocations(){
        return locations;
    }

    @Override
    public Location getLocation() {
        return locations.get(selectedLocation);
    }

    @Override
    public String toString() {
        return "Customer " + getId();
    }


    public int getDemand() {
        return demand;
    }

    public void setDemand(int demand) {
        this.demand = demand;
    }

    @PlanningVariable(valueRangeProviderRefs = {"vehicleRange", "customerRange"},
            graphType = PlanningVariableGraphType.CHAINED)
    public Standstill getPreviousStandstill() {
        return previousStandstill;
    }

    public void setPreviousStandstill(Standstill previousStandstill) {
        this.previousStandstill = previousStandstill;
    }

    @Override
    public Customer getNextCustomer() {
        return nextCustomer;
    }

    @Override
    public void setNextCustomer(Customer nextCustomer) {
        this.nextCustomer = nextCustomer;
    }

    @Override
    @AnchorShadowVariable(sourceVariableName = "previousStandstill")
    public Vehicle getVehicle() {
        return vehicle;
    }

    public void setVehicle(Vehicle vehicle) {
        this.vehicle = vehicle;
    }

    // ************************************************************************
    // Complex methods
    // ************************************************************************

    /**
     * @return a positive number, the distance multiplied by 1000 to avoid floating point arithmetic rounding errors
     */
    public long getDistanceFromPreviousStandstill() {
        if (previousStandstill == null) {
            throw new IllegalStateException("This method must not be called when the previousStandstill ("
                    + previousStandstill + ") is not initialized yet.");
        }
        return getDistanceFrom(previousStandstill);
    }

    /**
     * @param standstill never null
     * @return a positive number, the distance multiplied by 1000 to avoid floating point arithmetic rounding errors
     */
    public long getDistanceFrom(Standstill standstill) {
        return standstill.getLocation().getDistanceTo(getLocation());
    }

    /**
     * @param standstill never null
     * @return a positive number, the distance multiplied by 1000 to avoid floating point arithmetic rounding errors
     */
    public long getDistanceTo(Standstill standstill) {
        return getLocation().getDistanceTo(standstill.getLocation());
    }


}

In VehicleRoutingImporter.java, the code that reads the customer's location has changed to:

List<Location> locations = new ArrayList<>(lineTokens.length-2);
for (int j = 2; j < lineTokens.length; ++j){
    long locationId = Long.parseLong(lineTokens[j]);
    Location location = locationMap.get(locationId);
    if (location == null)
        throw new IllegalArgumentException("Missing location (id=" + locationId + ") specified for customer id=" + id);
        locations.add(location)
}
customer.setLocations(locations);

All other code that used to call Customer.setLocation(location) now calls Customer.setLocations(Collections.singletonList(location)). I don't think any of it actually gets called.

In vehicleRoutingSolverConfig.xml, I removed these lines (otherwise, it complains about multiple variables):

  <subChainChangeMoveSelector>
    <selectReversingMoveToo>true</selectReversingMoveToo>
  </subChainChangeMoveSelector>
  <subChainSwapMoveSelector>
    <selectReversingMoveToo>true</selectReversingMoveToo>
  </subChainSwapMoveSelector>

tutorial-01-uncapacitated.vrp is now:

NAME : tutorial-01-uncapacitated
COMMENT : Geoffrey De Smet - OptaPlanner VRP demo 01
TYPE : CVRP
DIMENSION : 8
EDGE_WEIGHT_TYPE : EUC_2D 
CAPACITY : 100
NODE_COORD_SECTION 
 1 50 50
 2 45 100
 3 30 80
 4 70 85
 5 60 60
 6 35 10
 7 30 30
 8 45 20
DEMAND_SECTION
1 0
2 1 2 3
3 1 3 4
4 1 4 5
5 1 5 6
6 1 6 7
7 1 7 8
8 1 8 2
DEPOT_SECTION
 1  
 -1  
VEHICLES : 2
EOF

It appears to work, and finds the optimal solution in small test cases (and, what looks like a reasonable solution in large cases). However when I set FAST_ASSERT mode, it fails with the following error:

Caused by: java.lang.IllegalStateException: Impossible VariableListener corruption: the expectedWorkingScore (0hard/-30000soft) is not the workingScore (0hard/-34142soft) after all VariableListeners were triggered without changes to the genuine variables. But all the shadow variable values are still the same, so this is impossible.

msasha
  • 247
  • 2
  • 9
  • When I use VehicleRoutingEasyScoreCalculator instead of the drl, it works without error. – msasha Aug 20 '17 at 13:53
  • Can you comment out one score rule in the DRL at a time to find out which score rule is causing this problem and then copy paste it in this question? (Even if you can fix it then, I need to be able to reproduce to fix the error message.) In any case, the error message is misleading and should include a corruption analysis, so I created [this issue](https://issues.jboss.org/browse/PLANNER-866). – Geoffrey De Smet Aug 21 '17 at 06:47
  • There are only two rules in my DRL - the hard constraint for capacity, and the soft constraint for distance. When I remove the soft constraint, it doesn't return this error. Here is the DRL rule: rule "distanceToPreviousStandstill" when $customer : Customer(previousStandstill != null, $distanceFromPreviousStandstill : distanceFromPreviousStandstill) then scoreHolder.addSoftConstraintMatch(kcontext, - $distanceFromPreviousStandstill); end – msasha Aug 22 '17 at 10:08
  • Can you also add the implementation of `Customer.getDistanceFromPreviousStandstill()`? – Geoffrey De Smet Aug 22 '17 at 10:46
  • Added, but it's the same as in the example. – msasha Aug 22 '17 at 12:51
  • I see nothing that can explain your score corruption. There's nothing in that rule that can explain that the score is originally calculated at -34142soft and then changes to -30000soft when all variables are still the same. Everything looks good. The only step forward is to have a corruption analysis in that error message. – Geoffrey De Smet Aug 22 '17 at 15:14
  • Try with `7.3.0-SNAPSHOT` tomorrow (I just pushed some changes for a better error message as part of https://issues.jboss.org/browse/PLANNER-866). – Geoffrey De Smet Aug 23 '17 at 09:18
  • How do I download that? I can only seem to find 7.2 final and snapshots. – msasha Aug 27 '17 at 11:58
  • 1
    Never mind, I got optaplanner-master from github, which seems to have your commit too. The error I now get is: ''Caused by: java.lang.IllegalStateException: Impossible VariableListener corruption: the expectedWorkingScore (0hard/-170000soft) is not the workingScore (0hard/-172361soft) after all VariableListeners were triggered without changes to the genuine variables after completedAction (Customer 2 {1 -> 1}). But all the shadow variable values are still the same, so this is impossible. '' – msasha Aug 27 '17 at 12:17
  • `Customer 2 {1 -> 1}` that looks like a change move that moves entity Customer 2 from previous 1 to previous 1. Normally, that change move should be undoable (and therefore filtered out earlier). The reason that's not considered undoable is probably because those two 1's are different instances. Prove that through debugging ChainedChangeMove.isDoable(). You might have multiple things with the same id in your dataset xor the planning cloning might be wrong (but you don't have any custom cloning, right?). – Geoffrey De Smet Aug 29 '17 at 07:30
  • I do have multiple objects with the same id, but of different types (for example, I have a Location with id 1 and a Customer with id 1). I just changed that and gave every entity a different id, but that didn't help. Also, commenting out the subChainChangeMoveSelector and subChainSwapMoveSelector didn't help. I'm not sure how to debug ChainedChangeMove.isDoable() or what custom cloning is. – msasha Aug 29 '17 at 12:02
  • @GeoffreyDeSmet I've edited the post to contain the full set of changes relative to optaplanner-master. You can apply them yourself and hopefully reproduce the issue. – msasha Aug 29 '17 at 13:01
  • @GeoffreyDeSmet ping – msasha Sep 14 '17 at 10:43
  • can you make a PR with the full set of changes relative to optaplanner master? – Geoffrey De Smet Sep 15 '17 at 06:44
  • 1
    Created: https://github.com/kiegroup/optaplanner/pull/338 – msasha Sep 17 '17 at 12:29

1 Answers1

1

Turn on FULL_ASSERT instead of FAST_ASSERT to fail faster pointing to the real problem. See the PR comment that explains how that helps detects the problem.

Geoffrey De Smet
  • 26,223
  • 11
  • 73
  • 120