aboutsummaryrefslogtreecommitdiffstats
path: root/src/jcgp/JCGP.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/jcgp/JCGP.java')
-rw-r--r--src/jcgp/JCGP.java421
1 files changed, 354 insertions, 67 deletions
diff --git a/src/jcgp/JCGP.java b/src/jcgp/JCGP.java
index f36ac7c..be09b4a 100644
--- a/src/jcgp/JCGP.java
+++ b/src/jcgp/JCGP.java
@@ -1,19 +1,14 @@
package jcgp;
import java.io.File;
+import java.lang.reflect.Constructor;
+import java.util.ArrayList;
+import java.util.Set;
import jcgp.backend.modules.es.EvolutionaryStrategy;
-import jcgp.backend.modules.es.MuPlusLambda;
-import jcgp.backend.modules.es.TournamentSelection;
-import jcgp.backend.modules.mutator.FixedPointMutator;
import jcgp.backend.modules.mutator.Mutator;
-import jcgp.backend.modules.mutator.PercentPointMutator;
-import jcgp.backend.modules.mutator.ProbabilisticMutator;
-import jcgp.backend.modules.problem.DigitalCircuitProblem;
import jcgp.backend.modules.problem.Problem;
-import jcgp.backend.modules.problem.SymbolicRegressionProblem;
import jcgp.backend.modules.problem.TestCaseProblem;
-import jcgp.backend.modules.problem.TravellingSalesmanProblem;
import jcgp.backend.parsers.ChromosomeParser;
import jcgp.backend.parsers.FunctionParser;
import jcgp.backend.parsers.ParameterParser;
@@ -22,103 +17,253 @@ import jcgp.backend.population.Population;
import jcgp.backend.resources.Console;
import jcgp.backend.resources.ModifiableResources;
import jcgp.backend.resources.Resources;
+import jcgp.backend.statistics.StatisticsLogger;
+
+import org.reflections.Reflections;
/**
*
- * Top-level CGP class. This class is the entry point for a CGP experiment.
- * <br>
- * An instance of JCGP encapsulates the entire experiment. It contains a Resources
+ * Top-level JCGP class. This class is the entry point for a CGP experiment.
+ * <br><br>
+ * An instance of JCGP encapsulates the entire experiment. It contains a {@code Resources}
* object which can be retrieved via a getter. Modules can be selected using their
- * respective setters and function sets can be selected through the resources.
- *
- * The flow of the experiment is controlled using start() and nextGeneration(). The
- * experiment can be reset with reset(), TODO comment
- *
+ * respective setters.
+ * <br><br>
+ * The flow of the experiment is controlled using {@code start()}, {@code nextGeneration()}
+ * and {@code reset()}. Files can be loaded with their respective load methods and
+ * chromosome configurations can be saved with {@code saveChromosome()}.
+ * <br><br>
+ * JCGP supports an extra console in addition to {@code System.console()}, so that messages
+ * can also be printed to a GUI, for example. This extra console can be set with {@code setConsole()},
+ * and must implement jcgp.resources.Console.
*
* @author Eduardo Pedroni
- * @see Resources, Module, FunctionSet
+ * @see Resources, Module
*/
public class JCGP {
- // make resources
private final ModifiableResources resources = new ModifiableResources();
/*
* The following arrays contain all available modules. These collections are read by the GUI
- * when generating menus, so modules not added here will *NOT* be selectable in the GUI.
+ * when generating menus and are populated automatically using reflection.
*
* Each array is accompanied by a field which contains a reference to the currently selected
* module, 0 by default.
*/
// mutators
- private Mutator[] mutators = new Mutator[] {
- new PercentPointMutator(resources),
- new FixedPointMutator(resources),
- new ProbabilisticMutator(resources)};
- private Mutator mutator = mutators[0];
+ private ArrayList<Mutator> mutators;
+ private Mutator mutator;
// evolutionary algorithms
- private EvolutionaryStrategy[] evolutionaryStrategies = new EvolutionaryStrategy[] {
- new MuPlusLambda(resources),
- new TournamentSelection(resources)};
- private EvolutionaryStrategy evolutionaryStrategy = evolutionaryStrategies[0];
+ private ArrayList<EvolutionaryStrategy> evolutionaryStrategies;
+ private EvolutionaryStrategy evolutionaryStrategy;
// problem types
- private Problem[] problems = new Problem[] {
- new SymbolicRegressionProblem(resources),
- new DigitalCircuitProblem(resources),
- new TravellingSalesmanProblem(resources)};
- private Problem problem = problems[0];
+ private ArrayList<Problem> problems;
+ private Problem problem;
- /*
- * the population of chromosomes
- */
private Population population;
+
+ private StatisticsLogger statistics = new StatisticsLogger();
+ private Reflections reflections;
+
+ // these record the best results found in the run, in case the runs ends before a perfect solution is found
+ private int lastImprovementGeneration = 0, activeNodes = 0;
+ private double bestFitnessFound = 0;
+
private boolean finished = false;
/**
- * TODO comment this!
+ * JCGP main method, this is used to execute JCGP from the command line.
+ * <br><br>
+ * In this case the program works in the same way as the classic CGP implementation,
+ * requiring a .par file and an optional problem data file. As in the traditional CGP
+ * implementation, the program must be compiled with the right problem type selected.
*
- * @param args
+ * @param args one or more files needed to perform the experiment.
*/
public static void main(String... args) {
+ // check that files have been provided
if (args.length < 1) {
System.err.println("JCGP requires at least a .par file.");
System.exit(1);
}
+ // prepare experiment
JCGP jcgp = new JCGP();
jcgp.loadParameters(new File(args[0]));
if (jcgp.getProblem() instanceof TestCaseProblem) {
TestCaseParser.parse(new File(args[2]), (TestCaseProblem<?>) jcgp.getProblem(), jcgp.getResources());
}
-
+ // kick it off
jcgp.start();
}
+
+ /**
+ * Creates a new instance of JCGP.
+ */
public JCGP() {
- resources.setFunctionSet(problem.getFunctionSet());
+ // prepare reflections instance
+ reflections = new Reflections("jcgp");
+
+ // generate module lists from all encountered valid modules
+ createEvolutionaryStrategyList();
+ createMutatorList();
+ createProblemList();
+
+ // create a new population
population = new Population(resources);
}
+ /**
+ * Iterates through all classes in the classpath and builds a list
+ * of instances of all valid evolutionary strategies found. In order
+ * to be included in this list, a class must implement {@code EvolutionaryStrategy}
+ * and must contain a constructor with a Resources object as its only argument.
+ */
+ private void createEvolutionaryStrategyList() {
+ Set<Class<? extends EvolutionaryStrategy>> esList = reflections.getSubTypesOf(EvolutionaryStrategy.class);
+ evolutionaryStrategies = new ArrayList<EvolutionaryStrategy>();
+
+ // go through all subclasses of EvolutionaryStrategy
+ for (Class<? extends EvolutionaryStrategy> esType : esList) {
+ Constructor<? extends EvolutionaryStrategy> constructor;
+ try {
+ constructor = esType.getConstructor(Resources.class);
+ } catch (NoSuchMethodException | SecurityException e) {
+ // constructor most likely doesnt exist, try next class
+ System.out.println("Warning: could not find valid constructor for " + esType.getName() + ", skipping...");
+ continue;
+ }
+
+ if (constructor != null) {
+ EvolutionaryStrategy es;
+ try {
+ es = (EvolutionaryStrategy) constructor.newInstance(resources);
+ } catch (Exception e) {
+ // could not instantiate for some reason, keep going
+ System.out.println("Warning: could not instantiate " + esType.getName() + ", skipping...");
+ continue;
+ }
+ // add it to the list if it was instantiated properly
+ evolutionaryStrategies.add(es);
+ }
+ }
+ // choose the initial module
+ evolutionaryStrategy = evolutionaryStrategies.get(0);
+ }
+
+ /**
+ * Iterates through all classes in the classpath and builds a list
+ * of instances of all valid mutators found. In order to be included
+ * in this list, a class must implement {@code Mutator}
+ * and must contain a constructor with a Resources object as its only argument.
+ */
+ private void createMutatorList() {
+ Set<Class<? extends Mutator>> mutatorList = reflections.getSubTypesOf(Mutator.class);
+ mutators = new ArrayList<Mutator>();
+
+ // go through all subclasses of Mutator
+ for (Class<? extends Mutator> mutatorType : mutatorList) {
+ Constructor<? extends Mutator> constructor;
+ try {
+ constructor = mutatorType.getConstructor(Resources.class);
+ } catch (NoSuchMethodException | SecurityException e) {
+ // constructor most likely doesnt exist, try next class
+ System.out.println("Warning: could not find valid constructor for "
+ + mutatorType.getName() + ", skipping...");
+ continue;
+ }
+
+ if (constructor != null) {
+ Mutator mutator;
+ try {
+ mutator = (Mutator) constructor.newInstance(resources);
+ } catch (Exception e) {
+ // could not instantiate for some reason, keep going
+ System.out.println("Warning: could not instantiate " + mutatorType.getName() + ", skipping...");
+ continue;
+ }
+ // add it to the list if it was instantiated properly
+ mutators.add(mutator);
+ }
+ }
+ // choose the initial module
+ mutator = mutators.get(0);
+ }
+
+ /**
+ * Iterates through all classes in the classpath and builds a list
+ * of instances of all valid problem types found. In order to be included
+ * in this list, a class must implement {@code Problem}
+ * and must contain a constructor with a Resources object as its only argument.
+ */
+ private void createProblemList() {
+ Set<Class<? extends Problem>> problemTypes = reflections.getSubTypesOf(Problem.class);
+ problems = new ArrayList<Problem>();
+
+ // go through all subclasses of Problem
+ for (Class<? extends Problem> problemType : problemTypes) {
+
+ Constructor<? extends Problem> constructor;
+ try {
+ constructor = problemType.getConstructor(Resources.class);
+ } catch (NoSuchMethodException | SecurityException e) {
+ // constructor most likely doesnt exist, try next class
+ System.out.println("Warning: could not find valid constructor for "
+ + problemType.getName() + ", skipping...");
+ continue;
+ }
+
+ if (constructor != null) {
+ Problem problem;
+ try {
+ problem = (Problem) constructor.newInstance(resources);
+ } catch (Exception e) {
+ // could not instantiate for some reason, keep going
+ System.out.println("Warning: could not instantiate " + problemType.getName() + ", skipping...");
+ continue;
+ }
+ // add it to the list if it was instantiated properly
+ problems.add(problem);
+ }
+ }
+ // choose the initial module
+ problem = problems.get(0);
+ resources.setFunctionSet(problem.getFunctionSet());
+ }
+
+ /**
+ * Returns a reference to the ModifiableResources used by the
+ * experiment. <br>
+ * Use this with care, since changing experiment parameters may
+ * have unintended effects if not done properly.
+ *
+ * @return a reference to the experiment's resources.
+ */
public ModifiableResources getResources() {
return resources;
}
+ /**
+ * @return a reference to the experiment's population.
+ */
public Population getPopulation() {
return population;
}
/**
- * @return the mutators
+ * @return a complete list of the experiment's mutators.
*/
- public Mutator[] getMutators() {
+ public ArrayList<Mutator> getMutators() {
return mutators;
}
/**
- * @return the mutator
+ * @return the currently selected mutator.
*/
public Mutator getMutator() {
return mutator;
@@ -126,15 +271,15 @@ public class JCGP {
/**
- * @return the evolutionaryAlgorithms
+ * @return a complete list of the experiment's evolutionary strategies.
*/
- public EvolutionaryStrategy[] getEvolutionaryStrategies() {
+ public ArrayList<EvolutionaryStrategy> getEvolutionaryStrategies() {
return evolutionaryStrategies;
}
/**
- * @return the evolutionaryAlgorithm
+ * @return the currently selected evolutionary strategy.
*/
public EvolutionaryStrategy getEvolutionaryStrategy() {
return evolutionaryStrategy;
@@ -142,15 +287,15 @@ public class JCGP {
/**
- * @return the fitnessFunctions
+ * @return a complete list of the experiment's problem types.
*/
- public Problem[] getProblems() {
+ public ArrayList<Problem> getProblems() {
return problems;
}
/**
- * @return the fitnessFunction
+ * @return the currently selected problem type.
*/
public Problem getProblem() {
return problem;
@@ -158,83 +303,163 @@ public class JCGP {
/**
- * @param mutator the mutator to set
+ * @param mutator the index of the desired mutator.
*/
public void setMutator(int index) {
- this.mutator = mutators[index];
+ this.mutator = mutators.get(index);
resources.println("[CGP] Mutator selected: " + mutator.toString());
}
/**
- * @param evolutionaryStrategy the evolutionaryAlgorithm to set
+ * @param evolutionaryStrategy the index of the desired evolutionary strategy.
*/
public void setEvolutionaryStrategy(int index) {
- this.evolutionaryStrategy = evolutionaryStrategies[index];
+ this.evolutionaryStrategy = evolutionaryStrategies.get(index);
resources.println("[CGP] Evolutionary strategy selected: " + evolutionaryStrategy.toString());
}
/**
- * @param problem the fitnessFunction to set
+ * @param problem the index of the desired problem type.
*/
public void setProblem(int index) {
- this.problem = problems[index];
+ this.problem = problems.get(index);
resources.setFunctionSet(problem.getFunctionSet());
}
+ /**
+ * Performs one full generational cycle. More specifically,
+ * this method evaluates the current population using the
+ * selected problem, and checks whether a solution has been found.
+ * <br>
+ * If the experiment is to continue, a new generation is created
+ * using the selected evolutionary strategy and mutator.
+ * <br><br>
+ * This method also deals with ending runs, in other words,
+ * a new population is created at the end of each run automatically.
+ * When all runs have been performed, this method sets the experiment
+ * finished flag and does nothing until {@code reset()} is called.
+ */
public void nextGeneration() {
if (!finished) {
problem.evaluate(population, (Resources) resources);
- reportGeneration();
if (resources.currentGeneration() < resources.generations()) {
+
// we still have generations left to go
if (problem.isPerfectSolution(population.getFittest())) {
+ // log results
+ statistics.logRun(resources.currentGeneration(), population.getFittest().getFitness(), population.getFittest().getActiveNodes().size(), true);
+ resetStatisticsValues();
+
// solution has been found, start next run
- resources.println("[CGP] Solution found, generation " + resources.currentGeneration() + ", chromosome " + population.getFittestIndex());
+ resources.println("[CGP] Solution found, generation " + resources.currentGeneration() + ", chromosome " + population.getFittestIndex() + "\n");
if (resources.currentRun() < resources.runs()) {
+
// there are still runs left
- resources.setCurrentRun(resources.currentRun() + 1);
- resources.setCurrentGeneration(0);
+ resources.incrementRun();
+ resources.setCurrentGeneration(1);
// start a new population
population.reinitialise();
} else {
// no more generations and no more runs, we're done
+ printStatistics();
finished = true;
}
} else {
- resources.setCurrentGeneration(resources.currentGeneration() + 1);
+ if (problem.isImprovement(population.getFittest())) {
+ printImprovement();
+ lastImprovementGeneration = resources.currentGeneration();
+ bestFitnessFound = population.getFittest().getFitness();
+ activeNodes = population.getFittest().getActiveNodes().size();
+ } else {
+ reportGeneration();
+ }
+ resources.incrementGeneration();
// solution isn't perfect and we still have generations left, evolve more!
evolutionaryStrategy.evolve(population, mutator, (Resources) resources);
}
} else {
- // the run has ended, check if any more runs must be done
- resources.println("[CGP] Solution not found, highest fitness achieved was "
- + population.getFittest().getFitness()
- + " by chromosome " + population.getFittestIndex());
-
+ // the run has ended, tell the user and log it
+ resources.println("[CGP] Solution not found, best fitness achieved was "
+ + bestFitnessFound + "\n");
+
+ statistics.logRun(lastImprovementGeneration, bestFitnessFound, activeNodes, false);
+ resetStatisticsValues();
+
+ // check if any more runs must be done
if (resources.currentRun() < resources.runs()) {
// the run has ended but there are still runs left
- resources.setCurrentRun(resources.currentRun() + 1);
- resources.setCurrentGeneration(0);
+ resources.incrementRun();
+ resources.setCurrentGeneration(1);
// start a new population
population.reinitialise();
} else {
// no more generations and no more runs, we're done
+ printStatistics();
finished = true;
}
}
}
}
+ /**
+ * Used internally for printing statistics at the end of the experiment.
+ * This method currently prints the exact same statistics as the ones
+ * provided by the classic CGP implementation.
+ */
+ private void printStatistics() {
+ resources.println("[CGP] Experiment finished");
+ resources.println("[CGP] Average fitness: " + statistics.getAverageFitness());
+ resources.println("[CGP] Std dev fitness: " + statistics.getAverageFitnessStdDev());
+
+ resources.println("[CGP] Average number of active nodes: " + statistics.getAverageActiveNodes());
+ resources.println("[CGP] Std dev number of active nodes: " + statistics.getAverageActiveNodesStdDev());
+
+ resources.println("[CGP] Average best generation: " + statistics.getAverageGenerations());
+ resources.println("[CGP] Std dev best generation: " + statistics.getAverageGenerationsStdDev());
+
+ resources.println("[CGP] Highest fitness of all runs: " + statistics.getHighestFitness());
+ resources.println("[CGP] Lowest fitness of all runs: " + statistics.getLowestFitness());
+
+ resources.println("[CGP] Perfect solutions: " + statistics.getSuccessfulRuns());
+ resources.println("[CGP] Success rate: " + (statistics.getSuccessRate() * 100) + "%");
+
+ resources.println("[CGP] Average generations for perfect solutions only: " + statistics.getAverageSuccessfulGenerations());
+ resources.println("[CGP] Std dev generations for perfect solutions only: " + statistics.getAverageSuccessfulGenerationsStdDev());
+ }
+
+ /**
+ * Used internally for reporting improvement, which happens independently of
+ * the report interval parameter.
+ */
+ private void printImprovement() {
+ resources.println("[CGP] Generation: " + resources.currentGeneration() + ", fittest chromosome ("
+ + population.getFittestIndex() + ") has fitness: " + population.getFittest().getFitness());
+ }
+
+ /**
+ * Used internally for reporting generation information, which is affected
+ * by the report interval parameter.
+ */
private void reportGeneration() {
resources.reportln("[CGP] Generation: " + resources.currentGeneration() + ", fittest chromosome ("
+ population.getFittestIndex() + ") has fitness: " + population.getFittest().getFitness());
}
+ /**
+ * This method calls {@code nextGeneration()} in a loop
+ * until the experiment is flagged as finished. This is
+ * performed on the same thread of execution, so this
+ * method will most likely block for a significant amount
+ * of time (problem-dependent, but anywhere from seconds to days).
+ * <br>
+ * Once the experiment is finished, calling this method does
+ * nothing until {@code reset()} is called.
+ */
public void start() {
if (!finished) {
while (!finished) {
@@ -243,9 +468,19 @@ public class JCGP {
}
}
+ /**
+ * Resets the experiment.
+ * <br>
+ * More specifically: this creates a new population, resets
+ * the current generation and run parameters to 1 and prints
+ * a complete list of the experiment's parameters.
+ *
+ */
public void reset() {
finished = false;
+ statistics = new StatisticsLogger();
population = new Population(resources);
+ resetStatisticsValues();
resources.setCurrentGeneration(1);
resources.setCurrentRun(1);
resources.println("*********************************************************");
@@ -262,30 +497,82 @@ public class JCGP {
resources.println("[CGP] Evolutionary strategy: " + evolutionaryStrategy.toString());
resources.println("[CGP] Mutator: " + mutator.toString());
}
+
+ /**
+ * Internally used to reset the fields used
+ * for logging results statistics.
+ */
+ private void resetStatisticsValues() {
+ problem.reset();
+ lastImprovementGeneration = 0;
+ bestFitnessFound = 0;
+ activeNodes = 0;
+ }
+ /**
+ * When given a .par file, this method loads the parameters into the
+ * experiment's resources. This causes an experiment-wide reset.
+ *
+ * @param file the file to parse.
+ */
public void loadParameters(File file) {
ParameterParser.parse(file, resources);
FunctionParser.parse(file, problem.getFunctionSet(), resources);
reset();
}
+ /**
+ * Parses a problem data file. This is problem-dependent, not
+ * all problems require a data file.
+ *
+ * @param file the file to parse.
+ */
public void loadProblemData(File file) {
problem.parseProblemData(file, resources);
reset();
}
+ /**
+ * Loads a chromosome from the given file into
+ * the specified population index.
+ *
+ * @param file the chromosome to parse.
+ * @param chromosomeIndex the population index into which to parse.
+ */
public void loadChromosome(File file, int chromosomeIndex) {
ChromosomeParser.parse(file, population.getChromosome(chromosomeIndex), resources);
}
+ /**
+ * Saves a copy of the specified chromosome
+ * into the given file.
+ *
+ * @param file the target file.
+ * @param chromosomeIndex the index of the chromosome to save.
+ */
public void saveChromosome(File file, int chromosomeIndex) {
ChromosomeParser.save(file, population.getChromosome(chromosomeIndex), resources);
}
+ /**
+ * Returns the experiment's status. When finished, the only
+ * way to continue is by calling {@code reset()}.
+ *
+ * @return true if the experiment is finished.
+ */
public boolean isFinished() {
return finished;
}
+ /**
+ * Sets an extra console. The entire JCGP library prints
+ * messages to {@code System.console()} but also to an
+ * additional console, if one is defined. This is used so
+ * that messages are printed on a user interface as well,
+ * or written directly to a file, for example.
+ *
+ * @param console the extra console to be used.
+ */
public void setConsole(Console console) {
resources.setConsole(console);
}