diff --git a/jsprit-core/src/main/java/com/graphhopper/jsprit/core/algorithm/SearchStrategy.java b/jsprit-core/src/main/java/com/graphhopper/jsprit/core/algorithm/SearchStrategy.java index a68808e4..c95959ef 100644 --- a/jsprit-core/src/main/java/com/graphhopper/jsprit/core/algorithm/SearchStrategy.java +++ b/jsprit-core/src/main/java/com/graphhopper/jsprit/core/algorithm/SearchStrategy.java @@ -164,7 +164,5 @@ public class SearchStrategy { for (SearchStrategyModule module : searchStrategyModules) { module.addModuleListener(moduleListener); } - } - } diff --git a/jsprit-core/src/main/java/com/graphhopper/jsprit/core/algorithm/termination/IterationWithoutImprovementTermination.java b/jsprit-core/src/main/java/com/graphhopper/jsprit/core/algorithm/termination/IterationWithoutImprovementTermination.java index c2ee487d..bacf33e2 100644 --- a/jsprit-core/src/main/java/com/graphhopper/jsprit/core/algorithm/termination/IterationWithoutImprovementTermination.java +++ b/jsprit-core/src/main/java/com/graphhopper/jsprit/core/algorithm/termination/IterationWithoutImprovementTermination.java @@ -18,9 +18,13 @@ package com.graphhopper.jsprit.core.algorithm.termination; import com.graphhopper.jsprit.core.algorithm.SearchStrategy; +import com.graphhopper.jsprit.core.problem.solution.VehicleRoutingProblemSolution; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; +import java.util.List; + /** * Terminates algorithm prematurely based on iterations without any improvement (i.e. new solution acceptance). @@ -39,13 +43,28 @@ public class IterationWithoutImprovementTermination implements PrematureAlgorith private int iterationsWithoutImprovement = 0; + private List costs; + + private List unassignedJobsCount; + + private double bestCost = Double.MAX_VALUE; + + private double terminationByCostPercentage = 0.0; + /** * Constructs termination. * * @param noIterationsWithoutImprovement previous iterations without any improvement */ public IterationWithoutImprovementTermination(int noIterationsWithoutImprovement) { + this(noIterationsWithoutImprovement, 0.0); + } + + public IterationWithoutImprovementTermination(int noIterationsWithoutImprovement, double terminationByCostPercentage) { this.noIterationWithoutImprovement = noIterationsWithoutImprovement; + this.terminationByCostPercentage = terminationByCostPercentage; + costs = new ArrayList<>(); + unassignedJobsCount = new ArrayList<>(); log.debug("initialise " + this); } @@ -56,10 +75,39 @@ public class IterationWithoutImprovementTermination implements PrematureAlgorith @Override public boolean isPrematureBreak(SearchStrategy.DiscoveredSolution discoveredSolution) { - if (discoveredSolution.isAccepted()) iterationsWithoutImprovement = 0; - else iterationsWithoutImprovement++; + if(this.terminationByCostPercentage == 0.0) + return checkStrictTerminationCondition(discoveredSolution); + else + return checkPercentageTerminationCondition(discoveredSolution); + } + + private boolean checkStrictTerminationCondition(SearchStrategy.DiscoveredSolution discoveredSolution){ + // The original logic that is counting the number of iterations without any change + if (discoveredSolution.isAccepted()) + iterationsWithoutImprovement = 0; + else + iterationsWithoutImprovement++; return (iterationsWithoutImprovement > noIterationWithoutImprovement); } + private boolean checkPercentageTerminationCondition(SearchStrategy.DiscoveredSolution discoveredSolution){ + // The alternative logic that detects also very slow improvment + // On large tasks small improvments to the route may significantly increase the runtime + VehicleRoutingProblemSolution sol = discoveredSolution.getSolution(); + double currentCost = sol.getCost(); + bestCost = Math.min(currentCost, bestCost); + costs.add(bestCost); + + int currentJobsUnassigned = sol.getUnassignedJobs().size(); + unassignedJobsCount.add(currentJobsUnassigned); + + int i = costs.size() - 1; + if (i < noIterationWithoutImprovement) + return false; + + boolean unassignedJobsEqual = (currentJobsUnassigned == unassignedJobsCount.get(i - noIterationWithoutImprovement)); + boolean progressTooSlow = 100 * ((costs.get(i - noIterationWithoutImprovement) - currentCost) / currentCost) <= terminationByCostPercentage; + return (unassignedJobsEqual && progressTooSlow); + } } diff --git a/jsprit-core/src/main/java/com/graphhopper/jsprit/core/reporting/SolutionPrinter.java b/jsprit-core/src/main/java/com/graphhopper/jsprit/core/reporting/SolutionPrinter.java index 53690df2..7633eabd 100644 --- a/jsprit-core/src/main/java/com/graphhopper/jsprit/core/reporting/SolutionPrinter.java +++ b/jsprit-core/src/main/java/com/graphhopper/jsprit/core/reporting/SolutionPrinter.java @@ -131,7 +131,7 @@ public class SolutionPrinter { out.format("+---------------+------------------------------------------+%n"); out.printf("| indicator | value |%n"); out.format("+---------------+------------------------------------------+%n"); - out.format(leftAlignSolution, "costs", solution.getCost()); + out.format(leftAlignSolution, "costs", Math.round(solution.getCost())); out.format(leftAlignSolution, "noVehicles", solution.getRoutes().size()); out.format(leftAlignSolution, "unassgndJobs", solution.getUnassignedJobs().size()); out.format("+----------------------------------------------------------+%n"); diff --git a/jsprit-core/src/test/java/com/graphhopper/jsprit/core/algorithm/termination/IterationsWithoutImprovementTest.java b/jsprit-core/src/test/java/com/graphhopper/jsprit/core/algorithm/termination/IterationsWithoutImprovementTest.java index 8cdd708a..6a0bfb43 100644 --- a/jsprit-core/src/test/java/com/graphhopper/jsprit/core/algorithm/termination/IterationsWithoutImprovementTest.java +++ b/jsprit-core/src/test/java/com/graphhopper/jsprit/core/algorithm/termination/IterationsWithoutImprovementTest.java @@ -20,8 +20,13 @@ package com.graphhopper.jsprit.core.algorithm.termination; import com.graphhopper.jsprit.core.algorithm.SearchStrategy; -import junit.framework.Assert; +import com.graphhopper.jsprit.core.problem.job.Job; +import com.graphhopper.jsprit.core.problem.solution.VehicleRoutingProblemSolution; +import org.junit.Assert; import org.junit.Test; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -76,4 +81,73 @@ public class IterationsWithoutImprovementTest { } Assert.assertEquals(150, terminatedAfter); } + + @Test + public void isPrematureBreakZeroPercentage() { + int maxIterations = 200; + IterationWithoutImprovementTermination termination = new IterationWithoutImprovementTermination(100, 0); + SearchStrategy.DiscoveredSolution discoveredSolution = mock(SearchStrategy.DiscoveredSolution.class); + + int terminatedAfter = maxIterations; + for (int i = 0; i < maxIterations; i++) { + when(discoveredSolution.isAccepted()).thenReturn(i< 50 ? true : false); + if (termination.isPrematureBreak(discoveredSolution)) { + terminatedAfter = i; + break; + } + } + Assert.assertEquals(150, terminatedAfter); + } + + @Test + public void isPrematureBreakWithPercentageShouldBreak() { + int maxIterations = 200; + IterationWithoutImprovementTermination termination = new IterationWithoutImprovementTermination(10, 1.0); + SearchStrategy.DiscoveredSolution discoveredSolution = mock(SearchStrategy.DiscoveredSolution.class); + VehicleRoutingProblemSolution solution = mock(VehicleRoutingProblemSolution.class); + when(discoveredSolution.getSolution()).thenReturn(solution); + when(solution.getUnassignedJobs()).thenReturn(new ArrayList()); + + int terminatedAfter = maxIterations; + for (int i = 0; i < maxIterations; i++) { + + when(solution.getCost()).thenReturn(i < 100 ? 100.0 - (0.1*i) : 40-((i-100)*0.01)); + boolean terminate = termination.isPrematureBreak(discoveredSolution); + if (terminate) { + terminatedAfter = i; + break; + } + } + Assert.assertEquals(110, terminatedAfter); + } + + @Test + public void isPrematureBreakWithPercentageCostNotImprovedButUnassignedImproved() { + int maxIterations = 200; + IterationWithoutImprovementTermination termination = new IterationWithoutImprovementTermination(10, 1.0); + SearchStrategy.DiscoveredSolution discoveredSolution = mock(SearchStrategy.DiscoveredSolution.class); + VehicleRoutingProblemSolution solution = mock(VehicleRoutingProblemSolution.class); + Job job = mock(Job.class); + when(discoveredSolution.getSolution()).thenReturn(solution); + List unassignedJobs = new ArrayList<>(); + for (int i = 0; i < 100; i++) { + unassignedJobs.add(job); + } + + int terminatedAfter = maxIterations; + for (int i = 0; i < maxIterations; i++) { + when(solution.getCost()).thenReturn(100.0); + if (i <= 50){ + unassignedJobs.remove(0); + } + when(solution.getUnassignedJobs()).thenReturn(unassignedJobs); + boolean terminate = termination.isPrematureBreak(discoveredSolution); + if (terminate) { + terminatedAfter = i; + break; + } + } + Assert.assertEquals(60, terminatedAfter); + } + }