//============================================================================= // Copyright 2006-2010 Daniel W. Dyer // // 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.uncommons.watchmaker.framework.islands; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Random; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import org.uncommons.watchmaker.framework.CandidateFactory; import org.uncommons.watchmaker.framework.EvaluatedCandidate; import org.uncommons.watchmaker.framework.EvolutionEngine; import org.uncommons.watchmaker.framework.EvolutionObserver; import org.uncommons.watchmaker.framework.EvolutionUtils; import org.uncommons.watchmaker.framework.EvolutionaryOperator; import org.uncommons.watchmaker.framework.FitnessEvaluator; import org.uncommons.watchmaker.framework.GenerationalEvolutionEngine; import org.uncommons.watchmaker.framework.PopulationData; import org.uncommons.watchmaker.framework.SelectionStrategy; import org.uncommons.watchmaker.framework.TerminationCondition; import org.uncommons.watchmaker.framework.termination.GenerationCount; /** * An implementation of island evolution in which multiple independent populations are evolved in * parallel with periodic migration of individuals between islands. * @param <T> The type of entity that is to be evolved. * @author Daniel Dyer */ public class IslandEvolution<T> { private final List<EvolutionEngine<T>> islands; private final Migration<? super T> migration; private final boolean naturalFitness; private final Random rng; private final Set<IslandEvolutionObserver<? super T>> observers = new CopyOnWriteArraySet<IslandEvolutionObserver<? super T>>(); private List<TerminationCondition> satisfiedTerminationConditions; /** * Create an island system with the specified number of identically-configured islands. * If you want more fine-grained control over the configuration of each island, use the * {@link #IslandEvolution(List, Migration, boolean, Random)} constructor, which accepts * a list of pre-created islands (each is an instance of {@link EvolutionEngine}). * @param islandCount The number of separate islands that will be part of the system. * @param migration A migration strategy for moving individuals between islands at the * end of an epoch. * @param candidateFactory Generates the initial population for each island. * @param evolutionScheme The evolutionary operator, or combination of evolutionary operators, * used on each island. * @param fitnessEvaluator The fitness function used on each island. * @param selectionStrategy The selection strategy used on each island. * @param rng A source of randomness, used by all islands. * @see #IslandEvolution(List, Migration, boolean, Random) */ public IslandEvolution(int islandCount, Migration<? super T> migration, CandidateFactory<T> candidateFactory, EvolutionaryOperator<T> evolutionScheme, FitnessEvaluator<? super T> fitnessEvaluator, SelectionStrategy<? super T> selectionStrategy, Random rng) { this(createIslands(islandCount, candidateFactory, evolutionScheme, fitnessEvaluator, selectionStrategy, rng), migration, fitnessEvaluator.isNatural(), rng); } /** * Create an island evolution system from a list of pre-configured islands. This constructor * gives more control over the configuration of individual islands than the alternative constructor. * The other constructor should be used where possible to avoid having to explicitly create each * island. * @param islands A list of pre-configured islands. * @param migration A migration strategy for moving individuals between islands at the * end of an epoch. * @param naturalFitness If true, indicates that higher fitness values mean fitter * individuals. If false, indicates that fitter individuals will have lower scores. * @param rng A source of randomness, used by all islands. * @see #IslandEvolution(int, Migration, CandidateFactory, EvolutionaryOperator, FitnessEvaluator, * SelectionStrategy, Random) */ public IslandEvolution(List<EvolutionEngine<T>> islands, Migration<? super T> migration, boolean naturalFitness, Random rng) { this.islands = islands; this.migration = migration; this.naturalFitness = naturalFitness; this.rng = rng; for (int i = 0; i < islands.size(); i++) { final int islandIndex = i; EvolutionEngine<T> island = islands.get(islandIndex); island.addEvolutionObserver(new EvolutionObserver<T>() { public void populationUpdate(PopulationData<? extends T> populationData) { for (IslandEvolutionObserver<? super T> islandObserver : observers) { islandObserver.islandPopulationUpdate(islandIndex, populationData); } } }); } } /** * Helper method used by the constructor to create the individual islands if they haven't * been provided already (via the other constructor). */ private static <T> List<EvolutionEngine<T>> createIslands(int islandCount, CandidateFactory<T> candidateFactory, EvolutionaryOperator<T> evolutionScheme, FitnessEvaluator<? super T> fitnessEvaluator, SelectionStrategy<? super T> selectionStrategy, Random rng) { List<EvolutionEngine<T>> islands = new ArrayList<EvolutionEngine<T>>(islandCount); for (int i = 0; i < islandCount; i++) { GenerationalEvolutionEngine<T> island = new GenerationalEvolutionEngine<T>(candidateFactory, evolutionScheme, fitnessEvaluator, selectionStrategy, rng); island.setSingleThreaded(true); // Don't need fine-grained concurrency when each island is on a separate thread. islands.add(island); } return islands; } /** * <p>Start the evolutionary process on each island and return the fittest candidate so far at the point * any of the termination conditions is satisfied.</p> * * <p><em>If you interrupt the request thread before this method returns, the * method will return prematurely (with the best individual found so far). * After returning in this way, the current thread's interrupted flag * will be set. It is preferable to use an appropritate * {@link org.uncommons.watchmaker.framework.TerminationCondition} rather than interrupting the evolution in * this way.</em></p> * * @param populationSize The population size <em>for each island</em>. Therefore, if you have 5 islands, * setting this parameter to 200 will result in 1000 individuals overall, 200 on each island. * @param eliteCount The number of candidates preserved via elitism <em>on each island</em>. In elitism, * a sub-set of the population with the best fitness scores are preserved unchanged in * the subsequent generation. Candidate solutions that are preserved unchanged through * elitism remain eligible for selection for breeding the remainder of the next generation. * This value must be non-negative and less than the population size. A value of zero * means that no elitism will be applied. * @param epochLength The number of generations that make up an epoch. Islands evolve independently for * this number of generations and then migration occurs at the end of the epoch and the next epoch starts. * @param migrantCount The number of individuals that will be migrated from each island at the end of each * epoch. * @param conditions One or more conditions that may cause the evolution to terminate. * @return The fittest solution found by the evolutionary process on any of the islands. */ public T evolve(int populationSize, int eliteCount, int epochLength, int migrantCount, TerminationCondition... conditions) { ExecutorService threadPool = Executors.newFixedThreadPool(islands.size()); List<List<T>> islandPopulations = new ArrayList<List<T>>(islands.size()); List<EvaluatedCandidate<T>> evaluatedCombinedPopulation = new ArrayList<EvaluatedCandidate<T>>(); PopulationData<T> data = null; List<TerminationCondition> satisfiedConditions = null; int currentEpochIndex = 0; long startTime = System.currentTimeMillis(); while (satisfiedConditions == null) { List<Callable<List<EvaluatedCandidate<T>>>> islandEpochs = createEpochTasks(populationSize, eliteCount, epochLength, islandPopulations); try { List<Future<List<EvaluatedCandidate<T>>>> futures = threadPool.invokeAll(islandEpochs); evaluatedCombinedPopulation.clear(); List<List<EvaluatedCandidate<T>>> evaluatedPopulations = new ArrayList<List<EvaluatedCandidate<T>>>(islands.size()); for (Future<List<EvaluatedCandidate<T>>> future : futures) { List<EvaluatedCandidate<T>> evaluatedIslandPopulation = future.get(); evaluatedCombinedPopulation.addAll(evaluatedIslandPopulation); evaluatedPopulations.add(evaluatedIslandPopulation); } migration.migrate(evaluatedPopulations, migrantCount, rng); EvolutionUtils.sortEvaluatedPopulation(evaluatedCombinedPopulation, naturalFitness); data = EvolutionUtils.getPopulationData(evaluatedCombinedPopulation, naturalFitness, eliteCount, currentEpochIndex, startTime); notifyPopulationChange(data); islandPopulations.clear(); for (List<EvaluatedCandidate<T>> evaluatedPopulation : evaluatedPopulations) { islandPopulations.add(toCandidateList(evaluatedPopulation)); } ++currentEpochIndex; } catch (InterruptedException ex) { Thread.currentThread().interrupt(); } catch (ExecutionException ex) { throw new IllegalStateException(ex); } satisfiedConditions = EvolutionUtils.shouldContinue(data, conditions); } threadPool.shutdownNow(); this.satisfiedTerminationConditions = satisfiedConditions; return evaluatedCombinedPopulation.get(0).getCandidate(); } /** * Create the concurrently-executed tasks that perform evolution on each island. */ private List<Callable<List<EvaluatedCandidate<T>>>> createEpochTasks(int populationSize, int eliteCount, int epochLength, List<List<T>> islandPopulations) { List<Callable<List<EvaluatedCandidate<T>>>> islandEpochs = new ArrayList<Callable<List<EvaluatedCandidate<T>>>>(islands.size()); for (int i = 0; i < islands.size(); i++) { islandEpochs.add(new Epoch<T>(islands.get(i), populationSize, eliteCount, islandPopulations.isEmpty() ? Collections.<T>emptyList() : islandPopulations.get(i), new GenerationCount(epochLength))); } return islandEpochs; } /** * Convert a list of {@link EvaluatedCandidate}s into a simple list of candidates. * @param evaluatedCandidates The population of candidate objects to relieve of their * evaluation wrappers. * @param <T> The type of entity that is being evolved. * @return The candidates, stripped of their fitness scores. */ private static <T> List<T> toCandidateList(List<EvaluatedCandidate<T>> evaluatedCandidates) { List<T> candidates = new ArrayList<T>(evaluatedCandidates.size()); for (EvaluatedCandidate<T> evaluatedCandidate : evaluatedCandidates) { candidates.add(evaluatedCandidate.getCandidate()); } return candidates; } /** * <p>Returns a list of all {@link TerminationCondition}s that are satisfied by the current * state of the island evolution. Usually this list will contain only one item, but it * is possible that mutliple termination conditions will become satisfied at the same * time. In this case the condition objects in the list will be in the same order that * they were specified when passed to the engine.</p> * * <p>If the evolution has not yet terminated (either because it is still in progress or * because it hasn't even been started) then an IllegalStateException will be thrown.</p> * * <p>If the evolution terminated because the request thread was interrupted before any * termination conditions were satisfied then this method will return an empty list.</p> * * @throws IllegalStateException If this method is invoked on an island system before * evolution is started or while it is still in progress. * * @return A list of statisfied conditions. The list is guaranteed to be non-null. The * list may be empty because it is possible for evolution to terminate without any conditions * being matched. The only situation in which this occurs is when the request thread is * interrupted. */ public List<TerminationCondition> getSatisfiedTerminationConditions() { if (satisfiedTerminationConditions == null) { throw new IllegalStateException("EvolutionEngine has not terminated."); } else { return Collections.unmodifiableList(satisfiedTerminationConditions); } } /** * <p>Adds an observer to the evolution. Observers will receives two types of updates: * updates from each individual island at the end of each generation, and updates for * the combined global population at the end of each epoch.</p> * * <p>Updates are dispatched synchronously on the request thread. Observers should * complete their processing and return in a timely manner to avoid holding up * the evolution.</p> * * @param observer The callback that will be notified at the end of each generation and epoch. * * @see #removeEvolutionObserver(IslandEvolutionObserver) */ public void addEvolutionObserver(final IslandEvolutionObserver<? super T> observer) { observers.add(observer); } /** * Remove the specified observer. * @param observer The observer to remove (if it is registered). * * @see #addEvolutionObserver(IslandEvolutionObserver) */ public void removeEvolutionObserver(final IslandEvolutionObserver<? super T> observer) { observers.remove(observer); } /** * Send the population data to all registered observers. * @param data Information about the current state of the population. */ private void notifyPopulationChange(PopulationData<T> data) { for (IslandEvolutionObserver<? super T> observer : observers) { observer.populationUpdate(data); } } }