Skip to content

Commit

Permalink
Merge pull request #45 from hdbeukel/tabuvariant
Browse files Browse the repository at this point in the history
first-best-admissible tabu search
  • Loading branch information
hdbeukel committed Jun 21, 2017
2 parents f6bfd50 + c9047dd commit 4e85c20
Show file tree
Hide file tree
Showing 11 changed files with 914 additions and 96 deletions.
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
.DS_Store
/target/
**/target/*
**/bin/*
**/dist/*
!**/dist/resources/
*.log
*.jar
*.zip
/target/
*.iml
.classpath
.project
**/.settings/
**/.idea/
Empty file modified .travis/add-sonatype-server.py
100755 → 100644
Empty file.
135 changes: 95 additions & 40 deletions src/main/java/org/jamesframework/core/search/NeighbourhoodSearch.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import java.util.Arrays;
import org.jamesframework.core.search.status.SearchStatus;
import java.util.Collection;
import java.util.Iterator;
import java.util.function.Predicate;
import org.jamesframework.core.exceptions.SearchException;
import org.jamesframework.core.problems.Problem;
Expand All @@ -37,7 +38,8 @@
* which case the current solution is retained. The number of accepted and rejected moves during the current or last
* run can be accessed. This additional metadata applies to the current run only.
*
* @param <SolutionType> solution type of the problems that may be solved using this search, required to extend {@link Solution}
* @param <SolutionType> solution type of the problems that may be solved using this search,
* required to extend {@link Solution}
* @author <a href="mailto:herman.debeukelaer@ugent.be">Herman De Beukelaer</a>
*/
public abstract class NeighbourhoodSearch<SolutionType extends Solution> extends LocalSearch<SolutionType> {
Expand All @@ -46,8 +48,10 @@ public abstract class NeighbourhoodSearch<SolutionType extends Solution> extends
/* PRIVATE FIELDS */
/******************/

// number of accepted/rejected moves during current run
private long numAcceptedMoves, numRejectedMoves;
// number of accepted moves during current run
private long numAcceptedMoves;
// number of rejected moves during current run
private long numRejectedMoves;

// evaluated move cache
private EvaluatedMoveCache cache;
Expand All @@ -59,8 +63,8 @@ public abstract class NeighbourhoodSearch<SolutionType extends Solution> extends
/**
* Create a new neighbourhood search to solve the given problem, with default name "NeighbourhoodSearch".
*
* @throws NullPointerException if <code>problem</code> is <code>null</code>
* @param problem problem to solve
* @throws NullPointerException if <code>problem</code> is <code>null</code>
*/
public NeighbourhoodSearch(Problem<SolutionType> problem){
this(null, problem);
Expand All @@ -70,9 +74,9 @@ public NeighbourhoodSearch(Problem<SolutionType> problem){
* Create a new neighbourhood search to solve the given problem, with a custom name. If <code>name</code> is
* <code>null</code>, the default name "NeighbourhoodSearch" will be assigned.
*
* @throws NullPointerException if <code>problem</code> is <code>null</code>
* @param problem problem to solve
* @param name custom search name
* @throws NullPointerException if <code>problem</code> is <code>null</code>
*/
public NeighbourhoodSearch(String name, Problem<SolutionType> problem){
super(name != null ? name : "NeighbourhoodSearch", problem);
Expand Down Expand Up @@ -300,14 +304,15 @@ && validate(move).passed()

/**
* <p>
* Get the best valid move among a collection of possible moves. The best valid move is the one yielding the
* largest delta (see {@link #computeDelta(Evaluation, Evaluation)}) when being applied to the current solution.
* Get the best valid move among a collection of possible moves. The best move is the one yielding the
* largest delta (see {@link #computeDelta(Evaluation, Evaluation)}) when being applied to the current
* solution, from all valid moves.
* </p>
* <p>
* If <code>requireImprovement</code> is set to <code>true</code>, only moves that improve the current solution
* are considered, i.e. moves that yield a positive delta (unless the current solution is invalid, then all
* valid moves are improvements). Any number of additional filters can be specified so that moves are only
* considered if they pass through all filters. Each filter is a predicate that should return <code>true</code>
* valid moves are improvements). Any number of additional filters can be specified, where moves are only
* admitted if they pass through all filters. Each filter is a predicate that should return <code>true</code>
* if a given move is to be considered. If any filter returns <code>false</code> for a specific move, this
* move is discarded.
* </p>
Expand All @@ -316,8 +321,8 @@ && validate(move).passed()
* </p>
* <p>
* Note that all computed evaluations and validations are cached.
* Before returning the selected best move, if any, its evaluation and validity are cached
* again to maximize the probability that these values will remain available in the cache.
* Before returning the chosen move, if any, its evaluation and validation are re-cached
* to maximize the probability that these values will remain available in the cache for later retrieval.
* </p>
*
* @param moves collection of possible moves
Expand All @@ -329,41 +334,91 @@ && validate(move).passed()
protected final Move<? super SolutionType> getBestMove(Collection<? extends Move<? super SolutionType>> moves,
boolean requireImprovement,
Predicate<? super Move<? super SolutionType>>... filters){
// track best valid move + corresponding evaluation, validation and delta
Move<? super SolutionType> bestMove = null;
double bestMoveDelta = -Double.MAX_VALUE, curMoveDelta;
Evaluation curMoveEvaluation, bestMoveEvaluation = null;
Validation curMoveValidation, bestMoveValidation = null;
// go through all moves
for (Move<? super SolutionType> move : moves) {
// check filters
if(Arrays.stream(filters).allMatch(filter -> filter.test(move))){
// validate move
curMoveValidation = validate(move);
return this.getBestMove(moves, requireImprovement, false, filters);
}

/**
* <p>
* Get the best valid move among a collection of possible moves. The best move is the one yielding the
* largest delta (see {@link #computeDelta(Evaluation, Evaluation)}) when being applied to the current
* solution, from all valid moves.
* </p>
* <p>
* If <code>requireImprovement</code> is set to <code>true</code>, only moves that improve the current solution
* are considered, i.e. moves that yield a positive delta (unless the current solution is invalid, then all
* valid moves are improvements). Any number of additional filters can be specified, where moves are only
* admitted if they pass through all filters. Each filter is a predicate that should return <code>true</code>
* if a given move is to be considered. If any filter returns <code>false</code> for a specific move, this
* move is discarded.
* </p>
* <p>
* If <code>acceptFirstImprovement</code> is <code>true</code>, the first encountered admissible move that
* yields an improvement is returned. If there are no admissible improvements, as usual, the best admissible
* move is returned, which, in this case, always yields a negative delta. This option is used for first descent
* strategies, as opposed to steepest descent strategies.
* </p>
* <p>
* Returns <code>null</code> if no move is found that satisfies all conditions.
* </p>
* <p>
* Note that all computed evaluations and validations are cached.
* Before returning the chosen move, if any, its evaluation and validation are re-cached
* to maximize the probability that these values will remain available in the cache for later retrieval.
* </p>
*
* @param moves collection of possible moves
* @param requireImprovement if set to <code>true</code>, only improving moves are considered
* @param acceptFirstImprovement if set to <code>true</code>, the first improvement is returned, if any
* @param filters additional move filters
* @return selected move, may be <code>null</code>
*/
@SafeVarargs
protected final Move<? super SolutionType> getBestMove(Collection<? extends Move<? super SolutionType>> moves,
boolean requireImprovement, boolean acceptFirstImprovement,
Predicate<? super Move<? super SolutionType>>... filters){

// track the chosen move
Move<? super SolutionType> chosenMove = null;
// track evaluation, validation and delta of chosen move
double chosenMoveDelta = -Double.MAX_VALUE;
Evaluation chosenMoveEvaluation = null;
Validation chosenMoveValidation = null;
// define variables for metadata of current move
double curMoveDelta;
Evaluation curMoveEvaluation;
Validation curMoveValidation;
// iterate over all moves
Iterator<? extends Move<? super SolutionType>> it = moves.iterator();
while (
it.hasNext() // continue as long as there are more moves
&& !(acceptFirstImprovement && isImprovement(chosenMove)) // if requested, accept first improvement
){
Move<? super SolutionType> curMove = it.next();
if (Arrays.stream(filters).allMatch(filter -> filter.test(curMove))) {
curMoveValidation = validate(curMove);
if (curMoveValidation.passed()) {
// evaluate move
curMoveEvaluation = evaluate(move);
// compute delta
curMoveEvaluation = evaluate(curMove);
curMoveDelta = computeDelta(curMoveEvaluation, getCurrentSolutionEvaluation());
// compare with current best move
if (curMoveDelta > bestMoveDelta // higher delta
&& (!requireImprovement // ensure improvement, if required
|| curMoveDelta > 0
|| !getCurrentSolutionValidation().passed())) {
bestMove = move;
bestMoveDelta = curMoveDelta;
bestMoveEvaluation = curMoveEvaluation;
if (curMoveDelta > chosenMoveDelta // found better move?
&& (!requireImprovement || isImprovement(curMove)) // if requested, ensure improvement
) {
chosenMove = curMove;
chosenMoveDelta = curMoveDelta;
chosenMoveEvaluation = curMoveEvaluation;
chosenMoveValidation = curMoveValidation;
}
}
}
}
// re-cache best move, if any
if(bestMove != null && cache != null){
cache.cacheMoveEvaluation(bestMove, bestMoveEvaluation);
cache.cacheMoveValidation(bestMove, bestMoveValidation);

// re-cache the chosen move, if any
if(cache != null && chosenMove != null){
cache.cacheMoveEvaluation(chosenMove, chosenMoveEvaluation);
cache.cacheMoveValidation(chosenMove, chosenMoveValidation);
}
// return best move
return bestMove;

// return the chosen move
return chosenMove;
}

/**
Expand All @@ -378,7 +433,7 @@ protected final Move<? super SolutionType> getBestMove(Collection<? extends Move
*
* @param move accepted move to be applied to the current solution
* @return <code>true</code> if the update has been successfully performed,
* <code>false</code> if the update was cancelled because the obtained
* <code>false</code> if the update was canceled because the obtained
* neighbour is invalid
*/
protected boolean accept(Move<? super SolutionType> move){
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package org.jamesframework.core.search.algo.tabu;

import java.util.List;
import java.util.Collections;

import org.jamesframework.core.exceptions.IncompatibleTabuMemoryException;
import org.jamesframework.core.exceptions.JamesRuntimeException;

import org.jamesframework.core.problems.Problem;
import org.jamesframework.core.problems.sol.Solution;
import org.jamesframework.core.search.neigh.Move;
import org.jamesframework.core.search.neigh.Neighbourhood;

/**
* <p>
* Tabu search algorithm using first-best-admissible move strategy.
* In every search step, the possible moves are evaluated in random order (shuffled).
* The first encountered admissible improvement is accepted, i.e. the first valid non-tabu
* move that yields a positive delta, if any. Else, the best admissible move, in this case
* the one with the least negative delta, is performed, as in ordinary tabu search
* (see {@link org.jamesframework.core.search.algo.tabu.TabuSearch}).
* </p>
* <p>
* This tabu search implementation includes an aspiration criterion that always accepts all moves
* that yield an improvement over the currently known best solution, even if they have been declared tabu.
* </p>
* <p>
* The search terminates in case all valid neighbours of the current solution are tabu and do not improve the
* best known solution. Note that this may never happen, which means that a stop criterion should preferably
* be specified to ensure termination.
* </p>
*
* @param <SolutionType> solution type of the problems that may be solved using this search,
* required to extend {@link Solution}
* @author <a href="mailto:chenhuanfa@gmail.com">Huanfa Chen</a>,
* <a href="mailto:herman.debeukelaer@ugent.be">Herman De Beukelaer</a>
*/
public class FirstBestAdmissibleTabuSearch<SolutionType extends Solution> extends TabuSearch<SolutionType> {

/**
* Creates a new tabu search, specifying the problem to solve, the neighbourhood used to modify the current
* solution and the applied tabu memory. None of the arguments can be <code>null</code>. The search name defaults
* to "FirstBestAdmissibleTabuSearch".
* <p>
* Note that the applied neighbourhood and tabu memory should be compatible in terms of generated and accepted move
* types, respectively, else an {@link IncompatibleTabuMemoryException} might be thrown during search.
*
* @param problem problem to solve
* @param neighbourhood neighbourhood used to create neighbouring solutions
* @param tabuMemory applied tabu memory
* @throws NullPointerException if <code>problem</code>, <code>neighbourhood</code> or <code>tabuMemory</code>
* are <code>null</code>
*/
public FirstBestAdmissibleTabuSearch(Problem<SolutionType> problem,
Neighbourhood<? super SolutionType> neighbourhood,
TabuMemory<SolutionType> tabuMemory) {
this(null, problem, neighbourhood, tabuMemory);
}

/**
* Creates a new tabu search, specifying the problem to solve, the neighbourhood used to modify the current
* solution, the applied tabu memory and a custom search name. The problem, neighbourhood and tabu memory can
* not be <code>null</code>. The search name can be <code>null</code> in which case the default name
* "FirstBestAdmissibleTabuSearch" is assigned.
* <p>
* Note that the applied neighbourhood and tabu memory should be compatible in terms of generated and accepted move
* types, respectively, else an {@link IncompatibleTabuMemoryException} might be thrown during search.
*
* @param name custom search name
* @param problem problem to solve
* @param neighbourhood neighbourhood used to create neighbouring solutions
* @param tabuMemory applied tabu memory
* @throws NullPointerException if <code>problem</code>, <code>neighbourhood</code> or <code>tabyMemory</code>
* are <code>null</code>
*/
public FirstBestAdmissibleTabuSearch(String name, Problem<SolutionType> problem,
Neighbourhood<? super SolutionType> neighbourhood,
TabuMemory<SolutionType> tabuMemory){
super(name != null ? name : "FirstBestAdmissibleTabuSearch", problem, neighbourhood, tabuMemory);
}

/**
* One step of the first-best-admissible tabu search algorithm.
* All possible moves are inspected in random order, and either the first admissible improvement,
* if any, or, else, the best admissible move, is applied to the current solution.
*
* @throws IncompatibleTabuMemoryException if the applied tabu memory is not compatible with the type of moves
* generated by the applied neighbourhood
* @throws JamesRuntimeException if depending on malfunctioning components (problem, neighbourhood, ...)
*/
@Override
protected void searchStep() {
// get list of possible moves
List<? extends Move<? super SolutionType>> moves = getNeighbourhood().getAllMoves(getCurrentSolution());
// shuffle moves
Collections.shuffle(moves);
// find best admissible move, or first admissible improvement (if any)
Move<? super SolutionType> move = getBestMove(
// inspect all moves
moves,
// not necessarily an improvement
false,
// return first improvement move, if any
true,
// filter tabu moves (with aspiration criterion)
getTabuFilter()
);
// process chosen move
if (move != null) {
// accept move (automatically updates tabu memory)
accept(move);
} else {
// no valid non-tabu neighbour found: terminate search
stop();
}
}
}
Loading

0 comments on commit 4e85c20

Please sign in to comment.