diff options
Diffstat (limited to 'src/jcgp/JCGP.java')
-rw-r--r-- | src/jcgp/JCGP.java | 421 |
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); } |