/*
* Copyright 2015 S. Webber
*
* 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.oakgp.util;
import static java.util.Objects.requireNonNull;
import static org.oakgp.NodeSimplifier.simplify;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import java.util.logging.Logger;
import org.oakgp.Type;
import org.oakgp.evolve.GenerationEvolver;
import org.oakgp.evolve.GenerationEvolverImpl;
import org.oakgp.evolve.GeneticOperator;
import org.oakgp.evolve.crossover.SubtreeCrossover;
import org.oakgp.evolve.mutate.ConstantToFunctionMutation;
import org.oakgp.evolve.mutate.PointMutation;
import org.oakgp.evolve.mutate.SubTreeMutation;
import org.oakgp.function.Function;
import org.oakgp.generate.TreeGenerator;
import org.oakgp.generate.TreeGeneratorImpl;
import org.oakgp.node.ConstantNode;
import org.oakgp.node.Node;
import org.oakgp.primitive.ConstantSet;
import org.oakgp.primitive.FunctionSet;
import org.oakgp.primitive.PrimitiveSet;
import org.oakgp.primitive.PrimitiveSetImpl;
import org.oakgp.primitive.VariableSet;
import org.oakgp.rank.GenerationRanker;
import org.oakgp.rank.RankedCandidate;
import org.oakgp.rank.RankedCandidates;
import org.oakgp.rank.fitness.FitnessFunction;
import org.oakgp.rank.fitness.FitnessFunctionCache;
import org.oakgp.rank.fitness.FitnessFunctionGenerationRanker;
import org.oakgp.rank.tournament.RoundRobinTournament;
import org.oakgp.rank.tournament.TwoPlayerGame;
import org.oakgp.rank.tournament.TwoPlayerGameCache;
import org.oakgp.select.NodeSelectorFactory;
import org.oakgp.select.RankSelectionFactory;
import org.oakgp.terminate.CompositeTerminator;
import org.oakgp.terminate.MaxGenerationsTerminator;
import org.oakgp.terminate.MaxGenerationsWithoutImprovementTerminator;
import org.oakgp.terminate.TargetFitnessTerminator;
/**
* Provides a convenient way to configure and start a genetic programming run.
*
* @see <a href="http://oakgp.org/getting-started-with-oakgp">Getting Started with OakGP</a>
*/
public final class RunBuilder {
private static final Random RANDOM = new JavaUtilRandomAdapter();
private static final double RATIO_VARIABLES = .6;
private static final int DEFAULT_CACHE_SIZE = 10000;
private Type _returnType;
private Random _random = RANDOM;
private PrimitiveSet _primitiveSet;
private GenerationRanker _generationRanker;
private GenerationEvolver _generationEvolver;
private Collection<Node> _initialPopulation;
/** Sets the required {@code Type} associated with the values produced as a result of evaluating the programs that are automatically generated by the run. */
public RandomSetter setReturnType(final Type returnType) {
_returnType = requireNonNull(returnType);
return new RandomSetter();
}
/**
* Provides the option to set a random number generator, or to skip that option and instead configure the primitive set.
* <p>
* If you do not explicitly specify a random number generator then the {@code RunBuilder} will default to using {@link JavaUtilRandomAdapter}. Unless you
* have a specific requirement about how random numbers are generated then the default random number generator will be sufficient.
*/
public final class RandomSetter extends PrimitiveSetSetter {
private RandomSetter() {
}
/** Sets the {@code Random} to use to generate random numbers required by the run. */
public PrimitiveSetSetter setRandom(final Random random) {
_random = requireNonNull(random);
return new PrimitiveSetSetter();
}
}
/** Allows the primitive set to be configured. */
public class PrimitiveSetSetter {
private PrimitiveSetSetter() {
}
/** Sets the functions and terminal nodes that are available for use in the construction of programs generated by the run. */
public GenerationRankerSetter setPrimitiveSet(final PrimitiveSet primitiveSet) {
_primitiveSet = requireNonNull(primitiveSet);
return new GenerationRankerSetter();
}
/** Sets the constants that are available for use in the construction of programs generated by the run. */
public VariablesSetter setConstants(final ConstantNode... constants) {
ConstantSet constantSet = new ConstantSet(constants);
return new VariablesSetter(constantSet);
}
/** Sets the constants that are available for use in the construction of programs generated by the run. */
public VariablesSetter setConstants(final List<ConstantNode> constants) {
return setConstants(constants.toArray(new ConstantNode[constants.size()]));
}
}
/** Allows the variable types to be configured. */
public final class VariablesSetter {
private final ConstantSet constantSet;
private VariablesSetter(final ConstantSet constantSet) {
this.constantSet = constantSet;
}
/** Sets the {@code Type}s to associate with the variables available for use in the construction of programs generated by the run. */
public VariablesRatioSetter setVariables(final Type... variableTypes) {
VariableSet variableSet = VariableSet.createVariableSet(variableTypes);
return new VariablesRatioSetter(constantSet, variableSet);
}
}
/** Allows the ratio of variables to constants to be configured. */
public final class VariablesRatioSetter implements FunctionSetSetter {
private final ConstantSet constantSet;
private final VariableSet variableSet;
private VariablesRatioSetter(ConstantSet constantSet, VariableSet variableSet) {
this.constantSet = constantSet;
this.variableSet = variableSet;
}
/**
* Sets the ratio of terminal nodes that should be variable nodes, rather than constant nodes.
*
* @param ratioVariables
* a value in the range 0 to 1 (inclusive) which specifies the proportion of terminal nodes that should represent variables, rather than
* constants
* @throws IllegalArgumentException
* if {@code ratioVariables} is not in the range 0 to 1 inclusive
*/
public FunctionSetSetter setRatioVariables(final double ratioVariables) {
if (ratioVariables < 0 || ratioVariables > 1) {
throw new IllegalArgumentException("Ratio of variables must be in range 0 to 1, not: " + ratioVariables);
}
return new FunctionSetSetterImpl(constantSet, variableSet, ratioVariables);
}
@Override
public GenerationRankerSetter setFunctions(Function... functions) {
return setRatioVariables(RATIO_VARIABLES).setFunctions(functions);
}
@Override
public GenerationRankerSetter setFunctions(final List<Function> functions) {
return setRatioVariables(RATIO_VARIABLES).setFunctions(functions);
}
}
private final class FunctionSetSetterImpl implements FunctionSetSetter {
private final ConstantSet constantSet;
private final VariableSet variableSet;
private final double ratioVariables;
private FunctionSetSetterImpl(ConstantSet constantSet, VariableSet variableSet, double ratioVariables) {
this.constantSet = constantSet;
this.variableSet = variableSet;
this.ratioVariables = ratioVariables;
}
@Override
public GenerationRankerSetter setFunctions(final Function... functions) {
logFunctionSet(functions);
FunctionSet functionSet = new FunctionSet(functions);
_primitiveSet = new PrimitiveSetImpl(functionSet, constantSet, variableSet, _random, ratioVariables);
return new GenerationRankerSetter();
}
@Override
public GenerationRankerSetter setFunctions(final List<Function> functions) {
return setFunctions(functions.toArray(new Function[functions.size()]));
}
private void logFunctionSet(final Function[] functions) {
boolean first = true;
Arrays.sort(functions, (o1, o2) -> o1.getClass().getName().compareTo(o2.getClass().getName()));
for (Function function : functions) {
if (first) {
first = false;
} else {
System.out.println("<br>");
}
System.out.println("|Class:|" + function.getClass().getName());
System.out.println("|Symbol:|" + function.getDisplayName().replace("&", "&").replace("<", "<").replace(">", ">"));
System.out.println("|Return Type:|" + function.getSignature().getReturnType());
String argumentTypes = function.getSignature().getArgumentTypes().toString();
argumentTypes = argumentTypes.substring(1, argumentTypes.length() - 1);
System.out.println("|Arguments:|" + argumentTypes);
}
}
}
/** Allows the configuration of the mechanism for ranking candidates. */
public final class GenerationRankerSetter {
private GenerationRankerSetter() {
}
/** Set the {@code GenerationRanker} used to rank and sort the candidates of a generation. */
public InitialPopulationSetter setGenerationRanker(final GenerationRanker generationRanker) {
_generationRanker = requireNonNull(generationRanker);
return new InitialPopulationSetter();
}
/** Set the {@code FitnessFunction} used to determine the fitness of a candidate. */
public InitialPopulationSetter setFitnessFunction(final FitnessFunction fitnessFunction) {
requireNonNull(fitnessFunction);
return setGenerationRanker(new FitnessFunctionGenerationRanker(ensureCached(fitnessFunction)));
}
private FitnessFunction ensureCached(final FitnessFunction fitnessFunction) {
if (fitnessFunction instanceof FitnessFunctionCache) {
return fitnessFunction;
} else {
return new FitnessFunctionCache(DEFAULT_CACHE_SIZE, fitnessFunction);
}
}
/** Set the {@code TwoPlayerGame} used to determine the relative fitness of two candidates. */
public InitialPopulationSetter setTwoPlayerGame(final TwoPlayerGame twoPlayerGame) {
requireNonNull(twoPlayerGame);
return setGenerationRanker(new RoundRobinTournament(ensureCached(twoPlayerGame)));
}
private TwoPlayerGame ensureCached(final TwoPlayerGame twoPlayerGame) {
if (twoPlayerGame instanceof TwoPlayerGameCache) {
return twoPlayerGame;
} else {
return new TwoPlayerGameCache(DEFAULT_CACHE_SIZE, twoPlayerGame);
}
}
}
/** Allows the initial population to be specified. */
public final class InitialPopulationSetter {
private InitialPopulationSetter() {
}
/** Set the contents of the initial population. */
public GenerationEvolverSetter setInitialPopulation(final java.util.function.Function<Config, Collection<Node>> initialPopulation) {
return setInitialPopulation(initialPopulation.apply(new Config()));
}
/** Set the contents of the initial population. */
private GenerationEvolverSetter setInitialPopulation(Collection<Node> initialPopulation) {
_initialPopulation = requireNonNull(initialPopulation);
return new GenerationEvolverSetter();
}
/** Set the number of randomly generated trees to include in the initial population. */
public TreeDepthSetter setInitialPopulationSize(final int generationSize) {
return new TreeDepthSetter(generationSize);
}
}
/** Allows configuration of the maximum tree depth of trees randomly generated for the initial population. */
public final class TreeDepthSetter {
private final int generationSize;
private TreeDepthSetter(final int generationSize) {
this.generationSize = requiresPositive(generationSize);
}
/** Set the maximum depth of the trees randomly generated for the initial population. */
public GenerationEvolverSetter setTreeDepth(final int treeDepth) {
requiresPositive(treeDepth);
// NOTE could use a NodeSet rather than an ArrayList - but then the resulting population may be < generationSize (due to duplicates)
// NOTE could generate using a 50:50 split of TreeGeneratorImpl.grow and TreeGeneratorImpl.full
Collection<Node> initialPopulation = new ArrayList<>();
TreeGenerator treeGenerator = TreeGeneratorImpl.grow(_primitiveSet, _random);
for (int i = 0; i < generationSize; i++) {
Node n = treeGenerator.generate(_returnType, treeDepth);
initialPopulation.add(n);
}
return new InitialPopulationSetter().setInitialPopulation(initialPopulation);
}
private int requiresPositive(final int i) {
if (i > 0) {
return i;
} else {
throw new IllegalArgumentException("Expected a positive integer but got: " + i);
}
}
}
/**
* Provides the option to configure the how new generations evolve from existing ones, or to skip that option and instead configure the termination criteria.
* <p>
* If you do not explicitly specify how generations evolve then a default strategy will be used. The default strategy is sufficient for allowing people to
* quickly get started with OakGP
*/
public final class GenerationEvolverSetter extends FirstTerminatorSetter {
private GenerationEvolverSetter() {
}
/** Set how new generations will be created from existing ones. */
public TerminatorSetter setGenerationEvolver(final java.util.function.Function<Config, GenerationEvolver> generationEvolver) {
return setGenerationEvolver(generationEvolver.apply(new Config()));
}
/** Set how new generations will be created from existing ones. */
private TerminatorSetter setGenerationEvolver(GenerationEvolver generationEvolver) {
_generationEvolver = requireNonNull(generationEvolver);
return new FirstTerminatorSetter();
}
}
private class FirstTerminatorSetter implements TerminatorSetter {
private final List<Predicate<RankedCandidates>> terminators = new ArrayList<>();
private FirstTerminatorSetter() {
}
@Override
public TerminatorSetterOrProcessRunner setTerminator(final Predicate<RankedCandidates> terminator) {
terminators.add(requireNonNull(terminator));
return new SubsequentTerminatorSetter(terminators);
}
@Override
public MaxGenerationsTerminatorSetterOrProcessRunner setTargetFitness(double targetFitness) {
return new SubsequentTerminatorSetter(terminators).setTargetFitness(targetFitness);
}
@Override
public MaxGenerationsWithoutImprovementTerminatorSetterOrProcessRunner setMaxGenerations(final int maxGenerations) {
return new MaxGenerationsTerminatorSetterImpl(terminators).setMaxGenerations(maxGenerations);
}
@Override
public ProcessRunner setMaxGenerationsWithoutImprovement(int maxGenerationsWithoutImprovement) {
return new MaxGenerationsWithoutImprovementTerminatorSetterImpl(terminators).setMaxGenerationsWithoutImprovement(maxGenerationsWithoutImprovement);
}
}
private final class SubsequentTerminatorSetter extends MaxGenerationsTerminatorSetterImpl implements TerminatorSetterOrProcessRunner {
private SubsequentTerminatorSetter(List<Predicate<RankedCandidates>> terminators) {
super(terminators);
}
@Override
public TerminatorSetterOrProcessRunner setTerminator(final Predicate<RankedCandidates> terminator) {
terminators.add(terminator);
return this;
}
@Override
public MaxGenerationsTerminatorSetterOrProcessRunner setTargetFitness(double targetFitness) {
terminators.add(new TargetFitnessTerminator(c -> Math.abs(c.getFitness() - targetFitness) < .0000001));
return new MaxGenerationsTerminatorSetterImpl(terminators);
}
}
private class MaxGenerationsTerminatorSetterImpl extends MaxGenerationsWithoutImprovementTerminatorSetterImpl implements
MaxGenerationsTerminatorSetterOrProcessRunner {
private MaxGenerationsTerminatorSetterImpl(List<Predicate<RankedCandidates>> terminators) {
super(terminators);
}
@Override
public final MaxGenerationsWithoutImprovementTerminatorSetterOrProcessRunner setMaxGenerations(int maxGenerations) {
terminators.add(new MaxGenerationsTerminator(maxGenerations));
return new MaxGenerationsWithoutImprovementTerminatorSetterImpl(terminators);
}
}
private class MaxGenerationsWithoutImprovementTerminatorSetterImpl implements MaxGenerationsWithoutImprovementTerminatorSetterOrProcessRunner {
protected final List<Predicate<RankedCandidates>> terminators;
private MaxGenerationsWithoutImprovementTerminatorSetterImpl(List<Predicate<RankedCandidates>> terminators) {
this.terminators = terminators;
}
@Override
public final ProcessRunner setMaxGenerationsWithoutImprovement(int maxGenerationsWithoutImprovement) {
terminators.add(new MaxGenerationsWithoutImprovementTerminator(maxGenerationsWithoutImprovement));
return new ProcessRunnerImpl(terminators);
}
@Override
public final RankedCandidates process() {
return new ProcessRunnerImpl(terminators).process();
}
}
private final class ProcessRunnerImpl implements ProcessRunner {
private Predicate<RankedCandidates> terminator;
@SuppressWarnings("unchecked")
private ProcessRunnerImpl(List<Predicate<RankedCandidates>> terminators) {
if (terminators.isEmpty()) {
throw new IllegalStateException("No termination criteria set");
} else if (terminators.size() == 1) {
terminator = terminators.get(0);
} else {
terminator = new CompositeTerminator(terminators.toArray(new Predicate[terminators.size()]));
}
}
@Override
public RankedCandidates process() {
if (_generationEvolver == null) {
_generationEvolver = createDefaultGenerationEvolver();
}
RankedCandidates rankedCandidates = Runner.process(_generationRanker, _generationEvolver, terminator, _initialPopulation);
RankedCandidate best = rankedCandidates.best();
Node simplifiedBestNode = simplify(best.getNode());
Logger.getGlobal().info("Best candidate: Fitness: " + best.getFitness() + " Structure: " + simplifiedBestNode);
return rankedCandidates;
}
private GenerationEvolver createDefaultGenerationEvolver() {
int populationSize = _initialPopulation.size();
NodeSelectorFactory nodeSelectorFactory = new RankSelectionFactory(_random);
Map<GeneticOperator, Integer> operators = createDefaultGeneticOperators(populationSize);
int operatorsSize = operators.values().stream().mapToInt(l -> l).sum();
int elitismSize = populationSize - operatorsSize;
Logger.getGlobal().info("total: " + populationSize + " elitism: " + elitismSize + " " + operators);
return new GenerationEvolverImpl(elitismSize, nodeSelectorFactory, operators);
}
private Map<GeneticOperator, Integer> createDefaultGeneticOperators(int populationSize) {
Map<GeneticOperator, Integer> operators = new HashMap<>();
TreeGenerator treeGenerator = TreeGeneratorImpl.grow(_primitiveSet, _random);
operators.put(t -> treeGenerator.generate(_returnType, 4), ratio(populationSize, .08));
operators.put(new SubtreeCrossover(_random, 5), ratio(populationSize, .4));
operators.put(new PointMutation(_random, _primitiveSet), ratio(populationSize, .4));
operators.put(new SubTreeMutation(_random, treeGenerator), ratio(populationSize, .04));
operators.put(new ConstantToFunctionMutation(_random, TreeGeneratorImpl.full(_primitiveSet)), ratio(populationSize, .04));
return operators;
}
private int ratio(int whole, double ratio) {
return (int) (whole * ratio);
}
}
/** Allows the function set to be configured. */
public interface FunctionSetSetter {
/** Sets the functions that are available for use in the construction of programs generated by the run. */
GenerationRankerSetter setFunctions(Function... functions);
/** Sets the functions that are available for use in the construction of programs generated by the run. */
GenerationRankerSetter setFunctions(List<Function> functions);
}
/** Provides a method for starting the genetic programming run or setting more termination criteria. */
public interface TerminatorSetterOrProcessRunner extends TerminatorSetter, ProcessRunner {
}
/** Allows termination criteria to be configured. */
public interface TerminatorSetter extends MaxGenerationsTerminatorSetter {
/** Sets the criteria used by this run to determine when it should stop. */
TerminatorSetterOrProcessRunner setTerminator(Predicate<RankedCandidates> terminator);
/** Set the target fitness that when found should cause the run to stop. */
MaxGenerationsTerminatorSetterOrProcessRunner setTargetFitness(double targetFitness);
}
/** Provides a method for starting the genetic programming run or setting more termination criteria. */
public interface MaxGenerationsTerminatorSetterOrProcessRunner extends MaxGenerationsTerminatorSetter, ProcessRunner {
}
/** Allows termination criteria to be configured. */
public interface MaxGenerationsTerminatorSetter extends MaxGenerationsWithoutImprovementTerminatorSetter {
/** Sets the maximum number of generations the run should process before stopping. */
MaxGenerationsWithoutImprovementTerminatorSetterOrProcessRunner setMaxGenerations(int maxGenerations);
}
/** Provides a method for starting the genetic programming run or setting more termination criteria. */
public interface MaxGenerationsWithoutImprovementTerminatorSetterOrProcessRunner extends MaxGenerationsWithoutImprovementTerminatorSetter, ProcessRunner {
}
/** Allows termination criteria to be configured. */
public interface MaxGenerationsWithoutImprovementTerminatorSetter {
/** Sets the number of consecutive generations without improvement the run should process before stopping. */
ProcessRunner setMaxGenerationsWithoutImprovement(int maxGenerationsWithoutImprovement);
}
/** Provides a method for starting the genetic programming run. */
public interface ProcessRunner {
/**
* Processes a genetic programming run using the values configured earlier for this {@code RunBuilder}.
*
* @return the final generation produced as part of this run - the best candidate of this generation can be retrieved using
* {@link RankedCandidates#best()}
*/
RankedCandidates process();
}
/** Provides access to configuration values that have already been set on a {@code RunBuilder}. */
public final class Config {
public PrimitiveSet getPrimitiveSet() {
return _primitiveSet;
}
public Random getRandom() {
return _random;
}
public Type getReturnType() {
return _returnType;
}
}
}