All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jamesframework.core.search.algo.ParallelTempering Maven / Gradle / Ivy

Go to download

The James core module is part of the James framework for optimization using local search metaheuristics in Java. The core contains general components to model problems, objectives and constraints, as well as generic algorithms to solve the problems. Moreover, the core provides implementations of specific utilities for subset selection.

There is a newer version: 1.2
Show newest version
//  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.algo;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadLocalRandom;
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.NeighbourhoodSearch;
import org.jamesframework.core.search.Search;
import org.jamesframework.core.search.SearchStatus;
import org.jamesframework.core.search.SingleNeighbourhoodSearch;
import org.jamesframework.core.search.listeners.SearchListener;
import org.jamesframework.core.search.neigh.Neighbourhood;
import org.jamesframework.core.search.stopcriteria.MaxSteps;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 

* Parallel tempering algorithm which uses several Metropolis search replicas with different temperatures in a given range, * where good solutions are pushed towards cool replicas for the sake of convergence, while bad solutions are pushed towards * hot replicas in an attempt to find further improvements. Each step of parallel tempering consists of the following two actions: *

*
    *
  1. * Every replica performs a fixed number of steps (defaults to 500) in an attempt to improve its own solution. *
  2. *
  3. * Solutions of adjacent replica (ordered by temperature) are considered to be swapped. Solutions of replicas * \(R_1\) and \(R_2\) with temperatures \(T_1\) and \(T_2\) (\(T_1 < T_2\)) and current solution evaluation * \(E_1\) and \(E_2\), respectively, are always swapped if \(\Delta E \ge 0\), where \(\Delta E = computeDelta(E_2,E_1)\) * (see {@link #computeDelta(double, double)}). If \(\Delta E < 0\), solutions are swapped with probability * \[ * e^{(\frac{1}{k_1T_1}-\frac{1}{k_2T_2})\Delta E}, * \] * where \(k_1\) and \(k_2\) are the temperature scale factors of replica \(R_1\) and \(R_2\), respectively * (scale factors default to 1, see {@link MetropolisSearch}). *
  4. *
*

* All replicas use the same neighbourhood which is specified when creating the parallel tempering search. If an initial * solution is set, a copy of this solution is set as initial solution in each replica. Else, each replica starts with a * distinct randomly generated solution. *

*

* The overall best solution found by all replicas is tracked and eventually returned by the parallel tempering algorithm. * The main algorithm does not actively generate nor apply any moves to its current solution, but simply updates it when a * replica has found a new global improvement, in which case the best solution is also updated. After setting a current * solution using {@link #setCurrentSolution(Solution)}, the main algorithm's current solution may differ from its best * solution, until a new global improvement is found and both are again updated. *

*

* The reported number of accepted and rejected moves (see {@link #getNumAcceptedMoves()} and {@link #getNumRejectedMoves()}) * corresponds to the sum of the number of accepted and rejected moves in all replicas, during the current run of the * parallel tempering search. These values are updated with some delay, whenever a Metropolis replica has completed * its current run. *

*

* When creating the parallel tempering algorithm, the number of replicas and a minimum and maximum temperature have to be * specified. Temperatures assigned to the replicas are unique and equally spaced in the desired interval. The number of * replica steps defaults to 500 but it is strongly advised to tune this parameter for every specific problem, e.g. in case * of a computationally expensive objective function, a lower number of steps may be more appropriate. *

*

* Note that every replica runs in a separate thread so that they will be executed in parallel on multi core machines. * Therefore, it is important that the problem (including all of its components such as the objective, constraints, etc.) * and neighbourhood specified at construction are thread-safe. *

* * @param solution type of the problems that may be solved using this search, required to extend {@link Solution} * @author Herman De Beukelaer */ public class ParallelTempering extends SingleNeighbourhoodSearch{ // logger private static final Logger logger = LoggerFactory.getLogger(ParallelTempering.class); // Metropolis replicas private final List> replicas; // number of steps performed by each replica private int replicaSteps; // thread pool for replica execution and corresponding queue of futures of submitted tasks private final ExecutorService pool; private final Queue> futures; // swap base: flipped (0/1) after every step for fair solution swaps private int swapBase; /** *

* Creates a new parallel tempering algorithm, specifying the problem to solve, the neighbourhood used in each replica, the number * of replicas, and the minimum and maximum temperature. The problem and neighbourhood can not be null, the number of * replicas and both temperature bounds should be strictly positive, and the minimum temperature should be smaller than the maximum * temperature. The default name "ParallelTempering" is assigned to the search. *

*

* Note that it is important that the given problem (including all of its components such as the objective, constraints, etc.) and * neighbourhood are thread-safe, because they will be accessed concurrently from several Metropolis searches running in separate * threads. *

* * @throws NullPointerException if problem or neighbourhood are null * @throws IllegalArgumentException if numReplicas, minTemperature or maxTemperature * are not strictly positive, or if minTemperature ≥ maxTemperature * @param problem problem to solve * @param neighbourhood neighbourhood used inside Metropolis search replicas * @param numReplicas number of Metropolis replicas * @param minTemperature minimum temperature of Metropolis replica * @param maxTemperature maximum temperature of Metropolis replica */ public ParallelTempering(Problem problem, Neighbourhood neighbourhood, int numReplicas, double minTemperature, double maxTemperature){ this(null, problem, neighbourhood, numReplicas, minTemperature, maxTemperature); } /** *

* Creates a new parallel tempering algorithm, specifying the problem to solve, the neighbourhood used in each replica, the number * of replicas, the minimum and maximum temperature, and a custom search name. The problem and neighbourhood can not be null, * the number of replicas and both temperature bounds should be strictly positive, and the minimum temperature should be smaller than the * maximum temperature. The search name can be null in which case the default name "ParallelTempering" is assigned. *

*

* Note that it is important that the given problem (including all of its components such as the objective, constraints, etc.) and * neighbourhood are thread-safe, because they will be accessed concurrently from several Metropolis searches running in separate * threads. *

* * @throws NullPointerException if problem or neighbourhood are null * @throws IllegalArgumentException if numReplicas, minTemperature or maxTemperature * are not strictly positive, or if minTemperature ≥ maxTemperature * @param name custom search name * @param problem problem to solve * @param neighbourhood neighbourhood used inside Metropolis search replicas * @param numReplicas number of Metropolis replicas * @param minTemperature minimum temperature of Metropolis replica * @param maxTemperature maximum temperature of Metropolis replica */ @SuppressWarnings("LeakingThisInConstructor") public ParallelTempering(String name, Problem problem, Neighbourhood neighbourhood, int numReplicas, double minTemperature, double maxTemperature){ super(name != null ? name : "ParallelTempering", problem, neighbourhood); // check number of replicas if(numReplicas <= 0){ throw new IllegalArgumentException("Error while creating parallel tempering algorithm: number of replicas should be > 0."); } // check minimum and maximum temperature if(minTemperature <= 0.0){ throw new IllegalArgumentException("Error while creating parallel tempering algorithm: minimum temperature should be > 0.0."); } if(maxTemperature <= 0.0){ throw new IllegalArgumentException("Error while creating parallel tempering algorithm: maximum temperature should be > 0.0."); } if(minTemperature >= maxTemperature){ throw new IllegalArgumentException("Error while creating parallel tempering algorithm: minimum temperature should be smaller than" + " maximum temperature."); } // create replicas replicas = new ArrayList<>(); for(int i=0; i(problem, neighbourhood, temperature)); } // set default replica steps replicaSteps = 500; // create thread pool pool = Executors.newFixedThreadPool(numReplicas); // initialize (empty) futures queue futures = new LinkedList<>(); // set initial swap base swapBase = 0; // listen to events fired by replicas ReplicaListener listener = new ReplicaListener(); for(MetropolisSearch r : replicas){ r.addSearchListener(listener); } } /** * Sets the number of steps performed by each replica in every iteration of the global parallel tempering * algorithm, before considering solution swaps. Defaults to 500. The specified number of steps should * be strictly positive. * * @throws IllegalArgumentException if steps is not strictly positive * @param steps number of steps performed by replicas in each iteration */ public void setReplicaSteps(int steps){ // check number of steps if(steps <= 0){ throw new IllegalArgumentException("Number of replica steps in parallel tempering should be strictly positive."); } // set number this.replicaSteps = steps; } /** * Get the number of steps performed by each replica in every iteration of the global parallel tempering * algorithm, before considering solution swaps. Defaults to 500 and can be changed using {@link #setReplicaSteps(int)}. * * @return number of steps performed by replicas in each iteration */ public int getReplicaSteps(){ return replicaSteps; } /** * Get the list of Metropolis replicas used by this parallel tempering algorithm. Replicas are ordered by temperature * (ascending). This method should be used with care, as modifying the parameters or order of replicas might break * the execution of the parallel tempering search. * * @return Metropolis replicas */ public List> getReplicas(){ return replicas; } /** * Set the same temperature scale factor \(k > 0\) for each replica. Temperatures are multiplied with this factor * in all computations. By default, the scale factor is set to 1 for every replica, see {@link MetropolisSearch}. * This method should be used with care when called while the search is running, as the scale factor update is * not guaranteed to happen atomically for all replicas. * * @param scale temperature scale factor to be set for each replica * @throws IllegalArgumentException if scale is not strictly positive */ public void setTemperatureScaleFactor(double scale){ // update scale factor in every replica for(MetropolisSearch r : replicas){ r.setTemperatureScaleFactor(scale); } } /** * Set the same neighbourhood for each replica. Note that neighbourhood can not * be null and that this method may only be called when the search is idle. * * @param neighbourhood neighbourhood to be set for each replica * @throws NullPointerException if neighbourhood is null * @throws SearchException if the search is not idle */ @Override public void setNeighbourhood(Neighbourhood neighbourhood){ // synchronize with status updates synchronized(getStatusLock()){ // call super super.setNeighbourhood(neighbourhood); // set neighbourhood in every replica for(MetropolisSearch r : replicas){ r.setNeighbourhood(neighbourhood); } } } /** * Set a custom current solution, of which a copy is passed to each replica. Note that solution * can not be null and that this method may only be called when the search is idle. * * @param solution current solution to be set for each replica * @throws NullPointerException if solution is null * @throws SearchException if the search is not idle */ @Override public void setCurrentSolution(SolutionType solution){ // synchronize with status updates synchronized(getStatusLock()){ // call super (also verifies status) super.setCurrentSolution(solution); // pass current solution to every replica (copy!) for(MetropolisSearch r : replicas){ r.setCurrentSolution(Solution.checkedCopy(solution)); } } } /** * Perform a search step, in which every replica performs several steps and solutions of adjacent replicas may be swapped. * * @throws SearchException if an error occurs during concurrent execution of the Metropolis replicas, or if * it is detected that replicas are not correctly ordered by temperature (ascending) * @throws JamesRuntimeException if depending on malfunctioning components (problem, neighbourhood, replicas, ...) */ @Override protected void searchStep() { // submit replicas for execution in thread pool for(MetropolisSearch r : replicas){ futures.add(pool.submit(r)); } logger.trace("{}: started {} Metropolis replicas", this, futures.size()); // wait for completion of all replicas and remove corresponding future logger.trace("{}: waiting for replicas to finish", this); while(!futures.isEmpty()){ // remove next future from queue and wait until it has completed try{ futures.poll().get(); logger.trace("{}: {}/{} replicas finished", this, replicas.size()-futures.size(), replicas.size()); } catch (InterruptedException | ExecutionException ex){ throw new SearchException("An error occured during concurrent execution of Metropolis replicas " + "in the parallel tempering algorithm.", ex); } } logger.trace("{}: swapping solutions", this); // consider swapping solutions of adjacent replicas for(int i=swapBase; i r1 = replicas.get(i); MetropolisSearch r2 = replicas.get(i+1); // compute delta double delta = computeDelta(r2.getCurrentSolutionEvaluation(), r1.getCurrentSolutionEvaluation()); // check if solutions should be swapped boolean swap = false; if(delta >= 0){ // always swap swap = true; } else { // randomized swap (with probability p) double b1 = 1.0 / (r1.getTemperatureScaleFactor() * r1.getTemperature()); double b2 = 1.0 / (r2.getTemperatureScaleFactor() * r2.getTemperature()); double p = Math.exp((b1 - b2) * delta); // double check: p should be a probability in [0,1], else the replicas are not // correctly orederd by temperature (ascending) if(p > 1.0){ throw new SearchException("Error in parallel tempering algorithm: replicas are not correctly ordered by " + "temperature (ascending)."); } // generate random number double r = ThreadLocalRandom.current().nextDouble(); // swap with probability p if(r < p){ swap = true; } } // swap solutions if(swap){ SolutionType temp = r1.getCurrentSolution(); r1.setCurrentSolution(r2.getCurrentSolution()); r2.setCurrentSolution(temp); } } // flip swap base swapBase = 1 - swapBase; } /** * When disposing a parallel tempering search, it will dispose each contained Metropolis replica and will * shut down the thread pool used for concurrent execution of replicas. */ @Override protected void searchDisposed(){ super.searchDisposed(); // dispose replicas for(MetropolisSearch r : replicas){ r.dispose(); } // shut down thread pool pool.shutdown(); } /** * Private listener attached to each replica, to keep track of the global best solution and aggregated number of * accepted and rejected moves, and to terminate a replica when it has performed the desired number of steps. */ private class ReplicaListener implements SearchListener{ /*******************************/ /* CALLBACKS FIRED BY REPLICAS */ /*******************************/ /** * Parallel tempering algorithm listens to its Metropolis replicas: whenever a new best solution is reported * inside a replica, it is verified whether this is also a global improvement. If so, the main algorithm's current * and best solution are both updated to refer to this new global best solution. This method is synchronized to * avoid conflicting updates by replicas running in separate threads. * * @param replica Metropolis replica that has found a (local) best solution * @param newBestSolution new best solution found in replica * @param newBestSolutionEvaluation evaluation of new best solution */ @Override public synchronized void newBestSolution(Search replica, final SolutionType newBestSolution, final double newBestSolutionEvaluation) { // update main algorithm's current and best solution (skip validation, // already known to be valid if reported as best solution by a replica) updateCurrentAndBestSolution(newBestSolution, newBestSolutionEvaluation, true); } /** * Parallel tempering algorithm listens to its Metropolis replicas: whenever a replica has completed a step, it is verified whether * the desired number of steps have been performed and, if so, the replica is stopped. This approach is favoured here over attaching * a generic maximum steps stop criterion (see {@link MaxSteps}) to each replica because of its finer granularity, i.e. because it is * checked after every single step. * * @param replica Metropolis replica that completed a search step * @param numSteps number of steps completed so far */ @Override public void stepCompleted(Search replica, long numSteps) { if (numSteps >= replicaSteps){ replica.stop(); } } /** * Parallel tempering algorithm listens to its Metropolis replicas: whenever a replica has finished its current run, * the number of accepted and rejected moves during this run are accounted for by increase the global counters. This * method is synchronized to avoid concurrent updates of the global number of accepted and rejected moves, as the * replicas run in separate threads. * * @param replica Metropolis replica that has finished its current run */ @Override public synchronized void searchStopped(Search replica) { // cast to neighbourhood search (should never fail, as this callback is only fired by Metropolis searches) NeighbourhoodSearch nreplica = (NeighbourhoodSearch) replica; // update number of accepted moves incNumAcceptedMoves(nreplica.getNumAcceptedMoves()); // update number of rejected moves incNumRejectedMoves(nreplica.getNumRejectedMoves()); } /** * Empty callback: no action taken here when a replica has started. * * @param replica ignored */ @Override public void searchStarted(Search replica) {} /** * Empty callback: no action taken here when a replica enters a new status. * * @param search ignored * @param newStatus ignored */ @Override public void statusChanged(Search search, SearchStatus newStatus) {} } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy