Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add getBestMove and FirstBestAdmissibleTabuSearch #39

Merged
merged 9 commits into from
Jun 14, 2017
105 changes: 105 additions & 0 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,8 @@
import java.util.Arrays;
import org.jamesframework.core.search.status.SearchStatus;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.function.Predicate;
import org.jamesframework.core.exceptions.SearchException;
import org.jamesframework.core.problems.Problem;
Expand Down Expand Up @@ -353,6 +355,8 @@ protected final Move<? super SolutionType> getBestMove(Collection<? extends Move
bestMove = move;
bestMoveDelta = curMoveDelta;
bestMoveEvaluation = curMoveEvaluation;
// should update the bestMoveValidation
bestMoveValidation = curMoveValidation;
}
}
}
Expand All @@ -366,6 +370,107 @@ protected final Move<? super SolutionType> getBestMove(Collection<? extends Move
return bestMove;
}

/**
* <p>
* Get the best valid move among a collection of possible moves. The best valid move is the FIRST one yielding the positive delta
* or the one that is non-tabu and yield the largest delta(see {@link #computeDelta(Evaluation, Evaluation)}) when being applied to the current solution.
* </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>
* 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>
* 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 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.
* </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>, return the first improvement move
* @param filters additional move filters
* @return first improving move or the best valid move, may be <code>null</code>
*/
@SafeVarargs
protected final Move<? super SolutionType> getBestMove(List<? extends Move<? super SolutionType>> moves,
boolean requireImprovement,
boolean acceptFirstImprovement,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parameter acceptFirstImprovement is never used. You implementation returns the first improvement, even if acceptFirstImprovement is false. See comments below to fix.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the parameter acceptFirstImprovement is never checked.

Predicate<? super Move<? super SolutionType>>... filters){
Collections.shuffle(moves);
// track first improvement move AND best valid move + corresponding evaluation, validation and delta
Move<? super SolutionType> bestMove = null, firstImprovementMove = null;
double bestMoveDelta = -Double.MAX_VALUE, curMoveDelta, firstImprovementMoveDelta = -Double.MAX_VALUE;
Evaluation curMoveEvaluation, bestMoveEvaluation = null, firstImprovementMoveEvaluation = null;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need separate variables for first and best improvement. One variable, say chosenMove, is sufficient to store the move you selected (either first improvement, or best). Then you also have chosenMoveDelta, chosenMoveEvaluation, and chosenMoveValidation.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to store first and best move (and their metadata separately). You can have one series of variables for the chosen move, say chosenMove, chosenMoveDelta, etc. which will either be the first or best one depending on the parameter acceptFirstImprovement.

Validation curMoveValidation, bestMoveValidation = null, firstImprovementMoveValidation = null;
// go through all moves
for (Move<? super SolutionType> move : moves) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use an Iterator here in a while loop with multiple conditions to avoid the use of break. You should continue as long as there are more moves to try and !(acceptFirstImprovement && chosenMoveDelta > 0).

// check filters
if(Arrays.stream(filters).allMatch(filter -> filter.test(move))){
// validate move
curMoveValidation = validate(move);
if (curMoveValidation.passed()) {
// evaluate move
curMoveEvaluation = evaluate(move);
// compute delta
curMoveDelta = computeDelta(curMoveEvaluation, getCurrentSolutionEvaluation());

// compare with current solution
if(curMoveDelta > 0 // better than curSolution
|| !getCurrentSolutionValidation().passed()){
firstImprovementMove = move;
firstImprovementMoveDelta = curMoveDelta;
firstImprovementMoveEvaluation = curMoveEvaluation;
firstImprovementMoveValidation = curMoveValidation;
break;
}
// if (curMoveDelta > bestMoveDelta // higher delta
// && (!requireImprovement // ensure improvement, if required
// || curMoveDelta > 0
// || !getCurrentSolutionValidation().passed())) {
// bestMove = move;
// bestMoveDelta = curMoveDelta;
// bestMoveEvaluation = curMoveEvaluation;
// bestMoveValidation = curMoveValidation;
// break;
// }
// compare with best move
if (curMoveDelta > bestMoveDelta){ // higher delta
bestMoveDelta = curMoveDelta;
bestMove = move;
bestMoveEvaluation = curMoveEvaluation;
bestMoveValidation = curMoveValidation;
}


}
}
}
// re-cache best move, if any
if(bestMove != null && cache != null){
cache.cacheMoveEvaluation(bestMove, bestMoveEvaluation);
cache.cacheMoveValidation(bestMove, bestMoveValidation);
}
// re-cache best non-tabu move, if any
if(firstImprovementMove != null && cache != null){
cache.cacheMoveEvaluation(firstImprovementMove, firstImprovementMoveEvaluation);
cache.cacheMoveValidation(firstImprovementMove, firstImprovementMoveValidation);
}
// If firstImprovementMove is not null, return firstImprovementMove.
// Otherwise, return bestMove (which should be no better than currentSolution but be better than any other move)
if(firstImprovementMove != null)
return firstImprovementMove;
else
return bestMove;

}

/**
* Accept the given move by applying it to the current solution. Updates the evaluation and validation of
* the current solution and checks whether a new best solution has been found. The updates only take place
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package org.jamesframework.core.search.algo.tabu;

import java.util.Arrays;
import java.util.Collection;
import java.util.function.Predicate;

import org.jamesframework.core.problems.Problem;
import org.jamesframework.core.problems.constraints.validations.Validation;
import org.jamesframework.core.problems.objectives.evaluations.Evaluation;
import org.jamesframework.core.problems.sol.Solution;
import org.jamesframework.core.search.SingleNeighbourhoodSearch;
import org.jamesframework.core.search.neigh.Move;
import org.jamesframework.core.search.neigh.Neighbourhood;
/**
* Tabu search algorithm using first-best-admissible move strategy.
* In every search step, shuffle the list of moves and then iterate over all moves. If a valid neighbour that is better
* than the current solution is found, it is adopted as the new solution. Otherwise, the best valid and non-tabu neighbour
* is adopted, even if it is no improvement over the current solution, which is the same as the ordinary tabu search.
* To avoid repeatedly revisiting the same solutions, moves might be declared tabu based on a tabu memory.
* This memory dynamically tracks (a limited number of) recently visited solutions, features of these solutions and/or
* recently applied moves (i.e. recently modified features). If a move is tabu, it is not considered, unless it yields
* a solution which is better than the best solution found so far (aspiration criterion).
* <p>
* If all valid neighbours of the current solution are tabu and no valid neighbours are better than the current solution,
* the search stops. Note that this may never happen so that a stop criterion should preferably be set to ensure termination.
*
* @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>
*/
public class FirstBestAdmissibleTabuSearch<SolutionType extends Solution> extends TabuSearch<SolutionType>{
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please also test this new Tabu search.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test added in the new pull request.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't create a new pull request for each commit. Just keep committing to the same branch, all changes will show up here. I closed the other pull request #43.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not very familiar with Github. Say, I add and commit the changes, and push them to the remote repo. Will it be automatically committed to the same branch?

Copy link
Owner

@hdbeukel hdbeukel Jun 2, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you don't specify an explicit branch when pushing, git will push to the remote branch that has been associated with your local branch. So yes, these commits will end up in the same branch each time. In your case tabuSearch/add_variant.

Are you using git on command line or a desktop client?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am using git bash on Windows. Just add a new push.


public FirstBestAdmissibleTabuSearch(Problem<SolutionType> problem, Neighbourhood<? super SolutionType> neighbourhood,
TabuMemory<SolutionType> tabuMemory) {
super(problem, neighbourhood, tabuMemory);
// TODO Auto-generated constructor stub
}

@Override
protected void searchStep() {
// get best valid, non tabu move
Move<? super SolutionType> move = getBestMove(
// inspect all moves
getNeighbourhood().getAllMoves(getCurrentSolution()),
// not necessarily an improvement
false,
// return first improvement move
true,
// filter tabu moves (with aspiration criterion)
m -> !getTabuMemory().isTabu(m, getCurrentSolution())
|| computeDelta(evaluate(m), getBestSolutionEvaluation()) > 0
);
if(move != null){
// accept move (also updates tabu memory by overriding move acceptance)
accept(move);
} else {
// no valid, non tabu neighbour found: stop search
stop();
}
}
}