/* Copyright 2012 Google, Inc.
*
* 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.arbeitspferde.groningen.hypothesizer;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import org.arbeitspferde.groningen.utility.Clock;
import org.arbeitspferde.groningen.utility.SystemClock;
import org.uncommons.watchmaker.framework.CandidateFactory;
import org.uncommons.watchmaker.framework.EvaluatedCandidate;
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 java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* This class provides incremental version of GA, where the calling code has control over the
* generations. We cannot use the default {@link GenerationalEvolutionEngine} for Groningen since
* the fitness is computed after the experiment is run on the servers. {@link
* GenerationalEvolutionEngine} will run the generations until the stopping conditions are met.
* Instead, this class evolves one generation at a time and returns the population back.
*
* This class is not thread safe.
*
* @param <T> the type of elements this evolution engine handles.
*/
public class IncrementalEvolutionEngine<T> extends GenerationalEvolutionEngine<T> {
private final Set<EvolutionObserver<T>> observers =
new CopyOnWriteArraySet<>();
private final CandidateFactory<T> candidateFactory;
private final EvolutionaryOperator<T> evolutionScheme;
private final FitnessEvaluator<? super T> fitnessEvaluator;
private final SelectionStrategy<? super T> selectionStrategy;
private final Random rng;
private final int populationSize;
private final int eliteCount;
private final TerminationCondition[] conditions;
private final Clock clock;
private int currentGenerationIndex;
private long startTime;
private boolean initialized;
private boolean terminated;
public IncrementalEvolutionEngine(
CandidateFactory<T> candidateFactory,
EvolutionaryOperator<T> evolutionScheme,
FitnessEvaluator<? super T> fitnessEvaluator,
SelectionStrategy<? super T> selectionStrategy,
Random rng,
int populationSize,
int eliteCount,
TerminationCondition... conditions) {
this(
candidateFactory,
evolutionScheme,
fitnessEvaluator,
selectionStrategy,
rng,
populationSize,
eliteCount,
new SystemClock(),
conditions);
}
@VisibleForTesting IncrementalEvolutionEngine(
CandidateFactory<T> candidateFactory,
EvolutionaryOperator<T> evolutionScheme,
FitnessEvaluator<? super T> fitnessEvaluator,
SelectionStrategy<? super T> selectionStrategy,
Random rng,
int populationSize,
int eliteCount,
Clock clock,
TerminationCondition... conditions) {
super(candidateFactory, evolutionScheme, fitnessEvaluator, selectionStrategy, rng);
Preconditions.checkArgument(
eliteCount < populationSize, "Elite count has to be smaller that the population size.");
this.candidateFactory = candidateFactory;
this.evolutionScheme = evolutionScheme;
this.fitnessEvaluator = fitnessEvaluator;
this.selectionStrategy = selectionStrategy;
this.rng = rng;
this.populationSize = populationSize;
this.eliteCount = eliteCount;
this.conditions = conditions;
this.clock = clock;
super.setSingleThreaded(true);
}
/**
* Performs the first step in the GA, which is to create the initial population by using {@link
* CandidateFactory}. The population is returned to the caller. This method can be called only
* once, and before calling {#performNextStep()} method.
*
* @return Initial population.
*/
public List<T> performFirstStep() {
if (initialized) {
throw new IllegalStateException("Evolutionary process has already started.");
}
if (terminated) {
throw new IllegalStateException("Evolutionary process has been terminated already.");
}
startTime = clock.now().getMillis();
List<T> population =
candidateFactory.generateInitialPopulation(populationSize, Collections.<T>emptySet(), rng);
initialized = true;
return population;
}
/**
* Performs the subsequent steps in the GA, which is to evolve the population one generation at a
* time. Unlike other evolution engines, here the population is passed in by the calling code.
* This method computes the fitness of each individual in the population, applies the evolutionary
* operators on the population, and returns the population. Call {#performFirstStep()} method
* before calling this method.
*
* @param population Current population.
* @return Evolved population.
*/
public List<T> performNextStep(List<T> population) {
if (!initialized) {
throw new IllegalStateException("Evolutionary process has not been started yet.");
}
if (terminated) {
throw new IllegalStateException("Evolutionary process has been terminated already.");
}
Preconditions.checkArgument(
population != null && population.size() == populationSize, "Invalid population.");
// Calculate the fitness scores for each member of the population.
List<EvaluatedCandidate<T>> evaluatedPopulation = evaluatePopulation(population);
// Sort the individuals according to their fitness. Individual with highest fitness value will
// be at the top of the population.
EvolutionUtils.sortEvaluatedPopulation(evaluatedPopulation, fitnessEvaluator.isNatural());
// Generate population data
PopulationData<T> data = EvolutionUtils.getPopulationData(
evaluatedPopulation, fitnessEvaluator.isNatural(), 0, currentGenerationIndex, startTime);
notifyPopulationChange(data, evaluatedPopulation);
// Check if we have met the termination conditions. Only check when there are conditions.
List<TerminationCondition> satisfiedConditions = null;
if (conditions[0] != null) {
satisfiedConditions = EvolutionUtils.shouldContinue(data, conditions);
}
// If not met, evolve the population by one generation, and return the evolved population.
if (satisfiedConditions == null) {
++currentGenerationIndex;
population = performEvolution(evaluatedPopulation, eliteCount, rng);
} else {
terminated = true;
}
// Otherwise, just return the current population
return population;
}
/**
* Returns whether the GA has met the termination conditions. If terminated, calling code should
* not call {#performFirstStep()} or {#performNextStep()} method any more.
*
* @return Whether the GA has met the termination conditions or not.
*/
public boolean isTerminated() {
return terminated;
}
/**
* Performs single evolution.
*/
private List<T> performEvolution(
List<EvaluatedCandidate<T>> evaluatedPopulation, int eliteCount, Random rng) {
List<T> population = Lists.newArrayListWithCapacity(evaluatedPopulation.size());
// First perform any elitist selection.
List<T> elite = Lists.newArrayListWithCapacity(eliteCount);
Iterator<EvaluatedCandidate<T>> iterator = evaluatedPopulation.iterator();
while (elite.size() < eliteCount) {
elite.add(iterator.next().getCandidate());
}
// Then select candidates that will be operated on to create the evolved
// portion of the next generation.
population.addAll(
selectionStrategy.select(evaluatedPopulation, fitnessEvaluator.isNatural(),
evaluatedPopulation.size() - eliteCount, rng));
// Then evolve the population.
population = evolutionScheme.apply(population, rng);
// When the evolution is finished, add the elite individuals to the population.
population.addAll(elite);
return population;
}
/**
* Adds a listener to receive status updates on the evolution progress. 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.
*
* @param observer An evolution observer call-back.
*/
public void addEvolutionObserver(EvolutionObserver<T> observer) {
observers.add(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, List<EvaluatedCandidate<T>> population) {
for (EvolutionObserver<T> observer : observers) {
observer.populationUpdate(data, population);
}
}
}