package jcgp; import java.io.File; 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.parsers.ChromosomeParser; import jcgp.backend.parsers.FunctionParser; import jcgp.backend.parsers.ParameterParser; import jcgp.backend.parsers.TestCaseParser; import jcgp.backend.population.Population; import jcgp.backend.resources.Console; import jcgp.backend.resources.ModifiableResources; import jcgp.backend.statistics.StatisticsLogger; /** * * Top-level JCGP class. This class is the entry point for a CGP experiment. *

* 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. *

* 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()}. *

* 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 */ public class JCGP { private final ModifiableResources resources = new ModifiableResources(); /* * The following arrays contain all available modules. These collections are read by the GUI * when generating menus. * * Each array is accompanied by a field which contains a reference to the currently selected * module, index 0 by default. */ // mutators private Mutator[] mutators = new Mutator[] { new PercentPointMutator(resources), new FixedPointMutator(resources), new ProbabilisticMutator(resources) }; private Mutator mutator; // evolutionary algorithms private EvolutionaryStrategy[] evolutionaryStrategies = new EvolutionaryStrategy[] { new MuPlusLambda(resources), new TournamentSelection(resources) }; private EvolutionaryStrategy evolutionaryStrategy; // problem types private Problem[] problems = new Problem[] { new DigitalCircuitProblem(resources), new SymbolicRegressionProblem(resources) }; private Problem problem; private Population population; private StatisticsLogger statistics = new StatisticsLogger(); // 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; /** * JCGP main method, this is used to execute JCGP from the command line. *

* 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 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() { // initialise modules setEvolutionaryStrategy(0); setMutator(0); setProblem(0); // create a new population population = new Population(resources); } /** * Returns a reference to the {@code ModifiableResources} used by the * experiment.
* 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 a complete list of the experiment's mutators. */ public Mutator[] getMutators() { return mutators; } /** * @return the currently selected mutator. */ public Mutator getMutator() { return mutator; } /** * @return a complete list of the experiment's evolutionary strategies. */ public EvolutionaryStrategy[] getEvolutionaryStrategies() { return evolutionaryStrategies; } /** * @return the currently selected evolutionary strategy. */ public EvolutionaryStrategy getEvolutionaryStrategy() { return evolutionaryStrategy; } /** * @return a complete list of the experiment's problem types. */ public Problem[] getProblems() { return problems; } /** * @return the currently selected problem type. */ public Problem getProblem() { return problem; } /** * @param index the index of the desired mutator. */ public void setMutator(int index) { this.mutator = mutators[index]; resources.println("[CGP] Mutator selected: " + mutator.toString()); } /** * @param index the index of the desired evolutionary strategy. */ public void setEvolutionaryStrategy(int index) { this.evolutionaryStrategy = evolutionaryStrategies[index]; resources.println("[CGP] Evolutionary strategy selected: " + evolutionaryStrategy.toString()); } /** * @param index the index of the desired problem type. */ public void setProblem(int index) { this.problem = problems[index]; resources.setFunctionSet(problem.getFunctionSet()); resources.setFitnessOrientation(problem.getFitnessOrientation()); } /** * 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. *
* If the experiment is to continue, a new generation is created * using the selected evolutionary strategy and mutator. *

* 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); if (resources.currentGeneration() < resources.generations()) { // we still have generations left to go int perfect = problem.hasPerfectSolution(population); if (perfect >= 0) { // log results statistics.logRun(resources.currentGeneration(), population.get(perfect).getFitness(), population.get(perfect).getActiveNodes().size(), true); resetStatisticsValues(); // solution has been found, start next run resources.println("[CGP] Solution found: generation " + resources.currentGeneration() + ", chromosome " + perfect + "\n"); resources.println("[CGP] Printing chromosome..."); ChromosomeParser.print(population.get(perfect), resources); resources.println("[CGP] Printing done. "); if (resources.currentRun() < resources.runs()) { // there are still runs left 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 { // solution not found, look for improvement int improvement = problem.hasImprovement(population); if (improvement >= 0) { // there has been improvement, print it printImprovement(improvement); lastImprovementGeneration = resources.currentGeneration(); bestFitnessFound = population.get(improvement).getFitness(); activeNodes = population.get(improvement).getActiveNodes().size(); } else { // there has been no improvement, report generation reportGeneration(); } resources.incrementGeneration(); // we still have generations left, evolve more! evolutionaryStrategy.evolve(population, mutator); } } else { // 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.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(int chromosome) { resources.println("[CGP] Generation: " + resources.currentGeneration() + ", fittest chromosome (" + chromosome + ") has fitness: " + population.get(chromosome).getFitness()); } /** * Used internally for reporting generation information, which is affected * by the report interval parameter. */ private void reportGeneration() { resources.reportln("[CGP] Generation: " + resources.currentGeneration() + ", best fitness: " + problem.getBestFitness()); } /** * 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). *
* Once the experiment is finished, calling this method does * nothing until {@code reset()} is called. */ public void start() { if (!finished) { while (!finished) { nextGeneration(); } } } /** * Resets the experiment. *
* 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() { statistics = new StatisticsLogger(); resources.setArity(problem.getFunctionSet().getMaxArity()); if (resources.arity() < 1) { resources.println("[CGP] Error: arity is smaller than 1. Check that at least one function is enabled"); return; } finished = false; population = new Population(resources); resetStatisticsValues(); resources.setCurrentGeneration(1); resources.setCurrentRun(1); resources.println("*********************************************************"); resources.println("[CGP] New experiment: " + problem.toString()); resources.println("[CGP] Rows: " + resources.rows()); resources.println("[CGP] Columns: " + resources.columns()); resources.println("[CGP] Levels back: " + resources.levelsBack()); resources.println("[CGP] Population size: " + resources.populationSize()); resources.println("[CGP] Total generations: " + resources.generations()); resources.println("[CGP] Total runs: " + resources.runs()); resources.println("[CGP] Report interval: " + resources.reportInterval()); resources.println("[CGP] Seed: " + resources.seed()); resources.println(""); 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.get(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.get(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); } }