org.jamesframework.core.search.Search 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;
import org.jamesframework.core.search.listeners.SearchListener;
import org.jamesframework.core.search.stopcriteria.StopCriterion;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.jamesframework.core.exceptions.IncompatibleStopCriterionException;
import org.jamesframework.core.exceptions.JamesRuntimeException;
import org.jamesframework.core.exceptions.SearchException;
import org.jamesframework.core.problems.Problem;
import org.jamesframework.core.problems.solutions.Solution;
import org.jamesframework.core.search.stopcriteria.StopCriterionChecker;
import org.jamesframework.core.util.JamesConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
*
* General abstract search used to solve a problem with the specified solution type. It provides general methods to
* start and stop the search, and to access state information and metadata such as the best solution found so far and the
* runtime of the current run. It also provides methods to add and remove search listeners and stop criteria.
*
*
* A search can have five possible statuses: IDLE, INITIALIZING, RUNNING, TERMINATING or DISPOSED (see {@link SearchStatus}).
* When a search is created, it is IDLE. Upon calling {@link #start()} it first goes to INITIALIZING and then RUNNING, after
* successful initialization. While the search is running, it iteratively calls {@link #searchStep()} to perform a search step
* as defined in each specific search implementation.
*
*
* Whenever a search is requested to stop, by calling {@link #stop()}, it goes to status TERMINATING. A terminating
* search will stop after it has completed its current step, and then its status goes back to status IDLE. A search
* may also terminate itself by calling {@link #stop()} internally, when it has come to its natural end. In particular,
* a single step algorithm can be implemented by calling {@link #stop()} immediately at the end of this first and
* only step, which guarantees that only one single step will be executed.
*
*
* An idle search may be restarted at any time. The search state is retained across subsequent runs, including the best
* solution found so far and any search specific state elements, unless explicitely stated otherwise. On the other hand,
* the following metadata applies to the current run only:
*
*
* - current runtime
* - current number of steps
* - time without improvement
* - steps without improvement
* - minimum delta
*
*
* This might also be the case for additional metadata in specific searches, which should be clearly indicated in their
* documentation. Note that stop criteria relying on such metadata will operate on a per-run basis.
*
*
* An idle search can also be disposed (see {@link #dispose()}), upon which it will release all of its resources. A disposed
* search can never be restarted. Note that it is important to always dispose a search after its last run so that it does not
* hold on to any of its resources. Not disposing a search may prevent termination of the application.
*
*
* @param solution type of the problems that may be solved using this search, required to extend {@link Solution}
* @author Herman De Beukelaer
*/
public abstract class Search implements Runnable {
/*********************/
/* UNIQUE ID COUNTER */
/*********************/
private static int nextID = 0;
/**********/
/* LOGGER */
/**********/
private static final Logger logger = LoggerFactory.getLogger(Search.class);
/******************/
/* PRIVATE FIELDS */
/******************/
// timestamp indicating when the current (or last) run was started
private long startTime;
// timestamp indicating when the last run finished
private long stopTime;
// number of steps completed in the current (or last) run
private long currentSteps;
// timestamp indicating when the last improvement was made during the current (or last) run
private long lastImprovementTime;
// steps completed since last improvement during current (or last) run
private long stepsSinceLastImprovement;
// flags improvement during current step, used to update steps since last improvement at the end of each step
private boolean improvementDuringCurrentStep;
// minimum improvement in evaluation of a newly found best solution
// over the previously known best solution, during the current (or last) run
private double minDelta;
// best solution found so far and its corresponding evaluation
private SolutionType bestSolution;
private double bestSolutionEvaluation;
// search status
private SearchStatus status;
/************************/
/* PRIVATE FINAL FIELDS */
/************************/
// search name & ID
private final String name;
private final int id;
// problem being solved
private final Problem problem;
// set of search listeners attached to this search
private final Set> searchListeners;
// stop criterion checker dedicated to checking the stop criteria attached to this search
private final StopCriterionChecker stopCriterionChecker;
/*********/
/* LOCKS */
/*********/
// lock acquired when updating the search status and when executing a block of code during which
// the status is not allowed to change
private final Object statusLock = new Object();
/****************/
/* CONSTRUCTORS */
/****************/
/**
* Creates a search to solve the given problem, with default search name "Search".
*
* @throws NullPointerException if problem
is null
* @param problem problem to solve
*/
public Search(Problem problem){
this(null, problem);
}
/**
* Creates a search to solve the given problem, with a custom search name. If
* name
is null
, a default search name "Search" will
* be assigned.
*
* @throws NullPointerException if problem
is null
* @param problem problem to solve
* @param name custom search name
*/
@SuppressWarnings("LeakingThisInConstructor")
public Search(String name, Problem problem){
// check problem
if(problem == null){
throw new NullPointerException("Error while creating search: problem can not be null.");
}
// store problem reference
this.problem = problem;
// store name
if(name != null){
this.name = name;
} else {
// no name given: default to "Search"
this.name = "Search";
}
// assign next unique id
id = getNextUniqueID();
// initialize search listener set
searchListeners = new HashSet<>();
// create dedicated stop criterion checker
stopCriterionChecker = new StopCriterionChecker(this);
// set initial status to idle
status = SearchStatus.IDLE;
// initially, best solution is null and its evaluation
// is arbitrary (as defined in getBestSolutionEvaluation())
bestSolution = null;
bestSolutionEvaluation = 0.0; // arbitrary value
// initialize per-run metadata
startTime = JamesConstants.INVALID_TIMESTAMP;
stopTime = JamesConstants.INVALID_TIMESTAMP;
currentSteps = JamesConstants.INVALID_STEP_COUNT;
lastImprovementTime = JamesConstants.INVALID_TIMESTAMP;
stepsSinceLastImprovement = JamesConstants.INVALID_STEP_COUNT;
minDelta = JamesConstants.INVALID_DELTA;
// initialize utility variables
improvementDuringCurrentStep = false;
// log search creation
logger.info("Created search {}", this);
}
/**
* Get the next unique ID to be assigned to this search. Synchronized to ensure
* uniqueness of IDs in multi threaded environments.
*
* @return next unique ID
*/
private synchronized int getNextUniqueID(){
return nextID++;
}
/**
* Get the problem being solved, as specified at construction.
*
* @return problem being solved
*/
public Problem getProblem(){
return problem;
}
/***************************/
/* NAME, ID & STRING VALUE */
/***************************/
/**
* Get the name that has been assigned to this search.
*
* @return search name
*/
public String getName(){
return name;
}
/**
* Get the unique ID that has been assigned to this search.
*
* @return unique search ID
*/
public int getID(){
return id;
}
/**
* Returns a string representation of the search, formatted as "%name(%id)".
*
* @return string representation containing name and id
*/
@Override
public String toString(){
return name + "(" + id + ")";
}
/***********************/
/* CONTROLLING METHODS */
/***********************/
/**
*
* Starts a search run and returns when this run has finished. The search run may either complete internally,
* i.e. come to its natural end, or be terminated by a stop criterion (see {@link #addStopCriterion(StopCriterion)}).
* This method does not return anything; the best solution found during search can be obtained by calling
* {@link #getBestSolution()} and its corresponding evaluation with {@link #getBestSolutionEvaluation()}.
*
*
* Note that a search can only be (re)started when it is idle (see {@link #getStatus()}). If attempted to start
* a search which is already running (or terminating), an exception will be thrown.
*
*
* Before the search is actually started, some initialization may take place. This initialization can also include
* a verification of the search configuration and in case of an invalid configuration, an exception may be thrown.
*
*
* @throws SearchException if the search is currently not idle, or if initialization fails because of an invalid
* search configuration
* @throws JamesRuntimeException in general, any {@link JamesRuntimeException} may be thrown
* in case of a malfunctioning component used during initialization,
* execution or finalization
*/
public void start(){
logger.trace("Search {} started", this);
// acquire status lock
synchronized(statusLock) {
// verify that search is idle
assertIdle("Cannot start search.");
// log
logger.trace("Search {} changed status: {} --> {}", this, status, SearchStatus.INITIALIZING);
// set status to INITIALIZING
status = SearchStatus.INITIALIZING;
// fire status update
fireStatusChanged(status);
}
// fire callback
fireSearchStarted();
// initialization and/or validation
searchStarted();
// check if search should be continued (may already
// have been stopped during initialization)
if(continueSearch()){
// instruct stop criterion checker to start checking
stopCriterionChecker.startChecking();
// initialization finished: update status
synchronized(statusLock){
// log
logger.trace("Search {} changed status: {} --> {}", this, status, SearchStatus.RUNNING);
// update
status = SearchStatus.RUNNING;
// fire status update
fireStatusChanged(status);
}
// enter search loop
while(continueSearch()){
// reset improvement flag (automatically flipped by
// updateBestSolution if improvement found during step)
improvementDuringCurrentStep = false;
// perform search step
searchStep();
// update step count
currentSteps++;
// update steps since last improvement
if(improvementDuringCurrentStep){
// improvement made
stepsSinceLastImprovement = 0;
} else if (stepsSinceLastImprovement != JamesConstants.INVALID_STEP_COUNT) {
// no improvement made now, but found improvement before in current run
stepsSinceLastImprovement++;
}
// fire callback
fireStepCompleted(currentSteps);
}
// instruct stop criterion checker to stop checking
stopCriterionChecker.stopChecking();
}
// finalization
searchStopped();
// fire callback
fireSearchStopped();
logger.trace("Search {} stopped (runtime: {} ms)", this, getRuntime());
// search run is complete: update status
synchronized(statusLock){
// log
logger.trace("Search {} changed status: {} --> {}", this, status, SearchStatus.IDLE);
// update
status = SearchStatus.IDLE;
// fire status update
fireStatusChanged(status);
}
}
/**
*
* Requests the search to stop. May be called from outside the search, e.g. by a stop criterion, as well as internally,
* when the search comes to its natural end. In the latter case, it is absolutely guaranteed that the step from which the search
* was requested to stop will be the last step executed during the current run. If the current search status is not INITIALIZING
* or RUNNING, calling this method has no effect. Else, it changes the search status to TERMINATING.
*
*
* In case the search is already requested to terminate during initialization, it will complete initialization, but is guaranteed
* to stop before executing any search steps.
*
*/
public void stop(){
// acquire status lock
synchronized(statusLock){
// check current status
if(status == SearchStatus.INITIALIZING || status == SearchStatus.RUNNING){
// log
logger.trace("Search {} changed status: {} --> {}", this, status, SearchStatus.TERMINATING);
// update status
status = SearchStatus.TERMINATING;
// fire status update
fireStatusChanged(status);
}
}
}
/**
* Dispose this search, upon which all of its resources are released. Note that only idle
* searches may be disposed and that a disposed search can never be restarted. Sets the search
* status to DISPOSED. When trying to dispose an already disposed search, nothing happens, i.e.
* calling this method on a disposed search has no effect.
*
* @throws SearchException if the search is currently not idle (and not already disposed)
*/
public void dispose(){
// acquire status lock
synchronized(statusLock){
// abort if already disposed
if(status == SearchStatus.DISPOSED){
return;
}
// assert idle
assertIdle("Cannot dispose search.");
// all good, handle disposed
searchDisposed();
// log
logger.trace("Search {} changed status: {} --> {}", this, status, SearchStatus.DISPOSED);
// update status
status = SearchStatus.DISPOSED;
// fire status update
fireStatusChanged(status);
}
}
/***************************/
/* RUNNABLE IMPLEMENTATION */
/***************************/
/**
* Equivalent to calling {@link #start()}. Through this method searches implement the {@link Runnable} interface
* so that they can easily be executed in a separate thread.
*/
@Override
public void run(){
start();
}
/*********************************************************/
/* METHODS FOR ADDING STOP CRITERIA AND SEARCH LISTENERS */
/*********************************************************/
/**
* Adds a stop criterion used to decide when the search should stop running. It might be verified whether the given stop criterion
* is compatible with the search and if not, an exception may be thrown. Note that this method can only be called when the search
* is idle.
*
* @param stopCriterion stop criterion used to decide when the search should stop running
* @throws IncompatibleStopCriterionException when the given stop criterion is incompatible with the search
* @throws SearchException if the search is not idle
*/
public void addStopCriterion(StopCriterion stopCriterion){
// acquire status lock
synchronized(statusLock){
// assert idle
assertIdle("Cannot add stop criterion.");
// check compatibility by performing a dummy call
try {
stopCriterion.searchShouldStop(this);
} catch (IncompatibleStopCriterionException ex){
// incompatible stop criterion: throw same exception to caller
throw ex;
}
// pass stop criterion to checker
stopCriterionChecker.add(stopCriterion);
// log
logger.info("{}: added stop criterion {}", this, stopCriterion);
}
}
/**
* Removes a stop criterion. In case this stop criterion had not been added, false
is returned.
* Note that this method may only be called when the search is idle.
*
* @param stopCriterion stop criterion to be removed
* @throws SearchException if the search is not idle
* @return true
if the stop criterion has been successfully removed
*/
public boolean removeStopCriterion(StopCriterion stopCriterion){
// acquire status lock
synchronized(statusLock){
// assert idle
assertIdle("Cannot remove stop criterion.");
// remove from checker
if (stopCriterionChecker.remove(stopCriterion)){
// log
logger.info("{}: removed stop criterion {}", this, stopCriterion);
return true;
} else {
return false;
}
}
}
/**
* Instructs the search to check its stop criteria at regular intervals separated by the given period.
* For the default period, see {@link StopCriterionChecker}, which is used internally for this purpose.
* The period should be at least 1 millisecond, else the stop criterion checker may thrown an exception
* when the search is started. Note that this method may only be called when the search is idle.
*
* @param period time between subsequent stop criterion checks (> 0)
* @param timeUnit corresponding time unit
* @throws SearchException if the search is not idle
* @throws IllegalArgumentException if the given period is not strictly positive
*/
public void setStopCriterionCheckPeriod(long period, TimeUnit timeUnit){
// acquire status lock
synchronized(statusLock){
// assert idle
assertIdle("Cannot modify stop criterion check period.");
// pass new settings to checker
stopCriterionChecker.setPeriod(period, timeUnit);
// log
logger.info("{}: set stop criterion check period to {} ms", this, timeUnit.toMillis(period));
}
}
/**
* Add a search listener, if it has not been added before. Any search listener
* with a matching solution type (or a more general solution type) may be added.
* Note that this method can only be called when the search is idle.
*
* @param listener search listener to add to the search
* @throws SearchException if the search is not idle
* @return true
if the search listener had not been added before
*/
public boolean addSearchListener(SearchListener listener){
// acquire status lock
synchronized(statusLock){
// assert idle
assertIdle("Cannot add search listener.");
// add listener
if(searchListeners.add(listener)){
// log
logger.info("{}: added search listener {}", this, listener);
return true;
} else {
return false;
}
}
}
/**
* Remove the given search listener. If the search listener had not been added, false
is returned.
* Note that this method may only be called when the search is idle.
*
* @param listener search listener to be removed
* @throws SearchException if the search is not idle
* @return true
if the listener has been successfully removed
*/
public boolean removeSearchListener(SearchListener listener){
// acquire status lock
synchronized(statusLock){
// assert idle
assertIdle("Cannot remove search listener.");
// remove listener
if (searchListeners.remove(listener)){
// log
logger.info("{}: removed search listener {}", this, listener);
return true;
} else {
return false;
}
}
}
/********************************************************/
/* PRIVATE METHODS FOR FIRING SEARCH LISTENER CALLBACKS */
/********************************************************/
/**
* Calls {@link SearchListener#searchStarted(Search)} on every attached search listener.
* Should only be executed when search is active (initializing, running or terminating).
*/
private void fireSearchStarted(){
for(SearchListener listener : searchListeners){
listener.searchStarted(this);
}
}
/**
* Calls {@link SearchListener#searchStopped(Search)} on every attached search listener.
* Should only be executed when search is active (initializing, running or terminating).
*/
private void fireSearchStopped(){
for(SearchListener listener : searchListeners){
listener.searchStopped(this);
}
}
/**
* Calls {@link SearchListener#newBestSolution(Search, Solution, double)} on every attached search listener.
* Should only be executed when search is active (initializing, running or terminating).
*
* @param newBestSolution new best solution
* @param newBestSolutionEvaluation evaluation of new best solution
*/
private void fireNewBestSolution(SolutionType newBestSolution, double newBestSolutionEvaluation){
for(SearchListener listener : searchListeners){
listener.newBestSolution(this, newBestSolution, newBestSolutionEvaluation);
}
}
/**
* Calls {@link SearchListener#stepCompleted(Search, long)} on every attached search listener.
* Should only be executed when search is active (initializing, running or terminating).
*
* @param numSteps number of steps completed so far (during the current run)
*/
private void fireStepCompleted(long numSteps){
for(SearchListener listener : searchListeners){
listener.stepCompleted(this, numSteps);
}
}
/**
* Calls {@link SearchListener#statusChanged(Search, SearchStatus)} on every attached search listener.
* Should only be called exactly once for every status update.
*
* @param newStatus new search status
*/
private void fireStatusChanged(SearchStatus newStatus){
for(SearchListener listener : searchListeners){
listener.statusChanged(this, newStatus);
}
}
/*****************/
/* SEARCH STATUS */
/*****************/
/**
* Get the current search status. The status may be IDLE, INITIALIZING, RUNNING, TERMINATING or DISPOSED.
*
* @return current search status
*/
public SearchStatus getStatus(){
// synchronize with status updates
synchronized(statusLock){
return status;
}
}
/**
* Returns a lock to be acquired when executing blocks of code that can not be interrupted by a status update.
* All status updates are synchronized using this lock. Whenever a fixed status is required during execution of
* a code block, this can be obtained by synchronizing the block using this status lock.
*
* @return status lock used for synchronization with status updates
*/
protected Object getStatusLock(){
return statusLock;
}
/**
* Asserts that the search is currently idle, more precisely that its status is equal to {@link SearchStatus#IDLE}.
* If not, a {@link SearchException} is thrown, which includes the given errorMessage
and the current
* search status (different from IDLE).
*
* @throws SearchException if the search is not idle
* @param errorMessage message to be included in the {@link SearchException} thrown if the search is not idle
*/
protected void assertIdle(String errorMessage){
// synchronize with status updates
synchronized(statusLock){
if(status != SearchStatus.IDLE){
// not idle, throw exception
throw new SearchException(errorMessage + " (current status: " + status + "; required: IDLE)");
}
}
}
/******************************************/
/* STATE ACCESSORS (RETAINED ACROSS RUNS) */
/******************************************/
/**
* Returns the best solution found so far. It is guaranteed that this solution is valid, in the sense that
* {@link Problem#rejectSolution(Solution)} returns false
. The best solution is retained
* across subsequent runs of the same search. May return null
if no solutions have been evaluated
* yet, for example when the search has just been created.
*
* @return best solution found so far, if already defined; null
otherwise
*/
public SolutionType getBestSolution(){
return bestSolution;
}
/**
* Get the evaluation of the best solution found so far. The best solution and its evaluation are retained
* across subsequent runs of the same search. If the best solution is not yet defined, i.e. when {@link #getBestSolution()}
* return null
, the result of this method is undefined; in such case it may return any arbitrary value.
*
* @return evaluation of best solution, if already defined; arbitrary value otherwise
*/
public double getBestSolutionEvaluation(){
return bestSolutionEvaluation;
}
/****************************/
/* PROTECTED STATE MUTATORS */
/****************************/
/**
*
* Checks whether a new best solution has been found and updates it accordingly.
* The best solution is updated only if the new solution is not rejected
* (see {@link Problem#rejectSolution(Solution)}) and
*
*
* - no best solution had been set before, or
* - the new solution has a better evaluation
*
*
* If the new solution is rejected, or has a worse evaluation than the current best solution, this
* method has no effect. Note that the best solution is retained across subsequent runs of
* the same search.
*
*
* @param newSolution newly constructed solution
* @return true
if the given solution is accepted as the new best solution
*/
protected boolean updateBestSolution(SolutionType newSolution){
// check if new solution is not rejected
if(!problem.rejectSolution(newSolution)){
// evaluate solution
double eval = problem.evaluate(newSolution);
// update, if better
return updateBestSolution(newSolution, eval);
}
// solution is rejected
return false;
}
/**
*
* Checks whether a new best solution has been found and updates it accordingly,
* assuming that the given solution is not rejected by the problem (see
* {@link Problem#rejectSolution(Solution)} and has already been evaluated.
* This method should only be called for solutions for which it has already been
* verified that they are not rejected, as this will not be checked here.
* Else, {@link #updateBestSolution(Solution)} should be used. This alternative
* method is specifically introduced to avoid unnecessary re-evaluation and
* re-validation of already evaluated, valid solutions.
*
*
* The best solution is updated if and only if
*
*
* - no best solution had been set before, or
* - the new solution has a better evaluation
*
*
* If the new solution has a worse evaluation than the current best solution, calling this
* method has no effect. Note that the best solution is retained across subsequent
* runs of the same search.
*
*
* @param newSolution newly constructed solution, which is known not to be rejected
* @param newSolutionEvaluation already computed evaluation of the given solution
* @return true
if the given solution is accepted as the new best solution
*/
protected boolean updateBestSolution(SolutionType newSolution, double newSolutionEvaluation){
// check if new solution has better evaluation, or no best solution set
double delta = computeDelta(newSolutionEvaluation, getBestSolutionEvaluation());
if(bestSolution == null || delta > 0){
// flag improvement
improvementDuringCurrentStep = true;
// store last improvement time
lastImprovementTime = System.currentTimeMillis();
// update minimum delta (only in case previous best solution was set)
if(bestSolution != null){
// update if first delta, or smaller than previous minimum
if(minDelta == JamesConstants.INVALID_DELTA || delta < minDelta){
minDelta = delta;
}
}
// update best solution: create copy because new solution
// might be further modified in subsequent search steps
bestSolution = Solution.checkedCopy(newSolution);
bestSolutionEvaluation = newSolutionEvaluation;
// log
logger.debug("{}: new best solution: {}", this, bestSolutionEvaluation);
// fire callback
fireNewBestSolution(bestSolution, bestSolutionEvaluation);
// found improvement
return true;
}
// no improvement
return false;
}
/*****************************************/
/* METADATA APPLYING TO CURRENT RUN ONLY */
/*****************************************/
/**
*
* Get the runtime of the current (or last) run, in milliseconds. The precise return value
* depends on the status of the search:
*
*
* -
* If the search is either RUNNING or TERMINATING, this method returns the time elapsed since
* the current run was started.
*
* -
* If the search is IDLE or DISPOSED, the total runtime of the last run is returned, if any. Before
* the first run, {@link JamesConstants#INVALID_TIME_SPAN} is returned.
*
* -
* While INITIALIZING the current run, {@link JamesConstants#INVALID_TIME_SPAN} is returned.
*
*
*
* The return value is always positive, except in those cases when {@link JamesConstants#INVALID_TIME_SPAN} is returned.
*
*
* @return runtime of the current (or last) run, in milliseconds
*/
public long getRuntime(){
// depends on status: synchronize with status updates
synchronized(statusLock){
if(status == SearchStatus.INITIALIZING){
// initializing
return JamesConstants.INVALID_TIME_SPAN;
} else if (status == SearchStatus.IDLE || status == SearchStatus.DISPOSED){
// idle or disposed: check if ran before
if(stopTime == JamesConstants.INVALID_TIMESTAMP){
// did not run before
return JamesConstants.INVALID_TIME_SPAN;
} else {
// return total runtime of last run
return stopTime - startTime;
}
} else {
// running or terminating
return System.currentTimeMillis() - startTime;
}
}
}
/**
*
* Get the number of completed steps during the current (or last) run. The precise return value
* depends on the status of the search:
*
*
* -
* If the search is either RUNNING or TERMINATING, this method returns the number of steps completed
* since the current run was started.
*
* -
* If the search is IDLE or DISPOSED, the total number of steps completed during the last run is returned,
* if any. Before the first run, {@link JamesConstants#INVALID_STEP_COUNT}.
*
* -
* While INITIALIZING the current run, {@link JamesConstants#INVALID_STEP_COUNT} is returned.
*
*
*
* The return value is always positive, except in those cases when {@link JamesConstants#INVALID_STEP_COUNT}
* is returned.
*
*
* @return number of steps completed during the current (or last) run
*/
public long getSteps(){
// depends on status: synchronize with status updates
synchronized(statusLock){
if(status == SearchStatus.INITIALIZING){
// initializing
return JamesConstants.INVALID_STEP_COUNT;
} else {
// idle, running, terminating or disposed
return currentSteps;
}
}
}
/**
*
* Get the amount of time elapsed during the current (or last) run, without finding any further improvement
* (in milliseconds). The precise return value depends on the status of the search:
*
*
* -
* If the search is either RUNNING or TERMINATING, but no improvements have yet been made during the current
* run, the returned value is equal to the current runtime; else it reflects the time elapsed since the last
* improvement during the current run.
*
* -
* If the search is IDLE or DISPOSED, but has been run before, the time without improvement observed during the
* last run, up to the point when this run completed, is returned. Before the first run, the return value is
* {@link JamesConstants#INVALID_TIME_SPAN}.
*
* -
* While INITIALIZING the current run, {@link JamesConstants#INVALID_TIME_SPAN} is returned.
*
*
*
* The return value is always positive, except in those cases when {@link JamesConstants#INVALID_TIME_SPAN}
* is returned.
*
*
* @return time without finding improvements during the current (or last) run, in milliseconds
*/
public long getTimeWithoutImprovement(){
// depends on status: synchronize with status updates
synchronized(statusLock){
if(status == SearchStatus.INITIALIZING){
// initializing
return JamesConstants.INVALID_TIME_SPAN;
} else {
// idle, running, terminating or disposed: check last improvement time
if(lastImprovementTime == JamesConstants.INVALID_TIMESTAMP){
// no improvement made during current/last run, or did not yet run: equal to total runtime
return getRuntime();
} else {
// running or ran before, and improvement made during current/last run
if(status == SearchStatus.IDLE || status == SearchStatus.DISPOSED){
// idle or disposed: return last time without improvement of previous run
return stopTime - lastImprovementTime;
} else {
// running or terminating: return time elapsed since last improvement
return System.currentTimeMillis() - lastImprovementTime;
}
}
}
}
}
/**
*
* Get the number of consecutive steps completed during the current (or last) run, without finding
* any further improvement. The precise return value depends on the status of the search:
*
*
* -
* If the search is either RUNNING or TERMINATING, but no improvements have yet been made during the current
* run, the returned value is equal to the total number of steps completed so far; else it reflects the number
* of steps completed since the last improvement during the current run.
*
* -
* If the search is IDLE or DISPOSED, but has been run before, the number of steps without improvement observed
* during the last run, up to the point when this run completed, is returned. Before the first run, the return
* value is {@link JamesConstants#INVALID_STEP_COUNT}.
*
* -
* While INITIALIZING the current run, {@link JamesConstants#INVALID_STEP_COUNT} is returned.
*
*
*
* The return value is always positive, except in those cases when {@link JamesConstants#INVALID_STEP_COUNT}
* is returned.
*
*
* @return number of consecutive completed steps without finding improvements during the current (or last) run
*/
public long getStepsWithoutImprovement(){
// depends on status: synchronize with status updates
synchronized(statusLock){
if(status == SearchStatus.INITIALIZING){
// initializing
return JamesConstants.INVALID_STEP_COUNT;
} else {
if(stepsSinceLastImprovement == JamesConstants.INVALID_STEP_COUNT){
// no improvement made during current/last run, or not yet run: equal to total step count
return getSteps();
} else {
// running or ran before, and improvement made during current/last run
return stepsSinceLastImprovement;
}
}
}
}
/**
*
* Get the minimum improvement in evaluation of a new best known solution over the previous best known solution,
* found during the current (or last) run. The precise return value depends on the status of the search:
*
*
* -
* If the search is either RUNNING or TERMINATING, but no improvements have yet been made during the current
* run, {@link JamesConstants#INVALID_DELTA} is returned. Else, the minimum observed delta over all improvements
* made during the current run is returned.
*
* -
* If the search is IDLE or DISPOSED, but has been run before, the minimum delta observed during the last run is returned.
* Before the first run, the return value is {@link JamesConstants#INVALID_DELTA}.
*
* -
* While INITIALIZING the current run, {@link JamesConstants#INVALID_DELTA} is returned.
*
*
*
* The return value is always positive, except in those cases when {@link JamesConstants#INVALID_DELTA} is returned.
* It corresponds to increase when solving a maximization problem, and decrease in case of a minimization problem.
*
*
* @return minimum delta of improvements observed during current (or last) run
*/
public double getMinDelta(){
// depends on status: synchronize with status updates
synchronized(statusLock){
if(status == SearchStatus.INITIALIZING){
// initializing
return JamesConstants.INVALID_DELTA;
} else {
// idle, running or terminating
return minDelta;
}
}
}
/*********************/
/* PRIVATE UTILITIES */
/*********************/
/**
* Indicates whether the search should continue, by verifying whether its status is not set to {@link SearchStatus#TERMINATING}.
* Once the search has been started, this method will return true
as long as {@link #stop()} has not been called.
* During that time, {@link #searchStep()} will be repeatedly called from a loop that uses {@link #continueSearch()} as its
* stop condition.
*
* @return true
if the search status is not {@link SearchStatus#TERMINATING}
*/
private boolean continueSearch(){
return status != SearchStatus.TERMINATING;
}
/***********************/
/* PROTECTED UTILITIES */
/***********************/
/**
* Computes the amount of improvement of currentEvaluation
over previousEvaluation
, taking into
* account whether a maximization or minimization problem is being solved, where positive deltas indicate improvement.
* In case of a maximization problem, the amount of increase is returned, which is equal to
* currentEvaluation - previousEvaluation
* while the amount of decrease, equal to
* previousEvaluation - currentEvaluation
* is returned when solving a minimization problem.
*
* @param currentEvaluation evaluation to be compared with previous evaluation
* @param previousEvaluation previous evaluation
* @return amount of improvement of current evaluation over previous evaluation
*/
protected double computeDelta(double currentEvaluation, double previousEvaluation){
if(problem.isMinimizing()){
// minimization problem: return decrease
return previousEvaluation - currentEvaluation;
} else {
// maximization problem: return increase
return currentEvaluation - previousEvaluation;
}
}
/*********************************************************************/
/* PROTECTED METHODS TO INITIALIZE, FINALIZE AND DISPOSE THE SEARCH */
/*********************************************************************/
/**
* This method is called when a search run is started, to perform initialization and/or validation of the search
* configuration. It may throw a {@link SearchException} if initialization fails because the search has not been
* configured validly. Moreover, any {@link JamesRuntimeException} could be thrown when initialization depends on
* malfunctioning components. The default implementation resets all general per run metadata. Therefore, it is of
* utmost importance to call super.searchStart()
in any overriding implementation.
*
* @throws SearchException if initialization fails, e.g. because the search has not been configured validly
* @throws JamesRuntimeException in general, any {@link JamesRuntimeException} may be thrown
* in case of a malfunctioning component used during initialization
*/
protected void searchStarted() {
startTime = System.currentTimeMillis();
stopTime = JamesConstants.INVALID_TIMESTAMP;
currentSteps = 0;
lastImprovementTime = JamesConstants.INVALID_TIMESTAMP;
stepsSinceLastImprovement = JamesConstants.INVALID_STEP_COUNT;
minDelta = JamesConstants.INVALID_DELTA;
}
/**
* This method is called when a search run has completed and may be used to perform some finalization. Any
* {@link JamesRuntimeException} may be thrown when finalization depends on malfunctioning search components.
* The default implementation ensures that the total runtime of the last run, if applicable, will be returned
* when calling {@link #getRuntime()} on an idle search. Therefore, it is of utmost importance to call
* super.searchStopped()
in any overriding implementation.
*
* @throws JamesRuntimeException in general, any {@link JamesRuntimeException} may be thrown
* in case of a malfunctioning component used during finalization
*/
protected void searchStopped() {
stopTime = System.currentTimeMillis();
}
/**
* This method is called when a search is disposed, immediately before the search status is updated to DISPOSED.
* The default implementation is empty but should be overridden when a specific search uses resources that have to
* be released (e.g. an active thread pool) when the search is no longer used. Any {@link JamesRuntimeException} may
* be thrown when trying to release malfunctioning resources.
*
* @throws JamesRuntimeException in general, any {@link JamesRuntimeException} may be thrown
* when trying to release malfunctioning resources
*/
protected void searchDisposed(){}
/************************************************************************/
/* ABSTRACT PROTECTED METHOD TO BE IMPLEMENTED IN EVERY SPECIFIC SEARCH */
/************************************************************************/
/**
* This method is iteratively called while the search is running and should be implemented in every specific
* search according to the corresponding search strategy. When a search comes to its natural end, it should call
* {@link #stop()} from within this method, which will cause the search loop to terminate and prevent further
* steps to be executed. Searches consisting of a single step may simply implement their strategy here and
* immediately call {@link #stop()} at the end of the execution of the step, so that it will be executed
* exactly once.
*
* @throws JamesRuntimeException any {@link JamesRuntimeException} may be thrown during a search step, when
* the algorithm has been supplied with a malfunctioning component
*/
abstract protected void searchStep();
}