Skip to content

Commit

Permalink
PLANNER-1254 UnimprovedTimeMillisSpentScoreDifferenceThresholdTermina…
Browse files Browse the repository at this point in the history
…tion
  • Loading branch information
ge0ffrey committed Sep 20, 2018
1 parent 28ce90d commit 64f89cc
Show file tree
Hide file tree
Showing 3 changed files with 199 additions and 2 deletions.
Expand Up @@ -37,6 +37,7 @@
import org.optaplanner.core.impl.solver.termination.Termination;
import org.optaplanner.core.impl.solver.termination.TimeMillisSpentTermination;
import org.optaplanner.core.impl.solver.termination.UnimprovedStepCountTermination;
import org.optaplanner.core.impl.solver.termination.UnimprovedTimeMillisSpentScoreDifferenceThresholdTermination;
import org.optaplanner.core.impl.solver.termination.UnimprovedTimeMillisSpentTermination;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -55,11 +56,13 @@ public class TerminationConfig extends AbstractConfig<TerminationConfig> {
private Long minutesSpentLimit = null;
private Long hoursSpentLimit = null;
private Long daysSpentLimit = null;

private Long unimprovedMillisecondsSpentLimit = null;
private Long unimprovedSecondsSpentLimit = null;
private Long unimprovedMinutesSpentLimit = null;
private Long unimprovedHoursSpentLimit = null;
private Long unimprovedDaysSpentLimit = null;
private String unimprovedScoreDifferenceThreshold = null;

private String bestScoreLimit = null;
private Boolean bestScoreFeasible = null;
Expand Down Expand Up @@ -173,6 +176,14 @@ public void setUnimprovedDaysSpentLimit(Long unimprovedDaysSpentLimit) {
this.unimprovedDaysSpentLimit = unimprovedDaysSpentLimit;
}

public String getUnimprovedScoreDifferenceThreshold() {
return unimprovedScoreDifferenceThreshold;
}

public void setUnimprovedScoreDifferenceThreshold(String unimprovedScoreDifferenceThreshold) {
this.unimprovedScoreDifferenceThreshold = unimprovedScoreDifferenceThreshold;
}

public String getBestScoreLimit() {
return bestScoreLimit;
}
Expand Down Expand Up @@ -301,6 +312,11 @@ public TerminationConfig withUnimprovedDaysSpentLimit(Long unimprovedDaysSpentLi
return this;
}

public TerminationConfig withUnimprovedScoreDifferenceThreshold(String unimprovedScoreDifferenceThreshold) {
this.unimprovedScoreDifferenceThreshold = unimprovedScoreDifferenceThreshold;
return this;
}

public TerminationConfig withBestScoreLimit(String bestScoreLimit) {
this.bestScoreLimit = bestScoreLimit;
return this;
Expand Down Expand Up @@ -359,11 +375,26 @@ public Termination buildTermination(HeuristicConfigPolicy configPolicy) {
}
Long unimprovedTimeMillisSpentLimit = calculateUnimprovedTimeMillisSpentLimit();
if (unimprovedTimeMillisSpentLimit != null) {
terminationList.add(new UnimprovedTimeMillisSpentTermination(unimprovedTimeMillisSpentLimit));
if (unimprovedScoreDifferenceThreshold == null) {
terminationList.add(new UnimprovedTimeMillisSpentTermination(unimprovedTimeMillisSpentLimit));
} else {
ScoreDefinition scoreDefinition = configPolicy.getScoreDefinition();
Score unimprovedScoreDifferenceThreshold_ = scoreDefinition.parseScore(unimprovedScoreDifferenceThreshold);
if (unimprovedScoreDifferenceThreshold_.compareTo(scoreDefinition.getZeroScore()) <= 0) {
throw new IllegalStateException("The unimprovedScoreDifferenceThreshold ("
+ unimprovedScoreDifferenceThreshold + ") must be positive.");

}
terminationList.add(new UnimprovedTimeMillisSpentScoreDifferenceThresholdTermination(unimprovedTimeMillisSpentLimit, unimprovedScoreDifferenceThreshold_));
}
} else if (unimprovedScoreDifferenceThreshold != null) {
throw new IllegalStateException("The unimprovedScoreDifferenceThreshold ("
+ unimprovedScoreDifferenceThreshold + ") can only be used if an unimproved*SpentLimit ("
+ unimprovedTimeMillisSpentLimit + ") is used too.");
}
if (bestScoreLimit != null) {
Score bestScoreLimit_ = configPolicy.getScoreDefinition().parseScore(bestScoreLimit);
ScoreDefinition scoreDefinition = configPolicy.getScoreDefinition();
Score bestScoreLimit_ = scoreDefinition.parseScore(bestScoreLimit);
double[] timeGradientWeightNumbers = new double[scoreDefinition.getLevelsSize() - 1];
for (int i = 0; i < timeGradientWeightNumbers.length; i++) {
timeGradientWeightNumbers[i] = 0.50; // Number pulled out of thin air
Expand Down Expand Up @@ -559,6 +590,8 @@ public void inherit(TerminationConfig inheritedConfig) {
inheritedConfig.getUnimprovedHoursSpentLimit());
unimprovedDaysSpentLimit = ConfigUtils.inheritOverwritableProperty(unimprovedDaysSpentLimit,
inheritedConfig.getUnimprovedDaysSpentLimit());
unimprovedScoreDifferenceThreshold = ConfigUtils.inheritOverwritableProperty(unimprovedScoreDifferenceThreshold,
inheritedConfig.getUnimprovedScoreDifferenceThreshold());
bestScoreLimit = ConfigUtils.inheritOverwritableProperty(bestScoreLimit,
inheritedConfig.getBestScoreLimit());
bestScoreFeasible = ConfigUtils.inheritOverwritableProperty(bestScoreFeasible,
Expand Down
Expand Up @@ -111,6 +111,8 @@ private void doStep(ConstructionHeuristicStepScope<Solution_> stepScope) {
// Causes a planning clone, which is expensive
// For example, on cloud balancing 1200c-4800p this reduces performance by 18%
bestSolutionRecaller.processWorkingSolutionDuringStep(stepScope);
} else {
stepScope.setBestScoreImproved(true);
}
}

Expand Down
@@ -0,0 +1,162 @@
/*
* Copyright 2018 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.core.impl.solver.termination;

import java.util.ArrayDeque;
import java.util.Iterator;
import java.util.Queue;

import org.apache.commons.lang3.tuple.Pair;
import org.optaplanner.core.api.score.Score;
import org.optaplanner.core.impl.phase.scope.AbstractPhaseScope;
import org.optaplanner.core.impl.phase.scope.AbstractStepScope;
import org.optaplanner.core.impl.solver.ChildThreadType;
import org.optaplanner.core.impl.solver.scope.DefaultSolverScope;

public class UnimprovedTimeMillisSpentScoreDifferenceThresholdTermination extends AbstractTermination {

private final long unimprovedTimeMillisSpentLimit;
private final Score unimprovedScoreDifferenceThreshold;

private Queue<Pair<Long, Score>> bestScoreImprovementHistoryQueue;
// safeTimeMillis is until when we're safe from termination
private long solverSafeTimeMillis = -1L;
private long phaseSafeTimeMillis = -1L;

public UnimprovedTimeMillisSpentScoreDifferenceThresholdTermination(long unimprovedTimeMillisSpentLimit,
Score unimprovedScoreDifferenceThreshold) {
this.unimprovedTimeMillisSpentLimit = unimprovedTimeMillisSpentLimit;
this.unimprovedScoreDifferenceThreshold = unimprovedScoreDifferenceThreshold;
if (unimprovedTimeMillisSpentLimit <= 0L) {
throw new IllegalArgumentException("The unimprovedTimeMillisSpentLimit (" + unimprovedTimeMillisSpentLimit
+ ") cannot be negative.");
}
}

public long getUnimprovedTimeMillisSpentLimit() {
return unimprovedTimeMillisSpentLimit;
}

public Score getUnimprovedScoreDifferenceThreshold() {
return unimprovedScoreDifferenceThreshold;
}

@Override
public void solvingStarted(DefaultSolverScope solverScope) {
bestScoreImprovementHistoryQueue = new ArrayDeque<>();
solverSafeTimeMillis = solverScope.getBestSolutionTimeMillis() + unimprovedTimeMillisSpentLimit;
}

@Override
public void solvingEnded(DefaultSolverScope solverScope) {
bestScoreImprovementHistoryQueue = null;
solverSafeTimeMillis = -1L;
}

@Override
public void phaseStarted(AbstractPhaseScope phaseScope) {
phaseSafeTimeMillis = phaseScope.getStartingSystemTimeMillis() + unimprovedTimeMillisSpentLimit;
}

@Override
public void phaseEnded(AbstractPhaseScope phaseScope) {
phaseSafeTimeMillis = -1L;
}

@Override
public void stepEnded(AbstractStepScope stepScope) {
if (stepScope.getBestScoreImproved()) {
DefaultSolverScope solverScope = stepScope.getPhaseScope().getSolverScope();
long bestSolutionTimeMillis = solverScope.getBestSolutionTimeMillis();
Score bestScore = solverScope.getBestScore();
for (Iterator<Pair<Long, Score>> it = bestScoreImprovementHistoryQueue.iterator(); it.hasNext(); ) {
Pair<Long, Score> bestScoreImprovement = it.next();
Score scoreDifference = bestScore.subtract(bestScoreImprovement.getValue());
if (scoreDifference.compareTo(unimprovedScoreDifferenceThreshold) >= 0) {
it.remove();
long safeTimeMillis = bestScoreImprovement.getKey() + unimprovedTimeMillisSpentLimit;
solverSafeTimeMillis = safeTimeMillis;
phaseSafeTimeMillis = safeTimeMillis;
} else {
break;
}
}
bestScoreImprovementHistoryQueue.add(Pair.of(bestSolutionTimeMillis, bestScore));
}
}

// ************************************************************************
// Terminated methods
// ************************************************************************

@Override
public boolean isSolverTerminated(DefaultSolverScope solverScope) {
return isTerminated(solverSafeTimeMillis);
}

@Override
public boolean isPhaseTerminated(AbstractPhaseScope phaseScope) {
return isTerminated(phaseSafeTimeMillis);
}

protected boolean isTerminated(long safeTimeMillis) {
// It's possible that there is already an improving move in the forager
// that will end up pushing the safeTimeMillis further
// but that doesn't change the fact that the best score didn't improve enough in the specified time interval.
// It just looks weird because it terminates even though the final step is a high enough score improvement.
long now = System.currentTimeMillis();
return now > safeTimeMillis;
}

// ************************************************************************
// Time gradient methods
// ************************************************************************

@Override
public double calculateSolverTimeGradient(DefaultSolverScope solverScope) {
return calculateTimeGradient(solverSafeTimeMillis);
}

@Override
public double calculatePhaseTimeGradient(AbstractPhaseScope phaseScope) {
return calculateTimeGradient(phaseSafeTimeMillis);
}

protected double calculateTimeGradient(long safeTimeMillis) {
long now = System.currentTimeMillis();
long unimprovedTimeMillisSpent = now - (safeTimeMillis - unimprovedTimeMillisSpentLimit);
double timeGradient = ((double) unimprovedTimeMillisSpent) / ((double) unimprovedTimeMillisSpentLimit);
return Math.min(timeGradient, 1.0);
}

// ************************************************************************
// Other methods
// ************************************************************************

@Override
public UnimprovedTimeMillisSpentScoreDifferenceThresholdTermination createChildThreadTermination(
DefaultSolverScope solverScope, ChildThreadType childThreadType) {
return new UnimprovedTimeMillisSpentScoreDifferenceThresholdTermination(
unimprovedTimeMillisSpentLimit, unimprovedScoreDifferenceThreshold);
}

@Override
public String toString() {
return "UnimprovedTimeMillisSpent(" + unimprovedTimeMillisSpentLimit + ")";
}

}

0 comments on commit 64f89cc

Please sign in to comment.