/*
* Copyright 2010 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.localsearch.decider.acceptor.tabu;
import java.util.ArrayDeque;
import java.util.Collection;
import java.util.Deque;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.optaplanner.core.impl.localsearch.decider.acceptor.AbstractAcceptor;
import org.optaplanner.core.impl.localsearch.decider.acceptor.Acceptor;
import org.optaplanner.core.impl.localsearch.decider.acceptor.tabu.size.TabuSizeStrategy;
import org.optaplanner.core.impl.localsearch.scope.LocalSearchMoveScope;
import org.optaplanner.core.impl.localsearch.scope.LocalSearchPhaseScope;
import org.optaplanner.core.impl.localsearch.scope.LocalSearchStepScope;
/**
* Abstract superclass for all Tabu Acceptors.
* @see Acceptor
*/
public abstract class AbstractTabuAcceptor extends AbstractAcceptor {
protected final String logIndentation;
protected TabuSizeStrategy tabuSizeStrategy = null;
protected TabuSizeStrategy fadingTabuSizeStrategy = null;
protected boolean aspirationEnabled = true;
protected boolean assertTabuHashCodeCorrectness = false;
protected Map<Object, Integer> tabuToStepIndexMap;
protected Deque<Object> tabuSequenceDeque;
protected int workingTabuSize = -1;
protected int workingFadingTabuSize = -1;
public AbstractTabuAcceptor(String logIndentation) {
this.logIndentation = logIndentation;
}
public void setTabuSizeStrategy(TabuSizeStrategy tabuSizeStrategy) {
this.tabuSizeStrategy = tabuSizeStrategy;
}
public void setFadingTabuSizeStrategy(TabuSizeStrategy fadingTabuSizeStrategy) {
this.fadingTabuSizeStrategy = fadingTabuSizeStrategy;
}
public void setAspirationEnabled(boolean aspirationEnabled) {
this.aspirationEnabled = aspirationEnabled;
}
public void setAssertTabuHashCodeCorrectness(boolean assertTabuHashCodeCorrectness) {
this.assertTabuHashCodeCorrectness = assertTabuHashCodeCorrectness;
}
// ************************************************************************
// Worker methods
// ************************************************************************
@Override
public void phaseStarted(LocalSearchPhaseScope phaseScope) {
super.phaseStarted(phaseScope);
LocalSearchStepScope lastCompletedStepScope = phaseScope.getLastCompletedStepScope();
// Tabu sizes do not change during stepStarted(), because they must be in sync with the tabuSequenceList.size()
workingTabuSize = tabuSizeStrategy == null ? 0 : tabuSizeStrategy.determineTabuSize(lastCompletedStepScope);
workingFadingTabuSize = fadingTabuSizeStrategy == null ? 0 : fadingTabuSizeStrategy.determineTabuSize(lastCompletedStepScope);
int totalTabuListSize = workingTabuSize + workingFadingTabuSize; // is at least 1
tabuToStepIndexMap = new HashMap<>(totalTabuListSize);
tabuSequenceDeque = new ArrayDeque<>();
}
@Override
public void phaseEnded(LocalSearchPhaseScope phaseScope) {
super.phaseEnded(phaseScope);
tabuToStepIndexMap = null;
tabuSequenceDeque = null;
workingTabuSize = -1;
workingFadingTabuSize = -1;
}
@Override
public void stepEnded(LocalSearchStepScope stepScope) {
super.stepEnded(stepScope);
// Tabu sizes do not change during stepStarted(), because they must be in sync with the tabuSequenceList.size()
workingTabuSize = tabuSizeStrategy == null ? 0 : tabuSizeStrategy.determineTabuSize(stepScope);
workingFadingTabuSize = fadingTabuSizeStrategy == null ? 0 : fadingTabuSizeStrategy.determineTabuSize(stepScope);
adjustTabuList(stepScope.getStepIndex(), findNewTabu(stepScope));
}
protected void adjustTabuList(int tabuStepIndex, Collection<? extends Object> tabus) {
int totalTabuListSize = workingTabuSize + workingFadingTabuSize; // is at least 1
// Remove the oldest tabu(s)
for (Iterator<Object> it = tabuSequenceDeque.iterator(); it.hasNext();) {
Object oldTabu = it.next();
Integer oldTabuStepIndexInteger = tabuToStepIndexMap.get(oldTabu);
if (oldTabuStepIndexInteger == null) {
throw new IllegalStateException("HashCode stability violation: the hashCode() of tabu ("
+ oldTabu + ") of class (" + oldTabu.getClass()
+ ") changed during planning, since it was inserted in the tabu Map or Set.");
}
int oldTabuStepCount = tabuStepIndex - oldTabuStepIndexInteger; // at least 1
if (oldTabuStepCount < totalTabuListSize) {
break;
}
it.remove();
tabuToStepIndexMap.remove(oldTabu);
}
// Add the new tabu(s)
for (Object tabu : tabus) {
// Push tabu to the end of the line
if (tabuToStepIndexMap.containsKey(tabu)) {
tabuToStepIndexMap.remove(tabu);
tabuSequenceDeque.remove(tabu);
}
tabuToStepIndexMap.put(tabu, tabuStepIndex);
tabuSequenceDeque.add(tabu);
}
}
@Override
public boolean isAccepted(LocalSearchMoveScope moveScope) {
int maximumTabuStepIndex = locateMaximumTabStepIndex(moveScope);
if (maximumTabuStepIndex < 0) {
// The move isn't tabu at all
return true;
}
if (aspirationEnabled) {
// Natural comparison because shifting penalties don't apply
if (moveScope.getScore().compareTo(
moveScope.getStepScope().getPhaseScope().getBestScore()) > 0) {
logger.trace("{} Proposed move ({}) is tabu, but is accepted anyway due to aspiration.",
logIndentation,
moveScope.getMove());
return true;
}
}
int tabuStepCount = moveScope.getStepScope().getStepIndex() - maximumTabuStepIndex; // at least 1
if (tabuStepCount <= workingTabuSize) {
logger.trace("{} Proposed move ({}) is tabu and is therefore not accepted.",
logIndentation, moveScope.getMove());
return false;
}
double acceptChance = calculateFadingTabuAcceptChance(tabuStepCount - workingTabuSize);
boolean accepted = moveScope.getWorkingRandom().nextDouble() < acceptChance;
if (accepted) {
logger.trace("{} Proposed move ({}) is fading tabu with acceptChance ({}) and is accepted.",
logIndentation,
moveScope.getMove(), acceptChance);
} else {
logger.trace("{} Proposed move ({}) is fading tabu with acceptChance ({}) and is not accepted.",
logIndentation,
moveScope.getMove(), acceptChance);
}
return accepted;
}
private int locateMaximumTabStepIndex(LocalSearchMoveScope moveScope) {
Collection<? extends Object> checkingTabus = findTabu(moveScope);
int maximumTabuStepIndex = -1;
for (Object checkingTabu : checkingTabus) {
Integer tabuStepIndexInteger = tabuToStepIndexMap.get(checkingTabu);
if (tabuStepIndexInteger != null) {
maximumTabuStepIndex = Math.max(tabuStepIndexInteger, maximumTabuStepIndex);
}
if (assertTabuHashCodeCorrectness) {
for (Object tabu : tabuSequenceDeque) {
// tabu and checkingTabu can be null with a nullable planning variable
if (tabu != null && tabu.equals(checkingTabu)) {
if (tabu.hashCode() != checkingTabu.hashCode()) {
throw new IllegalStateException("HashCode/equals contract violation: tabu (" + tabu
+ ") of class (" + tabu.getClass()
+ ") and checkingTabu (" + checkingTabu
+ ") are equals() but have a different hashCode().");
}
if (tabuStepIndexInteger == null) {
throw new IllegalStateException("HashCode stability violation: the hashCode() of tabu ("
+ tabu + ") of class (" + tabu.getClass()
+ ") changed during planning, since it was inserted in the tabu Map or Set.");
}
}
}
}
}
return maximumTabuStepIndex;
}
/**
* @param fadingTabuStepCount {@code 0 < fadingTabuStepCount <= fadingTabuSize}
* @return {@code 0.0 < acceptChance < 1.0}
*/
protected double calculateFadingTabuAcceptChance(int fadingTabuStepCount) {
// The + 1's are because acceptChance should not be 0.0 or 1.0
// when (fadingTabuStepCount == 0) or (fadingTabuStepCount + 1 == workingFadingTabuSize)
return ((double) (workingFadingTabuSize - fadingTabuStepCount)) / ((double) (workingFadingTabuSize + 1));
}
protected abstract Collection<? extends Object> findTabu(LocalSearchMoveScope moveScope);
protected abstract Collection<? extends Object> findNewTabu(LocalSearchStepScope stepScope);
}