Skip to content

Commit

Permalink
feat: check LA diversity
Browse files Browse the repository at this point in the history
  • Loading branch information
zepfred committed Feb 26, 2025
1 parent 417fcff commit 87b1672
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
import java.util.HashMap;
import java.util.Map;

import ai.timefold.solver.core.api.score.Score;
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.RestartableAcceptor;
Expand All @@ -14,6 +16,13 @@

public class LateAcceptanceAcceptor<Solution_> extends RestartableAcceptor<Solution_> {

// Using a start window of 30 seconds and a geometric factor of 1.4,
// five restarts will result in 300 seconds without improvement.
protected static final int MAX_RESTART_WITHOUT_IMPROVEMENT = 4;
protected static final double MIN_DIVERSITY_RATIO = 0.05;
// The goal is to increase from hundreds to thousands in the first restart event and then increment it linearly
protected static final int SCALING_FACTOR = 10;

protected int lateAcceptanceSize = -1;
protected boolean hillClimbingEnabled = true;

Expand All @@ -22,10 +31,13 @@ public class LateAcceptanceAcceptor<Solution_> extends RestartableAcceptor<Solut

private int maxBestScoreSize;
private Score<?> currentBestScore;
// Keep track of the best scores accumulated so far. This list will be used to reseed the later elements list.
private Deque<Score<?>> bestScoreQueue;
protected static final int SCALING_FACTOR = 10;
// Map to count late elements and allow faster diversity calculation
private Map<String, MutableInt> lateElementCountMap;
protected int defaultLateAcceptanceSize;
protected int coefficient;
protected int countRestartWithoutImprovement;

public LateAcceptanceAcceptor(StuckCriterion<Solution_> stuckCriterionDetection) {
super(stuckCriterionDetection);
Expand Down Expand Up @@ -58,10 +70,14 @@ public void phaseStarted(LocalSearchPhaseScope<Solution_> phaseScope) {
Arrays.fill(previousScores, initialScore);
lateScoreIndex = 0;
coefficient = 0;
countRestartWithoutImprovement = 0;
defaultLateAcceptanceSize = lateAcceptanceSize;
// The maximum size is three times the size of the initial element list
maxBestScoreSize = defaultLateAcceptanceSize * 3 * SCALING_FACTOR;
bestScoreQueue = new ArrayDeque<>(maxBestScoreSize);
bestScoreQueue.addLast(initialScore);
lateElementCountMap = new HashMap<>();
lateElementCountMap.put(initialScore.toString(), new MutableInt(lateAcceptanceSize));
}

private void validate() {
Expand Down Expand Up @@ -90,37 +106,101 @@ public boolean accept(LocalSearchMoveScope<Solution_> moveScope) {
@SuppressWarnings({ "rawtypes", "unchecked" })
public void stepEnded(LocalSearchStepScope<Solution_> stepScope) {
super.stepEnded(stepScope);
previousScores[lateScoreIndex] = stepScope.getScore();
lateScoreIndex = (lateScoreIndex + 1) % lateAcceptanceSize;
updateLateElement(stepScope);
if (((Score) currentBestScore).compareTo(stepScope.getScore()) < 0) {
if (countRestartWithoutImprovement > 0 && coefficient > 0) {
// We decrease the coefficient
// if there is an improvement
// to avoid altering the late element size in the next restart event.
// This action is performed only once after a restart event
coefficient--;
}
countRestartWithoutImprovement = 0;
if (bestScoreQueue.size() < maxBestScoreSize) {
bestScoreQueue.addLast(stepScope.getScore());
} else {
// // When the collection is full, we remove the lowest score and add the new best value.
bestScoreQueue.poll();
bestScoreQueue.addLast(stepScope.getScore());
}
}
}

private void updateLateElement(LocalSearchStepScope<Solution_> stepScope) {
var previousScoreStr = previousScores[lateScoreIndex].toString();
var count = lateElementCountMap.computeIfPresent(previousScoreStr, ((s, c) -> {
var value = c.decrement();
if (value == 0) {
return null;
}
return c;
}));
if (count == null) {
lateElementCountMap.remove(previousScoreStr);
}
var stepScoreStr = stepScope.getScore().toString();
lateElementCountMap.compute(stepScoreStr, (s, c) -> {
if (c == null) {
return new MutableInt(1);
}
c.increment();
return c;
});
previousScores[lateScoreIndex] = stepScope.getScore();
lateScoreIndex = (lateScoreIndex + 1) % lateAcceptanceSize;
}

@Override
public void restart(LocalSearchStepScope<Solution_> stepScope) {
countRestartWithoutImprovement++;
var diversity = lateElementCountMap.size() == 1 ? 0 : lateElementCountMap.size() / (double) lateAcceptanceSize;
if (countRestartWithoutImprovement <= MAX_RESTART_WITHOUT_IMPROVEMENT && diversity > MIN_DIVERSITY_RATIO) {
// We prefer not to restart until an initial time window of at least 5 minutes has passed.
// We have observed that this approach works better for more complex datasets.
// However, when the diversity is zero, it indicates that the LA may be stuck in a local minimum,
// and in such cases, we should restart before the initial five minutes.
logger.info("Restart event delayed. Diversity ({}), Distinct Elements ({}), Restart without Improvement ({})",
diversity, lateElementCountMap.size(), countRestartWithoutImprovement);
return;
}
coefficient++;
var newLateAcceptanceSize = defaultLateAcceptanceSize * coefficient * SCALING_FACTOR;
rebuildLateElementsList(newLateAcceptanceSize);
}

/**
* The method recreates the late elements list with the new size {@code newLateAcceptanceSize}.
* The aim is to recreate elements using previous best scores.
* This method provides diversity
* while avoiding the initialization phase
* that occurs when the later elements are set to the most recent best score.
* <p>
* The approach is based on the work:
* Parameter-less Late Acceptance Hill-climbing: Foundations & Applications by Mosab Bazargani.
*/
private void rebuildLateElementsList(int newLateAcceptanceSize) {
var newPreviousScores = new Score[newLateAcceptanceSize];
if (logger.isInfoEnabled()) {
logger.info("Changing the lateAcceptanceSize from %d to %d.".formatted(lateAcceptanceSize, newLateAcceptanceSize));
var diversity = lateElementCountMap.size() == 1 ? 0 : lateElementCountMap.size() / (double) lateAcceptanceSize;
if (lateAcceptanceSize == newLateAcceptanceSize) {
logger.info("Keeping the lateAcceptanceSize as {}. Diversity ({})", lateAcceptanceSize, diversity);
} else {
logger.info(
"Changing the lateAcceptanceSize from {} to {}. Diversity ({})", lateAcceptanceSize,
newLateAcceptanceSize, diversity);
}
}
var countPerScore = newLateAcceptanceSize / bestScoreQueue.size() + 1;
var count = new MutableInt(newLateAcceptanceSize - 1);
var iterator = bestScoreQueue.descendingIterator();
lateElementCountMap.clear();
while (count.intValue() >= 0 && iterator.hasNext()) {
var score = iterator.next();
var scoreCount = new MutableInt(0);
lateElementCountMap.put(score.toString(), scoreCount);
for (var i = 0; i < countPerScore; i++) {
newPreviousScores[count.intValue()] = score;
scoreCount.increment();
count.decrement();
if (count.intValue() < 0) {
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ public boolean isSolverStuck(LocalSearchMoveScope<Solution_> moveScope) {
var triggered = evaluateCriterion(moveScope);
if (triggered) {
logger.info(
"Restart triggered with geometric factor ({}), scaling factor of ({}), nextRestart ({}), best score ({})",
currentGeometricGrowFactor, scalingFactor, nextRestart,
"Restart triggered with geometric factor ({}), scaling factor of ({}), best score ({})",
currentGeometricGrowFactor, scalingFactor,
moveScope.getStepScope().getPhaseScope().getBestScore());
currentGeometricGrowFactor = Math.ceil(currentGeometricGrowFactor * GEOMETRIC_FACTOR);
nextRestart = calculateNextRestart();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

public class DiminishedReturnsStuckCriterion<Solution_, Score_ extends Score<Score_>>
extends AbstractGeometricStuckCriterion<Solution_> {
protected static final long TIME_WINDOW_MILLIS = 300_000;
protected static final long TIME_WINDOW_MILLIS = 30_000;
private static final double MINIMAL_IMPROVEMENT = 0.0001;

private DiminishedReturnsTermination<Solution_, Score_> diminishedReturnsCriterion;
Expand Down

0 comments on commit 87b1672

Please sign in to comment.