aboutsummaryrefslogtreecommitdiffstats
path: root/src/jcgp/JCGP.java
blob: 99ed55682dd4be0effc2eb499cf1aab8a8f1072b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
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. 
 * <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.
 * <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
 */
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 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;
	
	// 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. 
	 * <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 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. <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 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.
	 * <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);

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