org.jamesframework.core.search.neigh.subset.SinglePerturbationNeighbourhood Maven / Gradle / Ivy
Show all versions of james-core Show documentation
// Copyright 2014 Herman De Beukelaer
//
// 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.jamesframework.core.search.neigh.subset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ThreadLocalRandom;
import org.jamesframework.core.problems.solutions.SubsetSolution;
import org.jamesframework.core.search.neigh.Move;
import org.jamesframework.core.search.neigh.Neighbourhood;
import org.jamesframework.core.util.RouletteSelector;
import org.jamesframework.core.util.SetUtilities;
/**
*
* A subset neighbourhood that generates swap moves (see {@link SwapMove}), addition moves (see {@link AdditionMove}) and deletion
* moves (see {@link DeletionMove}). Applying an addition or deletion move to a given subset solution will increase, respectively
* decrease, the number of selected items. Therefore, this neighbourhood is also suited for variable size subset selection problems,
* in contrast to {@link SingleSwapNeighbourhood}, which can only handle fixed size subset problems.
*
*
* A single perturbation neighbourhood respects the minimum and maximum subset size specified at construction. When the given
* subset solution has minimal size, no deletion moves will be generated. Similarly, when the current solution has maximum size, no
* addition moves will be generated.
*
*
* If desired, a set of fixed IDs can be provided which are not allowed to be added, deleted nor swapped. None of the moves
* generated by this neighbourhood will ever select nor deselect any of the provided fixed IDs.
*
*
* Note that this neighbourhood is thread-safe: it can be safely used to concurrently generate moves in different searches running
* in separate threads.
*
*
* @author Herman De Beukelaer
*/
public class SinglePerturbationNeighbourhood implements Neighbourhood {
// move type enum used for randomly picking a move type
private enum MoveType {
ADDITION, DELETION, SWAP;
}
// minimum and maximum subset size
private final int minSubsetSize;
private final int maxSubsetSize;
// set of fixed IDs
private final Set fixedIDs;
/**
* Creates a new single perturbation neighbourhood with given minimum and maximum subset size.
* Only moves that result in a valid solution size after application to the current solution
* will ever be generated. Positive values are required for the minimum and maximum size,
* with minimum smaller than or equal to maximum; else, an exception is thrown.
*
* @throws IllegalArgumentException if minimum and maximum size are not both positive, or minimum > maximum
* @param minSubsetSize minimum subset size
* @param maxSubsetSize maximum subset size
*/
public SinglePerturbationNeighbourhood(int minSubsetSize, int maxSubsetSize){
this(minSubsetSize, maxSubsetSize, null);
}
/**
* Creates a new single perturbation neighbourhood with given minimum and maximum subset size,
* providing a set of fixed IDs which are not allowed to be added, deleted nor swapped.
* Only moves that result in a valid solution size after application to the current solution
* will ever be generated. Positive values are required for the minimum and maximum size,
* with minimum smaller than or equal to maximum; else, an exception is thrown.
*
* @throws IllegalArgumentException if minimum and maximum size are not both positive, or minimum > maximum
* @param minSubsetSize minimum subset size
* @param maxSubsetSize maximum subset size
* @param fixedIDs set of fixed IDs
*/
public SinglePerturbationNeighbourhood(int minSubsetSize, int maxSubsetSize, Set fixedIDs){
// validate sizes
if(minSubsetSize < 0){
throw new IllegalArgumentException("Error while creating single perturbation neighbourhood: minimum subset size should be non-negative.");
}
if(maxSubsetSize < 0){
throw new IllegalArgumentException("Error while creating single perturbation neighbourhood: maximum subset size should be non-negative.");
}
if(minSubsetSize > maxSubsetSize){
throw new IllegalArgumentException("Error while creating single perturbation neighbourhood: "
+ "minimum subset size should be smaller than or equal to maximum subset size.");
}
this.minSubsetSize = minSubsetSize;
this.maxSubsetSize = maxSubsetSize;
this.fixedIDs = fixedIDs;
}
/**
*
* Generates a random swap, deletion or addition move that transforms the given subset solution into
* a neighbour within the minimum and maximum allowed subset size. If no valid move can be generated,
* null
is returned. If any fixed IDs have been specified, these will not be considered
* for deletion nor addition.
*
*
* Note that every individual move is generated with equal probability, taking into account the possibly
* different number of possible moves of each type.
*
*
* @param solution solution for which a random move is generated
* @return random move, null
if no valid move can be generated
*/
@Override
public Move getRandomMove(SubsetSolution solution) {
// use thread local random for concurrent performance
Random rg = ThreadLocalRandom.current();
// get set of candidate IDs for deletion and addition
Set deleteCandidates = solution.getSelectedIDs();
Set addCandidates = solution.getUnselectedIDs();
// remove fixed IDs, if any, from candidates
if(fixedIDs != null && !fixedIDs.isEmpty()){
deleteCandidates = new HashSet<>(deleteCandidates);
addCandidates = new HashSet<>(addCandidates);
deleteCandidates.removeAll(fixedIDs);
addCandidates.removeAll(fixedIDs);
}
// compute number of possible moves of each type (addition, deletion, swap)
int numAdd = numValidAdditionMoves(solution, addCandidates);
int numDel = numValidDeletionMoves(solution, deleteCandidates);
int numSwap = numValidSwapMoves(solution, addCandidates, deleteCandidates);
// pick move type using roulette selector
MoveType selectedMoveType = new RouletteSelector(rg).select(
Arrays.asList(MoveType.ADDITION, MoveType.DELETION, MoveType.SWAP),
Arrays.asList((double) numAdd, (double) numDel, (double) numSwap)
);
// in case of no valid moves: return null
if(selectedMoveType == null){
return null;
} else {
// generate random move of chosen type
switch(selectedMoveType){
case ADDITION : return new AdditionMove(SetUtilities.getRandomElement(addCandidates, rg));
case DELETION : return new DeletionMove(SetUtilities.getRandomElement(deleteCandidates, rg));
case SWAP : return new SwapMove(
SetUtilities.getRandomElement(addCandidates, rg),
SetUtilities.getRandomElement(deleteCandidates, rg)
);
default : throw new Error("This should never happen. If this exception is thrown, "
+ "there is a serious bug in SinglePerturbationNeighbourhood.");
}
}
}
/**
* Generate all valid swap, deletion and addition moves that transform the given subset solution into
* a neighbour within the minimum and maximum allowed subset size. The returned set may be empty,
* if no valid moves exist. If any fixed IDs have been specified, these will not be considered
* for deletion nor addition.
*
* @param solution solution for which a set of all valid moves is generated
* @return set of all valid swap, deletion and addition moves
*/
@Override
public Set> getAllMoves(SubsetSolution solution) {
// get set of candidate IDs for deletion and addition
Set deleteCandidates = solution.getSelectedIDs();
Set addCandidates = solution.getUnselectedIDs();
// remove fixed IDs, if any, from candidates
if(fixedIDs != null && !fixedIDs.isEmpty()){
deleteCandidates = new HashSet<>(deleteCandidates);
addCandidates = new HashSet<>(addCandidates);
deleteCandidates.removeAll(fixedIDs);
addCandidates.removeAll(fixedIDs);
}
// create empty set of moves
Set> moves = new HashSet<>();
// generate all addition moves, if valid
if(numValidAdditionMoves(solution, addCandidates) > 0){
// go through candidate IDs for addition
for(int ID : addCandidates){
// create addition move that adds this ID
moves.add(new AdditionMove(ID));
}
}
// generate all deletion moves, if valid
if(numValidDeletionMoves(solution, deleteCandidates) > 0){
// go through candidate IDs for deletion
for(int ID : deleteCandidates){
// create deletion move that deletes this ID
moves.add(new DeletionMove(ID));
}
}
// generate all swap moves, if valid
if(numValidSwapMoves(solution, addCandidates, deleteCandidates) > 0){
// go through candidates for addition
for(int add : addCandidates){
// go through candidates for deletion
for(int del : deleteCandidates){
// create corresponding swap move
moves.add(new SwapMove(add, del));
}
}
}
// return generated moves
return moves;
}
/**
* Compute the number of possible addition moves that yield a neighbour
* within the valid subset size range, for the given solution.
*
* @param solution solution for which moves are generated
* @param addCandidates set of candidate IDs to be added
* @return number of possible addition moves
*/
private int numValidAdditionMoves(SubsetSolution solution, Set addCandidates){
return isValidSubsetSize(solution.getNumSelectedIDs()+1) ? addCandidates.size() : 0;
}
/**
* Compute the number of possible deletion moves that yield a neighbour
* within the valid subset size range, for the given solution.
*
* @param solution solution for which moves are generated
* @param deleteCandidates set of candidate IDs to be deleted
* @return number of possible deletion moves
*/
private int numValidDeletionMoves(SubsetSolution solution, Set deleteCandidates){
return isValidSubsetSize(solution.getNumSelectedIDs()-1) ? deleteCandidates.size() : 0;
}
/**
* Compute the number of possible swap moves that yield a neighbour
* within the valid subset size range, for the given solution.
*
* @param solution solution for which moves are generated
* @param addCandidates set of candidate IDs to be added
* @param deleteCandidates set of candidate IDs to be deleted
* @return number of possible swap moves
*/
private int numValidSwapMoves(SubsetSolution solution, Set addCandidates, Set deleteCandidates){
return isValidSubsetSize(solution.getNumSelectedIDs()) ? addCandidates.size()*deleteCandidates.size() : 0;
}
/**
* Verifies whether the given subset size is valid, taking into account
* the minimum and maximum size specified at construction.
*
* @param size size to verify
* @return true
if size falls within bounds
*/
private boolean isValidSubsetSize(int size){
return size >= minSubsetSize && size <= maxSubsetSize;
}
/**
* Get the minimum subset size specified at construction.
*
* @return minimum subset size
*/
public int getMinSubsetSize() {
return minSubsetSize;
}
/**
* Get the maximum subset size specified at construction.
*
* @return maximum subset size
*/
public int getMaxSubsetSize() {
return maxSubsetSize;
}
}