Jenetics Manual 4.3.0
User Manual:
Open the PDF directly: View PDF .
Page Count: 140
Download | |
Open PDF In Browser | View PDF |
JENETICS LIBRARY USER’S MANUAL 4.3 Franz Wilhelmstötter Franz Wilhelmstötter franz.wilhelmstoetter@gmail.com http://jenetics.io 4.3.0—2018/10/28 This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 License. To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/3.0/ or send a letter to Creative Commons, 444 Castro Street, Suite 900, Mountain View, California, 94041, USA. Contents 1 Fundamentals 1.1 Introduction . . . . . . . . . . . . . . 1.2 Architecture . . . . . . . . . . . . . . 1.3 Base classes . . . . . . . . . . . . . . 1.3.1 Domain classes . . . . . . . . 1.3.1.1 Gene . . . . . . . . 1.3.1.2 Chromosome . . . . 1.3.1.3 Genotype . . . . . . 1.3.1.4 Phenotype . . . . . 1.3.1.5 Population . . . . . 1.3.2 Operation classes . . . . . . . 1.3.2.1 Selector . . . . . . . 1.3.2.2 Alterer . . . . . . . 1.3.3 Engine classes . . . . . . . . . 1.3.3.1 Fitness function . . 1.3.3.2 Engine . . . . . . . 1.3.3.3 EvolutionStream . . 1.3.3.4 EvolutionResult . . 1.3.3.5 EvolutionStatistics . 1.3.3.6 Engine.Evaluator . . 1.4 Nuts and bolts . . . . . . . . . . . . 1.4.1 Concurrency . . . . . . . . . 1.4.1.1 Basic configuration 1.4.1.2 Concurrency tweaks 1.4.2 Randomness . . . . . . . . . 1.4.3 Serialization . . . . . . . . . . 1.4.4 Utility classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 1 4 5 6 6 7 8 11 11 12 12 16 21 22 22 25 26 27 29 30 30 30 31 32 35 36 2 Advanced topics 2.1 Extending Jenetics 2.1.1 Genes . . . . 2.1.2 Chromosomes 2.1.3 Selectors . . . 2.1.4 Alterers . . . 2.1.5 Statistics . . 2.1.6 Engine . . . . 2.2 Encoding . . . . . . 2.2.1 Real function . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39 39 39 40 42 43 43 44 45 45 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ii . . . . . . . . . . . . . . . . . . . . . . . . . . . CONTENTS CONTENTS 2.2.2 Scalar function . . . . . . 2.2.3 Vector function . . . . . . 2.2.4 Affine transformation . . 2.2.5 Graph . . . . . . . . . . . 2.3 Codec . . . . . . . . . . . . . . . 2.3.1 Scalar codec . . . . . . . . 2.3.2 Vector codec . . . . . . . 2.3.3 Subset codec . . . . . . . 2.3.4 Permutation codec . . . . 2.3.5 Mapping codec . . . . . . 2.3.6 Composite codec . . . . . 2.4 Problem . . . . . . . . . . . . . . 2.5 Validation . . . . . . . . . . . . . 2.6 Termination . . . . . . . . . . . . 2.6.1 Fixed generation . . . . . 2.6.2 Steady fitness . . . . . . . 2.6.3 Evolution time . . . . . . 2.6.4 Fitness threshold . . . . . 2.6.5 Fitness convergence . . . 2.6.6 Population convergence . 2.6.7 Gene convergence . . . . . 2.7 Reproducibility . . . . . . . . . . 2.8 Evolution performance . . . . . . 2.9 Evolution strategies . . . . . . . 2.9.1 (µ, λ) evolution strategy . 2.9.2 (µ + λ) evolution strategy 2.10 Evolution interception . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46 47 47 49 51 52 53 53 55 56 56 58 59 60 61 61 63 64 65 66 67 68 69 70 70 71 71 3 Internals 73 3.1 PRNG testing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73 3.2 Random seeding . . . . . . . . . . . . . . . . . . . . . . . . . . . 74 4 Modules 4.1 io.jenetics.ext . . . . . . . . . . . 4.1.1 Data structures . . . . . . . . . 4.1.1.1 Tree . . . . . . . . . . 4.1.1.2 Parentheses tree . . . 4.1.1.3 Flat tree . . . . . . . 4.1.2 Genes . . . . . . . . . . . . . . 4.1.2.1 BigInteger gene . . . 4.1.2.2 Tree gene . . . . . . . 4.1.3 Operators . . . . . . . . . . . . 4.1.4 Weasel program . . . . . . . . 4.1.5 Modifying Engine . . . . . . . 4.1.5.1 ConcatEngine . . . . 4.1.5.2 CyclicEngine . . . . 4.1.5.3 AdaptiveEngine . . . 4.1.6 Multi-objective optimization . 4.1.6.1 Pareto efficiency . . . 4.1.6.2 Implementing classes iii . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77 78 78 78 79 80 81 81 81 81 82 84 85 86 86 88 88 89 CONTENTS 4.2 4.3 4.4 CONTENTS 4.1.6.3 Termination . . io.jenetics.prog . . . . . . . . 4.2.1 Operations . . . . . . . . 4.2.2 Program creation . . . . . 4.2.3 Program repair . . . . . . 4.2.4 Program pruning . . . . . io.jenetics.xml . . . . . . . . 4.3.1 XML writer . . . . . . . . 4.3.2 XML reader . . . . . . . . 4.3.3 Marshalling performance . io.jenetics.prngine . . . . . . 5 Examples 5.1 Ones counting . . . . 5.2 Real function . . . . 5.3 Rastrigin function . 5.4 0/1 Knapsack . . . . 5.5 Traveling salesman . 5.6 Evolving images . . 5.7 Symbolic regression . 5.8 DTLZ1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92 92 93 94 95 96 96 96 97 98 99 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104 104 106 108 110 112 115 118 120 6 Build 124 Bibliography 128 iv List of Figures 1.2.1 Evolution workflow . . . . . . . . 1.2.2 Evolution engine model . . . . . 1.2.3 Package structure . . . . . . . . . 1.3.1 Domain model . . . . . . . . . . 1.3.2 Chromosome structure . . . . . . 1.3.3 Genotype structure . . . . . . . . 1.3.4 Row-major Genotype vector . . 1.3.5 Column-major Genotype vector . 1.3.6 Genotype scalar . . . . . . . . . . 1.3.7 Fitness proportional selection . . 1.3.8 Stochastic-universal selection . . 1.3.9 Single-point crossover . . . . . . 1.3.102-point crossover . . . . . . . . . 1.3.113-point crossover . . . . . . . . . 1.3.12Partially-matched crossover . . . 1.3.13Uniform crossover . . . . . . . . 1.3.14Line crossover hypercube . . . . 1.4.1 Evaluation batch . . . . . . . . . 1.4.2 Block splitting . . . . . . . . . . 1.4.3 Leapfrogging . . . . . . . . . . . 1.4.4 Seq class diagram . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 4 5 6 8 8 9 10 11 13 15 19 19 20 20 20 21 30 34 35 37 2.2.1 Undirected graph and adjacency matrix . . . . . . . 2.2.2 Directed graph and adjacency matrix . . . . . . . . . 2.2.3 Weighted graph and adjacency matrix . . . . . . . . 2.3.1 Mapping codec types . . . . . . . . . . . . . . . . . . 2.6.1 Fixed generation termination . . . . . . . . . . . . . 2.6.2 Steady fitness termination . . . . . . . . . . . . . . . 2.6.3 Execution time termination . . . . . . . . . . . . . . 2.6.4 Fitness threshold termination . . . . . . . . . . . . . 2.6.5 Fitness convergence termination: NS = 10, NL = 30 2.6.6 Fitness convergence termination: NS = 50, NL = 150 2.8.1 Selector-performance (Knapsack) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49 50 51 56 62 63 64 65 67 68 69 4.0.1 Module graph . . . . . . . . . . . 4.1.1 Example tree . . . . . . . . . . . 4.1.2 Parentheses tree syntax diagram 4.1.3 Example FlatTree . . . . . . . . 4.1.4 Single-node crossover . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77 78 79 81 82 v . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . LIST OF FIGURES LIST OF FIGURES 4.1.5 Engine concatenation . . . . 4.1.6 Cyclic Engine . . . . . . . . . 4.1.7 Adaptive Engine . . . . . . . 4.1.8 Circle points . . . . . . . . . 4.1.9 Maximizing Pareto front . . . 4.1.10Minimizing Pareto front . . . 4.3.1 Genotype write performance . 4.3.2 Genotype read performance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85 . 86 . 87 . 89 . 90 . 91 . 99 . 100 5.2.1 Real function . . . . . . . . . . 5.3.1 Rastrigin function . . . . . . . 5.6.1 Evolving images UI . . . . . . . 5.6.2 Evolving Mona Lisa images . . 5.7.1 Symbolic regression polynomial 5.8.1 Pareto front DTLZ1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . vi 106 108 116 117 120 123 Chapter 1 Fundamentals Jenetics is an advanced Genetic Algorithm, Evolutionary Algorithm and Genetic Programming library, respectively, written in modern day Java. It is designed with a clear separation of the several algorithm concepts, e. g. Gene, Chromosome, Genotype, Phenotype, population and fitness Function. Jenetics allows you to minimize or maximize a given fitness function without tweaking it. In contrast to other GA implementations, the library uses the concept of an evolution stream (EvolutionStream) for executing the evolution steps. Since the EvolutionStream implements the Java Stream interface, it works smoothly with the rest of the Java Stream API. This chapter describes the design concepts and its implementation. It also gives some basic examples and best practice tips.1 1.1 Introduction Jenetics is a library, written in Java2 , which provides an genetic algorithm (GA) and genetic programming (GP) implementation. It has no runtime dependencies to other libraries, except the Java 8 runtime. Although the library is written in Java 8, it is compilable with Java 9, 10 and 11. Jenetics is available on Maven central repository3 and can be easily integrated into existing projects. The very clear structuring of the different parts of the GA allows an easy adaption for different problem domains. This manual is not an introduction or a tutorial for genetic and/or evolutionary algorithms in general. It is assumed that the reader has a knowledge about the structure and the functionality of genetic algorithms. Good introductions to GAs can be found in [29], [21], [28], [20], [22] or [33]. For genetic programming you can have a look at [18] or [19]. 1 The classes described in this chapter reside in the io.jenetics.base module or io:jenetics:jenetics:4.3.0 artifact, respectively. 2 The library is build with and depends on Java SE 8: http://www.oracle.com/technetwork/ java/javase/downloads/index.html 3 https://mvnrepository.com/artifact/io.jenetics/jenetics: If you are using Gradle, you can use the following dependency string: »io.jenetics:jenetics:4.3.0«. 1 1.1. INTRODUCTION CHAPTER 1. FUNDAMENTALS To give you a first impression on how to use Jenetics, lets start with a simple »Hello World« program. This first example implements the well known bit-counting problem. 1 2 3 4 5 6 import import import import import import io io io io io io . . . . . . jenetics jenetics jenetics jenetics jenetics jenetics . BitChromosome ; . BitGene ; . Genotype ; . e n g i n e . Engine ; . engine . EvolutionResult ; . u t i l . Factory ; 7 8 9 10 11 12 13 14 public f i n a l c l a s s HelloWorld { // 2 . ) D e f i n i t i o n o f t h e f i t n e s s f u n c t i o n . private s t a t i c i n t e v a l ( f i n a l Genotypeg t ) { return g t . getChromosome ( ) . a s ( BitChromosome . c l a s s ) . bitCount ( ) ; } 15 public s t a t i c void main ( f i n a l S t r i n g [ ] a r g s ) { // 1 . ) D e f i n e t h e g e n o t y p e ( f a c t o r y ) s u i t a b l e // f o r t h e problem . f i n a l Factory > g t f = Genotype . o f ( BitChromosome . o f ( 1 0 , 0 . 5 ) ) ; 16 17 18 19 20 21 // 3 . ) C r e a t e t h e e x e c u t i o n e n v i ro n m en t . f i n a l Engine e n g i n e = Engine . b u i l d e r ( HelloWorld : : e v a l , g t f ) . build () ; 22 23 24 25 26 // 4 . ) S t a r t t h e e x e c u t i o n ( e v o l u t i o n ) and // c o l l e c t the r e s u l t . f i n a l Genotype r e s u l t = e n g i n e . stream ( ) . l i m i t (100) . c o l l e c t ( E v o l u t i o n R e s u l t . toBestGenotype ( ) ) ; 27 28 29 30 31 32 33 34 35 } } System . out . p r i n t l n ( " H e l l o World : \ n\ t " + r e s u l t ) ; Listing 1.1: »Hello World« GA In contrast to other GA implementations, Jenetics uses the concept of an evolution stream (EvolutionStream) for executing the evolution steps. Since the EvolutionStream implements the Java Stream interface, it works smoothly with the rest of the Java Stream API. Now let’s have a closer look at listing 1.1 and discuss this simple program step by step: 1. The probably most challenging part, when setting up a new evolution Engine, is to transform the problem domain into an appropriate Genotype (factory) representation.4 In our example we want to count the number of ones of a BitChromosome. Since we are counting only the ones of one chromosome, we are adding only one BitChromosome to our Genotype. In general, the Genotype can be created with 1 to n chromosomes. For a detailed description of the genotype’s structure have a look at section 1.3.1.3 on page 8. 2. Once this is done, the fitness function, which should be maximized, can be defined. Utilizing language features introduced in Java 8, we simply 4 Section 2.2 on page 45 describes some common problem encodings. 2 1.1. INTRODUCTION CHAPTER 1. FUNDAMENTALS write a private static method, which takes the genotype we defined and calculate it’s fitness value. If we want to use the optimized bit-counting method, bitCount(), we have to cast the Chromosome class to the actual used BitChromosome class. Since we know for sure that we created the Genotype with a BitChromosome, this can be done safely. A reference to the eval method is then used as fitness function and passed to the Engine.build method. 3. In the third step we are creating the evolution Engine, which is responsible for changing, respectively evolving, a given population. The Engine is highly configurable and takes parameters for controlling the evolutionary and the computational environment. For changing the evolutionary behavior, you can set different alterers and selectors (see section 1.3.2 on page 12). By changing the used Executor service, you control the number of threads, the Engine is allowed to use. An new Engine instance can only be created via its builder, which is created by calling the Engine.builder method. 4. In the last step, we can create a new EvolutionStream from our Engine. The EvolutionStream is the model (or view) of the evolutionary process. It serves as a »process handle« and also allows you, among other things, to control the termination of the evolution. In our example, we simply truncate the stream after 100 generations. If you don’t limit the stream, the EvolutionStream will not terminate and run forever. The final result, the best Genotype in our example, is then collected with one of the predefined collectors of the EvolutionResult class. As the example shows, Jenetics makes heavy use of the Stream and Collector classes of Java 8. Also the newly introduced lambda expressions and the functional interfaces (SAM types) play an important roll in the library design. There are many other GA implementations out there and they may slightly differ in the order of the single execution steps. Jenetics uses an classical approach. Listing 1.2 shows the (imperative) pseudo-code of the Jenetics genetic algorithm steps. 1 2 3 4 5 6 7 8 9 P0 ← Pinitial F (P0 ) while ! f inished do g ←g+1 Sg ← selectS (Pg−1 ) Og ← selectO (Pg−1 ) Og ← alter(Og ) Pg ← f ilter[gi ≥ gmax ](Sg ) + f ilter[gi ≥ gmax ](Og ) F (Pg ) Listing 1.2: Genetic algorithm Line (1) creates the initial population and line (2) calculates the fitness value of the individuals. The initial population is created implicitly before the first evolution step is performed. Line (4) increases the generation number and line (5) and (6) selects the survivor and the offspring population. The offspring/survivor fraction is determined by the offspringFraction property of the Engine.Builder. The selected offspring are altered in line (7). The next line combines the survivor population and the altered offspring population—after removing the died individuals—to the new population. The steps from line (4) to (9) are repeated until a given termination criterion is fulfilled. 3 1.2. ARCHITECTURE 1.2 CHAPTER 1. FUNDAMENTALS Architecture The basic metaphor of the Jenetics library is the Evolution Stream, implemented via the Java 8 Stream API. Therefore it is no longer necessary (and advised) to perform the evolution steps in an imperative way. An evolution stream is powered by—and bound to—an Evolution Engine, which performs the needed evolution steps for each generation; the steps are described in the body of the while-loop of listing 1.2 on the previous page. Figure 1.2.1: Evolution workflow The described evolution workflow is also illustrated in figure 1.2.1, where ES(i) denotes the EvolutionStart object at generation i and ER(i) the EvolutionResult at the ith generation. Once the evolution Engine is created, it can be used by multiple EvolutionStreams, which can be safely used in different execution threads. This is possible, because the evolution Engine doesn’t have any mutable global state. It is practically a stateless function, fE : P → P, which maps a start population, P, to an evolved result population. The Engine function, fE , is, of course, non-deterministic. Calling it twice with the same start population will lead to different result populations. The evolution process terminates, if the EvolutionStream is truncated and the EvolutionStream truncation is controlled by the limit predicate. As long as the predicate returns true, the evolution is continued.5 At last, the EvolutionResult is collected from the EvolutionStream by one of the available EvolutionResult collectors. Figure 1.2.2: Evolution engine model Figure 1.2.2 shows the static view of the main evolution classes, together with its dependencies. Since the Engine class itself is immutable, and can’t be changed after creation, it is instantiated (configured) via a builder. The Engine can be used to create an arbitrary number of EvolutionStreams. The EvolutionStream is used to control the evolutionary process and collect the final 5 See section 2.6 on page 60 for a detailed description of the available termination strategies. 4 1.3. BASE CLASSES CHAPTER 1. FUNDAMENTALS result. This is done in the same way as for the normal java.util.stream.Stream classes. With the additional limit(Predicate) method, it is possible to truncate the EvolutionStream if some termination criteria is fulfilled. The separation of Engine and EvolutionStream is the separation of the evolution definition and evolution execution. Figure 1.2.3: Package structure In figure 1.2.3 the package structure of the library is shown and it consists of the following packages: io.jenetics This is the base package of the Jenetics library and contains all domain classes, like Gene, Chromosome or Genotype. Most of this types are immutable data classes and doesn’t implement any behavior. It also contains the Selector and Alterer interfaces and its implementations. The classes in this package are (almost) sufficient to implement an own GA. io.jenetics.engine This package contains the actual GA implementation classes, e. g. Engine, EvolutionStream or EvolutionResult. They mainly operate on the domain classes of the io.jenetics package. io.jenetics.stat This package contains additional statistics classes which are not available in the Java core library. Java only includes classes for calculating the sum and the average of a given numeric stream (e. g. DoubleSummaryStatistics). With the additions in this package it is also possible to calculate the variance, skewness and kurtosis—using the DoubleMomentStatistics class. The EvolutionStatistics object, which can be calculated for every generation, relies on the classes of this package. io.jenetics.util This package contains the collection classes (Seq, ISeq and MSeq) which are used in the public interfaces of the Chromosome and Genotype. It also contains the RandomRegistry class, which implements the global PRNG lookup, as well as helper IO classes for serializing Genotypes and whole populations. 1.3 Base classes This chapter describes the main classes which are needed to setup and run an genetic algorithm with the Jenetics6 library. They can roughly divided into three types: 6 The documentation of the whole API is part of the download package or can be viewed online: http://jenetics.io/javadoc/jenetics/4.3/index.html. 5 1.3. BASE CLASSES CHAPTER 1. FUNDAMENTALS Domain classes This classes form the domain model of the evolutionary algorithm and contain the structural classes like Gene and Chromosome. They are directly located in the io.jenetics package. Operation classes This classes operates on the domain classes and includes the Alterer and Selector classes. They are also located in the io.jenetics package. Engine classes This classes implements the actual evolutionary algorithm and reside solely in the io.jenetics.engine package. 1.3.1 Domain classes Most of the domain classes are pure data classes and can be treated as value objects7 . All Gene and Chromosome implementations are immutable as well as the Genotype and Phenotype class. Figure 1.3.1: Domain model Figure 1.3.1 shows the class diagram of the domain classes. All domain classes are located in the io.jenetics package. The Gene is the base of the class structure. Genes are aggregated in Chromosomes. One to n Chromosomes are aggregated in Genotypes. A Genotype and a fitness Function form the Phenotype, which are collected into a population Seq. 1.3.1.1 Gene Genes are the basic building blocks of the Jenetics library. They contain the actual information of the encoded solution, the allele. Some of the implementations also contains domain information of the wrapped allele. This is the case for all BoundedGene, which contain the allowed minimum and maximum values. All Gene implementations are final and immutable. In fact, they are all value-based classes and fulfill the properties which are described in the Java API documentation[24].8 Beside the container functionality for the allele, every Gene is its own factory and is able to create new, random instances of the same type and with the same constraints. The factory methods are used by the Alterers for creating new 7 https://en.wikipedia.org/wiki/Value_object 8 It is also worth reading the blog entry from Stephen Colebourne: http://blog.joda.org/ 2014/03/valjos-value-java-objects.html 6 1.3. BASE CLASSES CHAPTER 1. FUNDAMENTALS Genes from the existing one and play a crucial role by the exploration of the problem space. 1 2 3 4 5 6 7 8 public i n t e r f a c e Gene> extends Factory , V e r i f i a b l e { public A g e t A l l e l e ( ) ; public G n e w I n s t a n c e ( ) ; public G n e w I n s t a n c e (A a l l e l e ) ; public boolean i s V a l i d ( ) ; } Listing 1.3: Gene interface Listing 1.3 shows the most important methods of the Gene interface. The isValid method, introduced by the Verifiable interface, allows the gene to mark itself as invalid. All invalid genes are replaced with new ones during the evolution phase. The available Gene implementations in the Jenetics library should cover a wide range of problem encodings. Refer to chapter 2.1.1 on page 39 for how to implement your own Gene types. 1.3.1.2 Chromosome A Chromosome is a collection of Genes which contains at least one Gene. This allows to encode problems which requires more than one Gene. Like the Gene interface, the Chromosome is also it’s own factory and allows to create a new Chromosome from a given Gene sequence. 1 2 3 4 5 6 7 8 9 public i n t e r f a c e Chromosome > extends Factory >, I t e r a b l e , V e r i f i a b l e { public Chromosome n e w I n s t a n c e ( ISeq g e n e s ) ; public G getGene ( i n t i n d e x ) ; public ISeq t o S e q ( ) ; public Stream stream ( ) ; public i n t l e n g t h ( ) ; } Listing 1.4: Chromosome interface Listing 1.4 shows the main methods of the Chromosome interface. This are the methods for accessing single Genes by index and as an ISeq respectively, and the factory method for creating a new Chromosome from a given sequence of Genes. The factory method is used by the Alterer classes which were able to create altered Chromosome from a (changed) Gene sequence. Most of the Chromosome implementations can be created with variable length. E. g. the IntegerChromosome can be created with variable length, where the minimum value of the length range is included and the maximum value of the length range is excluded. 1 2 3 IntegerChromosome chromosome = IntegerChromosome . o f ( 0 , 1_000 , IntRange . o f ( 5 , 9 ) ); The factory method of the IntegerChromosome will now create chromosome instances with a length between [rangemin , rangemax ), equally distributed. Figure 1.3.2 on the following page shows the structure of a Chromosome with variable length. 7 1.3. BASE CLASSES CHAPTER 1. FUNDAMENTALS Figure 1.3.2: Chromosome structure 1.3.1.3 Genotype The central class, the evolution Engine is working with, is the Genotype. It is the structural and immutable representative of an individual and consists of one to n Chromosomes. All Chromosomes must be parameterized with the same Gene type, but it is allowed to have different lengths and constraints. The allowed minimal- and maximal values of a NumericChromosome is an example of such a constraint. Within the same chromosome, all numeric gene alleles must lay within the defined minimal- and maximal values. Figure 1.3.3: Genotype structure Figure 1.3.3 shows the Genotype structure. A Genotype consists of NG Chromosomes and a Chromosome consists of NC[i] Genes (depending on the Chromosome). The overall number of Genes of a Genotype is given by the sum of the Chromosome’s Genes, which can be accessed via the Genotype.geneCount() method: NX G −1 Ng = NC[i] (1.3.1) i=0 As already mentioned, the Chromosomes of a Genotype doesn’t have to have necessarily the same size. It is only required that all genes are from the same type and the Genes within a Chromosome have the same constraints; e. g. the same min- and max values for numerical Genes. 1 2 3 4 Genotype g e n o t y p e = Genotype . o f ( DoubleChromosome . o f ( 0 . 0 , 1.0 , 8) , DoubleChromosome . o f ( 1 . 0 , 2 . 0 , 10) , DoubleChromosome . o f ( 0 . 0 , 1 0 . 0 , 9) , 8 1.3. BASE CLASSES 5 6 ); DoubleChromosome . o f ( 0 . 1 , CHAPTER 1. FUNDAMENTALS 0.9 , 5) The code snippet in the listing above creates a Genotype with the same structure as shown in figure 1.3.3 on the preceding page. In this example the DoubleGene has been chosen as Gene type. Genotype vector The Genotype is essentially a two-dimensional composition of Genes. This makes it trivial to create Genotypes which can be treated as a Gene matrices. If its needed to create a vector of Genes, there are two possibilities to do so: 1. creating a row-major or 2. creating a column-major Genotype vector. Each of the two possibilities have specific advantages and disadvantages. Figure 1.3.4: Row-major Genotype vector Figure 1.3.4 shows a Genotype vector in row-major layout. A Genotype vector of length n needs one Chromosome of length n. Each Gene of such a vector obeys the same constraints. E. g., for Genotype vectors containing NumericGenes, all Genes must have the same minimum and maximum values. If the problem space doesn’t need to have different minimum and maximum values, the row-major Genotype vector is the preferred choice. Beside the easier Genotype creation, the available Recombinator alterers are more efficient in exploring the search domain. If the problem space allows equal Gene constraint, the row-major Genotype vector encoding should be chosen. It is easier to create and the available Recombinator classes are more efficient in exploring the search domain. The following code snippet shows the creation of a row-major Genotype vector. All Alterers derived from the Recombinator do a fairly good job in exploring the problem space for row-major Genotype vector. 1 2 3 Genotype g e n o t y p e = Genotype . o f ( DoubleChromosome . o f ( 0 . 0 , 1.0 , 8) ); The column-major Genotype vector layout must be chosen when the problem space requires components (Genes) with different constraints. This is almost the 9 1.3. BASE CLASSES CHAPTER 1. FUNDAMENTALS only reason for choosing the column-major layout. The layout of this Genotype vector is shown in 1.3.5. For a vector of length n, n Chromosomes of length one are needed. Figure 1.3.5: Column-major Genotype vector The code snippet below shows how to create a Genotype vector in columnmajor layout. It’s a little more effort to create such a vector, since every Gene has to be wrapped into a separate Chromosome. The DoubleChromosome in the given example has length of one, when the length parameter is omitted. 1 2 3 4 5 6 Genotype g e n o t y p e = Genotype . o f ( DoubleChromosome . o f ( 0 . 0 , 1.0) , DoubleChromosome . o f ( 1 . 0 , 2.0) , DoubleChromosome . o f ( 0 . 0 , 1 0 . 0 ) , DoubleChromosome . o f ( 0 . 1 , 0.9) ); The greater flexibility of a column-major Genotype vector has to be payed with a lower exploration capability of the Recombinator alterers. Using Crossover alterers will have the same effect as the SwapMutator, when used with row-major Genotype vectors. Recommended alterers for vectors of NumericGenes are: • MeanAlterer9 , • LineCrossover10 and • IntermediateCrossover11 See also 2.3.2 on page 53 for an advanced description on how to use the predefined vector codecs. Genotype scalar A very special case of a Genotype contains only one Chromosome with length one. The layout of such a Genotype scalar is shown in 1.3.6 on the next page. Such Genotypes are mostly used for encoding real function problems. How to create a Genotype for a real function optimization problem, is shown in the code snippet below. The recommended Alterers are the same as for column-major Genotype vectors: MeanAlterer, LineCrossover and IntermediateCrossover. 9 See 1.3.2.2 on page 20. 1.3.2.2 on page 21. 11 See 1.3.2.2 on page 21. 10 See 10 1.3. BASE CLASSES CHAPTER 1. FUNDAMENTALS Figure 1.3.6: Genotype scalar 1 2 3 Genotype g e n o t y p e = Genotype . o f ( DoubleChromosome . o f ( 0 . 0 , 1.0) ); See also 2.3.1 on page 52 for an advanced description on how to use the predefined scalar codecs. 1.3.1.4 Phenotype The Phenotype is the actual representative of an individual and consists of the Genotype and the fitness Function, which is used to (lazily) calculate the Genotype’s fitness value.12 It is only a container which forms the environment of the Genotype and doesn’t change the structure. Like the Genotype, the Phenotype is immutable and can’t be changed after creation. 1 2 3 4 5 6 7 8 9 10 11 public f i n a l c l a s s Phenotype< G extends Gene , G>, C extends Comparable super C> > implements Comparable > { public C g e t F i t n e s s ( ) ; public Genotype getGenotype ( ) ; public long getAge ( long c u r r e n t G e n e r a t i o n ) ; public void e v a l u a t e ( ) ; } Listing 1.5: Phenotype class Listing 1.5 shows the main methods of the Phenotype. The fitness property will return the actual fitness value of the Genotype, which can be fetched with the getGenotype method. To make the runtime behavior predictable, the fitness value is evaluated lazily. Either by querying the fitness property or through the call of the evaluate method. The evolution Engine is calling the evaluate method in a separate step and makes the fitness evaluation time available through the EvolutionDurations class. Additionally to the fitness value, the Phenotype contains the generation when it was created. This allows to calculate the current age and the removal of overaged individuals from the population. 1.3.1.5 Population There is no special class which represents a population. It’s just a collection of phenotypes. As collection class, the Seq interface is used. The own Seq 12 Since the fitness Function is shared by all Phenotypes, calls to the fitness Function must be idempotent. See section 1.3.3.1 on page 22. 11 1.3. BASE CLASSES CHAPTER 1. FUNDAMENTALS implementations allows to express the mutability state of the population at the type level and makes the coding more reliable. For a detailed description of this collection classes see section 1.4.4 on page 36. 1.3.2 Operation classes Genetic operators are used for creating genetic diversity (Alterer) and selecting potentially useful solutions for recombination (Selector). This section gives an overview about the genetic operators available in the Jenetics library. It also contains some theoretical information, which should help you to choose the right combination of operators and parameters, for the problem to be solved. 1.3.2.1 Selector Selectors are responsible for selecting a given number of individuals from the population. The selectors are used to divide the population into survivors and offspring. The selectors for offspring and for the survivors can be chosen independently. The selection process of the Jenetics library acts on Phenotypes and indirectly, via the fitness function, on Genotypes. Direct Gene- or population selection is not supported by the library. 1 2 3 4 5 Engine e n g i n e = Engine . b u i l d e r ( . . . ) . offspringFraction (0.7) . s u r v i v o r s S e l e c t o r (new R o u l e t t e W h e e l S e l e c t o r <>() ) . o f f s p r i n g S e l e c t o r (new T o u r n a m e n t S e l e c t o r <>() ) . build () ; The offspringFraction, fO ∈ [0, 1], determines the number of selected offspring NOg = kOg k = rint (kPg k · fO ) (1.3.2) and the number of selected survivors NSg = kSg k = kPg k − kOg k . (1.3.3) The Jenetics library contains the following selector implementations: • TournamentSelector • LinearRankSelector • TruncationSelector • ExponentialRankSelector • MonteCarloSelector • BoltzmannSelector • ProbabilitySelector • StochasticUniversalSelector • RouletteWheelSelector • EliteSelector Beside the well known standard selector implementation the ProbabilitySelector is the base of a set of fitness proportional selectors. 12 1.3. BASE CLASSES CHAPTER 1. FUNDAMENTALS Tournament selector In tournament selection the best individual from a random sample of s individuals is chosen from the population Pg . The samples are drawn with replacement. An individual will win a tournament only if the fitness is greater than the fitness of the other s − 1 competitors. Note that the worst individual never survives, and the best individual wins in all the tournaments it participates. The selection pressure can be varied by changing the tournament size s. For large values of s, weak individuals have less chance of being selected. Compared with fitness proportional selectors, the tournament selector is often used in practice because of its lack of stochastic noise. Tournament selectors are also independent to the scaling of the genetic algorithm fitness function. Truncation selector In truncation selection individuals are sorted according to their fitness and only the n best individuals are selected. The truncation selection is a very basic selection algorithm. It has it’s strength in fast selecting individuals in large populations, but is not very often used in practice; whereas the truncation selection is a standard method in animal and plant breeding. Only the best animals, ranked by their phenotypic value, are selected for reproduction. Monte Carlo selector The Monte Carlo selector selects the individuals from a given population randomly. Instead of a directed search, the Monte Carlo selector performs a random search. This selector can be used to measure the performance of a other selectors. In general, the performance of a selector should be better than the selection performance of the Monte Carlo selector. If the Monte Carlo selector is used for selecting the parents for the population, it will be a little bit more disruptive, on average, than roulette wheel selection.[29] Probability selectors Probability selectors are a variation of fitness proportional selectors and selects individuals from a given population based on it’s selection probability P (i). Fitness proportional selection works as shown in Figure 1.3.7: Fitness proportional selection figure 1.3.7. An uniform distributed random number r ∈ [0, F ) specifies which individual is selected, by argument minimization: ( ) n X i ← argmin r < fi , (1.3.4) n∈[0,N ) i=0 where N is the number of individuals and fi the fitness value of the ith individual. The probability selector works the same way, only the fitness value fi is replaced 13 1.3. BASE CLASSES CHAPTER 1. FUNDAMENTALS by the individual’s selection probability P (i). It is not necessary to sort the population. The selection probability of an individual i follows a binomial distribution n n−k P (i, k) = P (i) k (1 − P (i)) (1.3.5) k where n is the overall number of selected individuals andk the number of individual i in the set of selected individuals. The runtime complexity of the implemented probability selectors is O (n + log (n)) instead of O (n2) as for the naive approach: A binary (index) search is performed on the summed probability array. Roulette-wheel selector The roulette-wheel selector is also known as fitness proportional selector and Jenetics implements it as probability selector. For calculating the selection probability P (i), the fitness value fi of individual i is used. fi P (i) = PN −1 (1.3.6) j=0 fj Selecting n individuals from a given population is equivalent to play n times on the roulette-wheel. The population don’t have to be sorted before selecting the individuals. Notice that equation 1.3.6 assumes that all fitness values are positive and the sum of the fitness values is not zero. To cope with negative fitnesses, an adapted formula is used for calculating the selection probabilities. fi − fmin , P 0 (i) = PN −1 j=0 (fj − fmin ) where (1.3.7) fmin = min {fi , 0} i∈[0,N ) As you can see, the worst fitness value fmin , if negative, has now a selection probability of zero. In the case that the sum of the corrected fitness values is zero, the selection probability of all fitness values will be set N1 . Linear-rank selector The roulette-wheel selector will have problems when the fitness values differ very much. If the best chromosome fitness is 90%, its circumference occupies 90% of roulette-wheel, and then other chromosomes have too few chances to be selected.[29] In linear-ranking selection the individuals are sorted according to their fitness values. The rank N is assigned to the best individual and the rank 1 to the worst individual. The selection probability P (i) of individual i is linearly assigned to the individuals according to their rank. i−1 1 P (i) = n− + n+ − n− . (1.3.8) N N −1 − + Here nN is the probability of the worst individual to be selected and nN the probability of the best individual to be selected. As the population size is held constant, the condition n+ = 2 − n− and n− ≥ 0 must be fulfilled. Note that all individuals get a different rank, respectively a different selection probability, even if they have the same fitness value.[5] 14 1.3. BASE CLASSES CHAPTER 1. FUNDAMENTALS Exponential-rank selector An alternative to the weak linear-rank selector is to assign survival probabilities to the sorted individuals using an exponential function: ci−1 , (1.3.9) −1 wherec must within the range [0, 1). A small value of c increases the probability of the best individual to be selected. If c is set to zero, the selection probability of the best individual is set to one. The selection probability of all other individuals is zero. A value near one equalizes the selection probabilities. This selector sorts the population in descending order before calculating the selection probabilities. P (i) = (c − 1) cN Boltzmann selector The selection probability of the Boltzmann selector is defined as eb·fi P (i) = , (1.3.10) Z where b is a parameter which controls the selection intensity and Z is defined as Z= n X efi . (1.3.11) i=1 Positive values of b increases the selection probability of individuals with high fitness values and negative values of b decreases it. If b is zero, the selection probability of all individuals is set to N1 . Stochastic-universal selector Stochastic-universal selection[1] (SUS) is a method for selecting individuals according to some given probability in a way that minimizes the chance of fluctuations. It can be viewed as a type of roulette game where we now have p equally spaced points which we spin. SUS uses a single random value for selecting individuals by choosing them at equally spaced intervals. Weaker members of the population (according to their fitness) have a better chance to be chosen, which reduces the unfair nature of fitnessproportional selection methods. The selection method was introduced by James Baker.[2] Figure 1.3.8 shows the function of the stochastic-universal selection, Figure 1.3.8: Stochastic-universal selection where n is the number of individuals to select. Stochastic universal sampling ensures a selection of offspring, which is closer to what is deserved than roulette wheel selection.[29] 15 1.3. BASE CLASSES CHAPTER 1. FUNDAMENTALS Elite selector The EliteSelector copies a small proportion of the fittest candidates, without changes, into the next generation. This may have a dramatic impact on performance by ensuring that the GA doesn’t waste time re-discovering previously refused partial solutions. Individuals that are preserved through elitism remain eligible for selection as parents of the next generation. Elitism is also related with memory: remember the best solution found so far. A problem with elitism is that it may causes the GA to converge to a local optimum, so pure elitism is a race to the nearest local optimum. The elite selector implementation of the Jenetics library also lets you specify the selector for the non-elite individuals. 1.3.2.2 Alterer The problem encoding/representation determines the bounds of the search space, but the Alterers determine how the space can be traversed: Alterers are responsible for the genetic diversity of the EvolutionStream. The two Alterer hierarchies used in Jenetics are: 1. mutation and 2. recombination (e. g. crossover). First we will have a look at the mutation — There are two distinct roles mutation plays in the evolution process: 1. Exploring the search space: By making small moves, mutation allows a population to explore the search space. This exploration is often slow compared to crossover, but in problems where crossover is disruptive this can be an important way to explore the landscape. 2. Maintaining diversity: Mutation prevents a population from converging to a local minimum by stopping the solution to become to close to one another. A genetic algorithm can improve the solution solely by the mutation operator. Even if most of the search is being performed by crossover, mutation can be vital to provide the diversity which crossover needs. The mutation probability, P (m), is the parameter that must be optimized. The optimal value of the mutation rate depends on the role mutation plays. If mutation is the only source of exploration (if there is no crossover), the mutation rate should be set to a value that ensures that a reasonable neighborhood of solutions is explored. The mutation probability, P (m), is defined as the probability that a specific gene, over the whole population, is mutated. That means, the (average) number of genes mutated by a mutator is µ̂ = NP · Ng · P (m) (1.3.12) where Ng is the number of available genes of a genotype and NP the population size (revere to equation 1.3.1 on page 8). 16 1.3. BASE CLASSES CHAPTER 1. FUNDAMENTALS Mutator The mutator has to deal with the problem, that the genes are arranged in a 3D structure (see chapter 1.3.1.3). The mutator selects the gene which will be mutated in three steps: 1. Select a genotype G[i] from the population with probability PG (m), 2. select a chromosome C[j] from the selected genotype G[i] with probability PC (m) and 3. select a gene g[k] from the selected chromosome C[j] with probability Pg (m). The needed sub-selection probabilities are set to PG (m) = PC (m) = Pg (m) = p 3 P (m). (1.3.13) Gaussian mutator The Gaussian mutator performs the mutation of number genes. This mutator picks a new value based on a Gaussian distribution around the current value of the gene. The variance of the new value (before clipping to the allowed gene range) will be σ̂ = 2 gmax − gmin 4 2 (1.3.14) where gmin and gmax are the valid minimum and maximum values of the number gene. The new value will be cropped to the gene’s boundaries. Swap mutator The swap mutator changes the order of genes in a chromosome, with the hope of bringing related genes closer together, thereby facilitating the production of building blocks. This mutation operator can also be used for combinatorial problems, where no duplicated genes within a chromosome are allowed, e. g. for the TSP. The second alterer type is the recombination — An enhanced genetic algorithm (EGA) combine elements of existing solutions in order to create a new solution, with some of the properties of each parents. Recombination creates a new chromosome by combining parts of two (or more) parent chromosomes. This combination of chromosomes can be made by selecting one or more crossover points, splitting these chromosomes on the selected points, and merge those portions of different chromosomes to form new ones. 1 2 3 4 5 6 7 8 9 void r e c o m b i n e ( f i n a l ISeq > pop ) { // S e l e c t t h e Genotypes f o r c r o s s o v e r . f i n a l Random random = RandomRegistry . getRandom ( ) ; f i n a l i n t i 1 = random . n e x t I n t ( pop . l e n g t h ( ) ) ; f i n a l i n t i 2 = random . n e x t I n t ( pop . l e n g t h ( ) ) ; f i n a l Phenotype pt1 = pop . g e t ( i 1 ) ; f i n a l Phenotype pt2 = pop . g e t ( 2 ) ; f i n a l Genotype g t 1 = pt1 . getGenotype ( ) ; f i n a l Genotype g t 2 = pt2 . getGenotype ( ) ; 10 11 12 // Choosing t h e Chromosome f o r c r o s s o v e r . f i n a l int chIndex = 17 1.3. BASE CLASSES CHAPTER 1. FUNDAMENTALS random . n e x t I n t ( min ( g t 1 . l e n g t h ( ) , g t 2 . l e n g t h ( ) ) ) ; f i n a l MSeq > c1 = g t 1 . t o S e q ( ) . copy ( ) ; f i n a l MSeq > c2 = g t 2 . t o S e q ( ) . copy ( ) ; f i n a l MSeq g e n e s 1 = c1 . g e t ( c h I n d e x ) . t o S e q ( ) . copy ( ) ; f i n a l MSeq g e n e s 2 = c2 . g e t ( c h I n d e x ) . t o S e q ( ) . copy ( ) ; 13 14 15 16 17 18 // Perform t h e c r o s s o v e r . c r o s s o v e r ( genes1 , genes2 ) ; c1 . s e t ( chIndex , c1 . g e t ( c h I n d e x ) . n e w I n s t a n c e ( g e n e s 1 . t o I S e q ( ) ) ) ; c2 . s e t ( chIndex , c2 . g e t ( c h I n d e x ) . n e w I n s t a n c e ( g e n e s 2 . t o I S e q ( ) ) ) ; 19 20 21 22 23 24 25 26 27 28 } // C r e a t i n g two new Phenotypes and r e p l a c e t h e o l d one . MSeq > r e s u l t = pop . copy ( ) ; r e s u l t . s e t ( i 1 , pt1 . n e w I n s t a n c e ( g t 1 . n e w I n s t a n c e ( c1 . t o I S e q ( ) ) ) ) ; r e s u l t . s e t ( i 2 , pt2 . n e w I n s t a n c e ( g t 1 . n e w I n s t a n c e ( c2 . t o I S e q ( ) ) ) ) ; Listing 1.6: Chromosome selection for recombination Listing 1.6 on the preceding page shows how two chromosomes are selected for recombination. It is done this way for preserving the given constraints and to avoid the creation of invalid individuals. Because of the possible different Chromosome length and/or Chromosome constraints within a Genotype, only Chromosomes with the same Genotype position are recombined (see listing 1.6 on the previous page). The recombination probability, P (r), determines the probability that a given individual (genotype) of a population is selected for recombination. The (mean) number of changed individuals depend on the concrete implementation and can be vary from P (r) · NG to P (r) · NG · OR , where OR is the order of the recombination, which is the number of individuals involved in the combine method. Single-point crossover The single-point crossover changes two children chromosomes by taking two chromosomes and cutting them at some, randomly chosen, site. If we create a child and its complement we preserve the total number of genes in the population, preventing any genetic drift. Single-point crossover is the classic form of crossover. However, it produces very slow mixing compared with multi-point crossover or uniform crossover. For problems where the site position has some intrinsic meaning to the problem single-point crossover can lead to smaller disruption than multiple-point or uniform crossover. Figure 1.3.9 shows how the SinglePointCrossover class is performing the crossover for different crossover points—in the given example for the chromosome indexes 0,1,3,6 and 7. Multi-point crossover If the MultiPointCrossover class is created with one crossover point, it behaves exactly like the single-point crossover. The following picture shows how the multi-point crossover works with two crossover points, defined at index 1 and 4. Figure 1.3.11 you can see how the crossover works for an odd number of crossover points. 18 1.3. BASE CLASSES CHAPTER 1. FUNDAMENTALS Figure 1.3.9: Single-point crossover Figure 1.3.10: 2-point crossover Partially-matched crossover The partially-matched crossover guarantees that all genes are found exactly once in each chromosome. No gene is duplicated by this crossover strategy. The partially-matched crossover (PMX) can be applied usefully in the TSP or other permutation problem encodings. Permutation encoding is useful for all problems where the fitness only depends on the ordering of the genes within the chromosome. This is the case in many combinatorial optimization problems. Other crossover operators for combinatorial optimization are: • order crossover • edge recombination crossover • cycle crossover • edge assembly crossover The PMX is similar to the two-point crossover. A crossing region is chosen by selecting two crossing points (see figure 1.3.12 a)). After performing the crossover we–normally–got two invalid chromosomes (figure 1.3.12 b)). Chromosome 1 contains the value 6 twice and misses the value 3. On the other side chromosome 2 contains the value 3 twice and misses the value 6. We can observe that this crossover is equivalent to the exchange of the values 3→6, 4→5 and 5→4. To repair the two chromosomes we have to apply this exchange outside the crossing region (figure 1.3.12 b)). At the end figure 1.3.12 c) shows the repaired chromosome. Uniform crossover In uniform crossover, the genes at index i of two chromosomes are swapped with the swap-probability, pS . Empirical studies shows that uniform crossover is a more exploitative approach than the traditional 19 1.3. BASE CLASSES CHAPTER 1. FUNDAMENTALS Figure 1.3.11: 3-point crossover Figure 1.3.12: Partially-matched crossover exploitative approach that maintains longer schemata. This leads to a better search of the design space with maintaining the exchange of good information.[8] Figure 1.3.13: Uniform crossover Figure 1.3.13 shows an example of a uniform crossover with four crossover points. A gene is swapped, if a uniformly created random number, r ∈ [0, 1], is smaller than the swap-probability, pS . The following code snippet shows how these swap indexes are calculated, in a functional way. 1 2 3 4 5 6 f i n a l Random random = RandomRegistry . getRandom ( ) ; f i n a l int length = 8 ; f i n a l double ps = 0 . 5 ; f i n a l i n t [ ] i n d e x e s = IntRange . r a n g e ( 0 , l e n g t h ) . f i l t e r ( i −> random . nextDouble ( ) < ps ) . toArray ( ) ; Mean alterer The Mean alterer works on genes which implement the Mean interface. All numeric genes implement this interface by calculating the arithmetic mean of two genes. 20 1.3. BASE CLASSES CHAPTER 1. FUNDAMENTALS Line crossover The line crossover13 takes two numeric chromosomes and treats it as a real number vector. Each of this vectors can also be seen as a point in Rn . If we draw a line through this two points (chromosome), we have the possible values of the new chromosomes, which all lie on this line. Figure 1.3.14: Line crossover hypercube Figure 1.3.14 shows how the two chromosomes form the two three-dimensional vectors (black circles). The dashed line, connecting the two points, form the possible solutions created by the line crossover. An additional variable, p, determines how far out along the line the created children will be. If p = 0 then the children will be located along the line within the hypercube. If p > 0, the children may be located on an arbitrary place on the line, even outside of the hypercube.This is useful if you want to explore unknown regions, and you need a way to generate chromosomes further out than the parents are. The internal random parameters, which define the location of the new crossover point, are generated once for the whole vector (chromosome). If the LineCrossover generates numeric genes which lie outside the allowed minimum and maximum value, it simply uses the original gene and rejects the generated, invalid one. Intermediate crossover The intermediate crossover is quite similar to the line crossover. It differs in the way on how the internal random parameters are generated and the handling of the invalid–out of range–genes. The internal random parameters of the IntermediateCrossover class are generated for each gene of the chromosome, instead once for all genes. If the newly generated gene is not within the allowed range, a new one is created. This is repeated, until a valid gene is built. The crossover parameter, p, has the same properties as for the line crossover. If the chosen value for p is greater than 0, it is likely that some genes must be created more than once, because they are not in the valid range. The probability for gene re-creation rises sharply with the value of p. Setting p to a value greater than one, doesn’t make sense in most of the cases. A value greater than 10 should be avoided. 1.3.3 Engine classes The executing classes, which perform the actual evolution, are located in the io.jenetics.engine package. The evolution stream (EvolutionStream) is the 13 The line crossover, also known as line recombination, was originally described by Heinz Mühlenbein and Dirk Schlierkamp-Voosen.[23] 21 1.3. BASE CLASSES CHAPTER 1. FUNDAMENTALS base metaphor for performing an GA. On the EvolutionStream you can define the termination predicate and than collect the final EvolutionResult. This decouples the static data structure from the executing evolution part. The EvolutionStream is also very flexible, when it comes to collecting the final result. The EvolutionResult class has several predefined collectors, but you are free to create your own one, which can be seamlessly plugged into the existing stream. 1.3.3.1 Fitness function The fitness Function is also an important part when modeling an genetic algorithm. It takes a Genotype as argument and returns, at least, a Comparable object as result—the fitness value. This allows the evolution Engine, respectively the selection operators, to select the offspring- and survivor population. Some selectors have stronger requirements to the fitness value than a Comparable, but this constraints is checked by the Java type system at compile time. Since the fitness Function is shared by all Phenotypes, calls to the fitness Function has to be idempotent. A fitness Function is idempotent if, whenever it is applied twice to any Genotype, it returns the same fitness value as if it were applied once. In the simplest case, this is achieved by Functions which doesn’t contain any global mutable state. The following example shows the simplest possible fitness Function. This Function simply returns the allele of a 1x1 float Genotype. 1 2 3 4 public c l a s s Main { s t a t i c Double i d e n t i t y ( f i n a l Genotype g t ) { return g t . getGene ( ) . g e t A l l e l e ( ) ; } 5 public s t a t i c void main ( f i n a l S t r i n g [ ] a r g s ) { // C r e a t e f i t n e s s f u n c t i o n from method r e f e r e n c e . Function , Double>> f f 1 = Main : : i d e n t i t y ; 6 7 8 9 10 11 12 13 14 15 } } // C r e a t e f i t n e s s f u n c t i o n from lambda e x p r e s s i o n . Function , Double>> f f 2 = g t −> g t . getGene ( ) . g e t A l l e l e ( ) ; The first type parameter of the Function defines the kind of Genotype from which the fitness value is calculated and the second type parameter determines the return type, which must be, at least, a Comparable type. 1.3.3.2 Engine The evolution Engine controls how the evolution steps are executed. Once the Engine is created, via a Builder class, it can’t be changed. It doesn’t contain any mutable global state and can therefore safely used/called from different threads. This allows to create more than one EvolutionStreams from the Engine and execute them in parallel. 22 1.3. BASE CLASSES 1 2 3 4 5 6 7 8 9 10 11 12 13 CHAPTER 1. FUNDAMENTALS public f i n a l c l a s s Engine< G extends Gene , G>, C extends Comparable super C> > implements Function , E v o l u t i o n R e s u l t >, E v o l u t i o n S t r e a m a b l e { // The e v o l u t i o n f u n c t i o n , p e r f o r m s one e v o l u t i o n s t e p . E v o l u t i o n R e s u l t e v o l v e ( ISeq > p o p u l a t i o n , long g e n e r a t i o n ); 14 // E v o l u t i o n stream f o r " normal " e v o l u t i o n e x e c u t i o n . E v o l u t i o n S t r e a m stream ( ) ; 15 16 17 18 19 20 } // E v o l u t i o n i t e r a t o r f o r e x t e r n a l e v o l u t i o n i t e r a t i o n . I t e r a t o r > i t e r a t o r ( ) ; Listing 1.7: Engine class Listing 1.7 shows the main methods of the Engine class. It is used for performing the actual evolution of a give population. One evolution step is executed by calling the Engine.evolve method, which returns an EvolutionResult object. This object contains the evolved population plus additional information like execution duration of the several evolution sub-steps and information about the killed and as invalid marked individuals. With the stream method you create a new EvolutionStream, which is used for controlling the evolution process—see section 1.3.3.3 on page 25. Alternatively it is possible to iterate through the evolution process in an imperative way (for whatever reasons this should be necessary). Just create an Iterator of EvolutionResult object by calling the iterator method. As already shown in previous examples, the Engine can only be created via its Builder class. Only the fitness Function and the Chromosomes, which represents the problem encoding, must be specified for creating an Engine instance. For the rest of the parameters default values are specified. This are the Engine parameters which can configured: alterers A list of Alterers which are applied to the offspring population, in the defined order. The default value of this property is set to SinglePointCrossover<>(0.2) followed by Mutator<>(0.15). clock The java.time.Clock used for calculating the execution durations. A Clock with nanosecond precision (System.nanoTime()) is used as default. executor With this property it is possible to change the java.util.concurrent.Executor engine used for evaluating the evolution steps. This property can be used to define an application wide Executor or for controlling the number of execution threads. The default value is set to ForkJoinPool.commonPool(). evaluator This property allows you to replace the evaluation strategy of the Phenotype’s fitness function. Normally, each fitness value is evaluated concurrently, but independently from each other. In some configuration 23 1.3. BASE CLASSES CHAPTER 1. FUNDAMENTALS it is necessary, for performance reason, to evaluate the fitness values of a population at once. This is then performed by the Engine.Evaluator or Engine.GenotypeEvaluator interface. fitnessFunction This property defines the fitness Function used by the evolution Engine. (See section 1.3.3.1 on page 22.) genotypeFactory Defines the Genotype Factory used for creating new individuals. Since the Genotype is its own Factory, it is sufficient to create a Genotype, which then serves as template. genotypeValidator This property lets you override the default implementation of the Genotype.isValid method, which is useful if the Genotype validity not only depends on valid property of the elements it consists of. maximalPhenotypeAge Set the maximal allowed age of an individual (Phenotype). This prevents super individuals to live forever. The default value is set to 70. offspringFraction Through this property it is possible to define the fraction of offspring (and survivors) for evaluating the next generation. The fraction value must within the interval [0, 1]. The default value is set to 0.6. Additionally to this property, it is also possible to set the survivorsFraction, survivorsSize or offspringSize. All this additional properties effectively set the offspringFraction. offspringSelector This property defines the Selector used for selecting the offspring population. The default values is set to TournamentSelector<>(3). optimize With this property it is possible to define whether the fitness Function should be maximized of minimized. By default, the fitness Function is maximized. phenotypeValidator This property lets you override the default implementation of the Phenotype.isValid method, which is useful if the Phenotype validity not only depends on valid property of the elements it consists of. populationSize Defines the number of individuals of a population. The evolution Engine keeps the number of individuals constant. That means, the population of the EvolutionResult always contains the number of entries defined by this property. The default value is set to 50. selector This method allows to set the offspringSelector and survivorsSelector in one step with the same selector. survivorsSelector This property defines the Selector used for selecting the survivors population. The default values is set to TournamentSelector<>(3). individualCreationRetries The evolution Engine tries to create only valid individuals. If a newly created Genotype is not valid, the Engine creates another one, till the created Genotype is valid. This parameter sets the maximal number of retries before the Engine gives up and accept invalid individuals. The default value is set to 10. 24 1.3. BASE CLASSES CHAPTER 1. FUNDAMENTALS mapper This property lets you define an mapper, which transforms the final EvolutionResult object after every generation. One usage of the mapper is to remove duplicate individuals from the population. The EvolutionResult.toUniquePopulation() method provides such a de-duplication mapper. The EvolutionStreams created by the Engine class are unlimited. Such streams must be limited by calling the available EvolutionStream.limit methods. Alternatively, the Engine instance itself can be limited with the Engine.limit methods. This limited Engines no long creates infinite EvolutionStreams, they are truncated by the limit predicate defined by the Engine. This feature is needed when you are concatenating evolution Engines (see section 4.1.5.1 on page 85). 1 2 3 4 5 f i n a l E v o l u t i o n S t r e a m a b l e e n g i n e = Engine . b u i l d e r ( problem ) . minimizing ( ) . build () . l i m i t ( ( ) −> L i m i t s . b y S t e a d y F i t n e s s ( 1 0 ) ) ; As shown in the example code above, one important difference between the Engine.limit and the EvolutionStream.limit method is, that the limit method of the Engine takes a limiting Predicate Supplier instead of the the Predicate itself. The reason for this is that some Predicates has to maintain internal state to work properly. This means, every time the Engine creates a new stream, it must also create a new limiting Predicate. 1.3.3.3 EvolutionStream The EvolutionStream controls the execution of the evolution process and can be seen as a kind of execution handle. This handle can be used to define the termination criteria and to collect the final evolution result. Since the EvolutionStream extends the Java Stream interface, it integrates smoothly with the rest of the Java Stream API.14 1 2 3 4 5 6 7 8 9 public i n t e r f a c e E v o l u t i o n S t r e a m < G extends Gene , G>, C extends Comparable super C> > extends Stream > { public E v o l u t i o n S t r e a m l i m i t ( P r e d i c a t e super E v o l u t i o n R e s u l t > p r o c e e d ) ; } Listing 1.8: EvolutionStream class Listing 1.8 shows the whole EvolutionStream interface. As it can be seen, it only adds one additional method. But this additional limit method allows to truncate the EvolutionStream based on a Predicate which takes an EvolutionResult. Once the Predicate returns false, the evolution process is stopped. Since the limit method returns an EvolutionStream, it is possible to define more than one Predicate, which both must be fulfilled to continue the evolution process. 14 It is recommended to make yourself A good introduction can be found here: java8-stream-tutorial-examples/ 25 familiar with the Java Stream API. http://winterbe.com/posts/2014/07/31/ 1.3. BASE CLASSES 1 2 3 4 5 CHAPTER 1. FUNDAMENTALS Engine e n g i n e = . . . E v o l u t i o n S t r e a m stream = e n g i n e . stream ( ) . limit ( predicate1 ) . limit ( predicate2 ) . l i m i t (100) ; The EvolutionStream, created in the example above, will be truncated if one of the two predicates is false or if the maximal allowed generations, of 100, is reached. An EvolutionStream is usually created via the Engine.stream() method. The immutable and stateless nature of the evolution Engine allows to create more than one EvolutionStream with the same Engine instance. The generations of the EvolutionStream are evolved serially. Calls of the EvolutionStream methods (e. g. limit, peek, ...) are executed in the thread context of the created Stream. In a typical setup, no additional synchronization and/or locking is needed. In cases where you appreciate the usage of the EvolutionStream but need a different Engine implementation, you can use the EvolutionStream.of factory method for creating an new EvolutionStream. 1 2 3 4 5 s t a t i c , C extends Comparable super C>> E v o l u t i o n S t r e a m o f ( S u p p l i e r > s t a r t , Function super E v o l u t i o n S t a r t , E v o l u t i o n R e s u l t > f ); This factory method takes a start value, of type EvolutionStart, and an evolution Function. The evolution Function takes the start value and returns an EvolutionResult object. To make the runtime behavior more predictable, the start value is fetched/created lazily at the evolution start time. 1 2 3 f i n a l S u p p l i e r > s t a r t = . . . f i n a l E v o l u t i o n S t r e a m stream = E v o l u t i o n S t r e a m . o f ( s t a r t , new MySpecialEngine ( ) ) ; 1.3.3.4 EvolutionResult The EvolutionResult collects the result data of an evolution step into an immutable value class. This class is the type of the stream elements of the EvolutionStream, as described in section 1.3.3.3 on the previous page. 1 2 3 4 5 6 7 8 9 public f i n a l c l a s s E v o l u t i o n R e s u l t < G extends Gene , G>, C extends Comparable super C> > implements Comparable > { ISeq > g e t P o p u l a t i o n ( ) ; long g e t G e n e r a t i o n ( ) ; } Listing 1.9: EvolutionResult class Listing 1.3.3.4 shows the two most important properties, the population and the generation the result belongs to. This are also the two properties needed 26 1.3. BASE CLASSES CHAPTER 1. FUNDAMENTALS for the next evolution step. The generation is, of course, incremented by one. To make collecting the EvolutionResult object easier, it also implements the Comparable interface. Two EvolutionResults are compared by its best Phenotype. The EvolutionResult classes has three predefined factory methods, which will return Collectors usable for the EvolutionStream: toBestEvolutionResult() Collects the best EvolutionResult of a EvolutionStream according to the defined optimization strategy. toBestPhenotype() This collector can be used if you are only interested in the best Phenotype. toBestGenotype() Use this collector if you only need the best Genotype of the EvolutionStream. The following code snippets shows how to use the different EvolutionStream collectors. 1 2 3 // C o l l e c t i n g t h e b e s t E v o l u t i o n R e s u l t o f t h e E v o l u t i o n S t r e a m . E v o l u t i o n R e s u l t r e s u l t = stream . c o l l e c t ( EvolutionResult . toBestEvolutionResult () ) ; 4 5 6 7 // C o l l e c t i n g t h e b e s t Phenotype o f t h e E v o l u t i o n S t r e a m . Phenotype r e s u l t = stream . c o l l e c t ( EvolutionResult . toBestPhenotype ( ) ) ; 8 9 10 11 // C o l l e c t i n g t h e b e s t Genotype o f t h e E v o l u t i o n S t r e a m . Genotype r e s u l t = stream . c o l l e c t ( E v o l u t i o n R e s u l t . toBestGenotype ( ) ) ; 1.3.3.5 EvolutionStatistics The EvolutionStatistics class allows you to gather additional statistical information from the EvolutionStream. This is especially useful during the development phase of the application, when you have to find the right parametrization of the evolution Engine. Besides other informations, the EvolutionStatistics contains (statistical) information about the fitness, invalid and killed Phenotypes and runtime information of the different evolution steps. Since the EvolutionStatistics class implements the Consumer > interface, it can be easily plugged into the EvolutionStream, adding it with the peek method of the stream. 1 2 3 4 5 6 7 Engine e n g i n e = . . . E v o l u t i o n S t a t i s t i c s , Double> s t a t i s t i c s = E v o l u t i o n S t a t i s t i c s . ofNumber ( ) ; e n g i n e . stream ( ) . l i m i t (100) . peek ( s t a t i s t i c s ) . c o l l e c t ( toBestGenotype ( ) ) ; Listing 1.10: EvolutionStatistics usage Listing 1.10 shows how to add the the EvolutionStatistics to the EvolutionStream. Once the algorithm tuning is finished, it can be removed in the production environment. 27 1.3. BASE CLASSES CHAPTER 1. FUNDAMENTALS There are two different specializations of the EvolutionStatistics object available. The first is the general one, which will be working for every kind of Genes and fitness types. It can be created via the EvolutionStatistics.ofComparable() method. The second one collects additional statistical data for numeric fitness values. This can be created with the EvolutionStatistics.ofNumber() method. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -+ | Time statistics | + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -+ | Selection : sum =0 .0 4 65 38 2 78 00 0 s ; mean = 0 .0 03 8 78 18 98 3 3 s | | Altering : sum = 0. 08 6 15 54 57 0 00 s ; mean =0 . 00 71 7 96 21 4 17 s | | Fitness calculation : sum = 0. 02 2 90 1 60 6 00 0 s ; mean = 0 .0 01 9 08 4 67 16 7 s | | Overall execution : sum =0 .1 4 72 98 0 67 00 0 s ; mean =0 .0 1 22 74 83 8 91 7 s | + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -+ | Evolution statistics | + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -+ | Generations : 12 | | Altered : sum =7 ,331; mean =610 .91 6666 667 | | Killed : sum =0; mean =0.000000000 | | Invalids : sum =0; mean =0.000000000 | + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -+ | Population statistics | + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -+ | Age : max =11; mean =1.951000; var =5.545190 | | Fitness : | | min = 0.0000 00000000 | | max = 4 8 1. 7 4 8 2 2 7 1 1 4 5 3 7 | | mean = 3 8 4 . 4 3 0 3 4 5 0 7 8 6 6 0 | | var = 1 3 0 0 6 . 1 3 2 5 3 7 3 0 1 5 2 8 | + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -+ A typical output of an number EvolutionStatistics object will look like the example above. The EvolutionStatistics object is a simple for inspecting the EvolutionStream after it is finished. It doesn’t give you a live view of the current evolution process, which can be necessary for long running streams. In such cases you have to maintain/update the statistics yourself. 1 2 3 public c l a s s TSM { // The l o c a t i o n s t o v i s i t . s t a t i c f i n a l ISeq POINTS = I S e q . o f ( . . . ) ; 4 5 6 7 // The p e r m u t a t i o n c o d e c . s t a t i c f i n a l Codec , EnumGene > CODEC = Codecs . o f P e r m u t a t i o n (POINTS) ; 8 9 10 // The f i t n e s s f u n c t i o n ( i n t h e problem domain ) . s t a t i c double d i s t ( f i n a l ISeq p ) { . . . } 11 12 13 14 15 16 // The e v o l u t i o n e n g i n e . s t a t i c f i n a l Engine , Double> ENGINE = Engine . b u i l d e r (TSM : : d i s t , CODEC) . o p t i m i z e ( Optimize .MINIMUM) . build () ; 17 18 19 // Best phenotype found s o f a r . s t a t i c Phenotype , Double> b e s t = n u l l ; 20 21 22 23 24 25 26 // You w i l l be i n f o r m e d on new r e s u l t s . This a l l o w s t o // r e a c t on new b e s t phenotypes , e . g . l o g i t . private s t a t i c void update ( f i n a l E v o l u t i o n R e s u l t , Double> r e s u l t ) { i f ( b e s t == n u l l | | 28 1.3. BASE CLASSES 27 { 28 29 30 31 32 } 33 } CHAPTER 1. FUNDAMENTALS b e s t . compareTo ( r e s u l t . g e t B e s t P h e n o t y p e ( ) ) < 0 ) best = r e s u l t . getBestPhenotype ( ) ; System . out . p r i n t ( r e s u l t . g e t G e n e r a t i o n ( ) + " : " ) ; System . out . p r i n t l n ( " Found b e s t phenotype : " + b e s t ) ; 34 35 36 37 38 39 40 41 42 43 44 45 } // Find t h e s o l u t i o n . public s t a t i c void main ( f i n a l S t r i n g [ ] a r g s ) { f i n a l ISeq r e s u l t = CODEC. de c ode ( ENGINE . stream ( ) . peek (TSM : : update ) . limit (10) . c o l l e c t ( E v o l u t i o n R e s u l t . toBestGenotype ( ) ) ); System . out . p r i n t l n ( r e s u l t ) ; } Listing 1.11: Live evolution statistics Listing 1.11 on the preceding page shows how to implement a manual statistics gathering. The update method is called whenever a new EvolutionResult is has been calculated. If a new best Phenotype is available, it is stored and logged. With the TSM::update method, which is called on every finished generation, you have alive view on the evolution progress. 1.3.3.6 Engine.Evaluator This interface allows to define different strategies for evaluating the fitness functions of a given population. Usually, it is not necessary to override the default evaluation strategy. It is helpful if you have performance problems when the fitness function is evaluated serially—or in small concurrent batches, as it is implemented by the default strategy. In this case, the Engine.Evaluator interface can be used to calculate the fitness function for a population in one batch. 1 2 3 4 5 6 public i n t e r f a c e E v a l u a t o r < G extends Gene , G>, C extends Comparable super C> > { ISeq > e v a l u a t e ( Seq > pop ) ; } Listing 1.12: Engine.Evaluator interface The implementer is free to do the evaluation in place, or create new Phenotype instance and return the newly created one. A second use case for the Evaluator interface is, when the fitness value also depends on the current composition of the population. E. g. it is possible to normalize the population’s fitness values. 29 1.4. NUTS AND BOLTS 1.4 CHAPTER 1. FUNDAMENTALS Nuts and bolts 1.4.1 Concurrency The Jenetics library parallelizes independent task whenever possible. Especially the evaluation of the fitness function is done concurrently. That means that the fitness function must be thread safe, because it is shared by all phenotypes of a population. The easiest way for achieving thread safety is to make the fitness function immutable and re-entrant. Since the number of individuals of one population, which determines the number of fitness functions to be evaluated, is usually much higher then the number of available CPU cores, the fitness evaluation is done in batches. This reduces the evaluation overhead, for lightweight fitness functions. Figure 1.4.1: Evaluation batch Figure 1.4.1 shows an example population with 12 individuals. The evaluation of the phenotype’s fitness functions are evaluated in batches with three elements. For this purpose, the individuals of one batch are wrapped into a Runnable object. The batch size is automatically adapted to the available number of CPU cores. It is assumed that the evaluation cost of one fitness function is quite small. If this assumption doesn’t hold, you can configure the the maximal number of batch elements with the io.jenetics.concurrency.maxBatchSize system property15 . 1.4.1.1 Basic configuration The used Executor can be defined when building the evolution Engine object. 1 2 import j a v a . u t i l . c o n c u r r e n t . E x e c u t o r ; import j a v a . u t i l . c o n c u r r e n t . E x e c u t o r s ; 3 4 5 6 7 public c l a s s Main { private s t a t i c Double e v a l ( f i n a l Genotype g t ) { // c a l c u l a t e and r e t u r n f i t n e s s } 8 9 10 11 12 13 14 15 16 17 18 19 20 21 } public s t a t i c void main ( f i n a l S t r i n g [ ] a r g s ) { // C r e a t i n g an f i x e d s i z e E x e c u t o r S e r v i c e final ExecutorService executor = Executors . newFixedThreadPool ( 1 0 ) f i n a l Factory > g t f = . . . f i n a l Engine e n g i n e = Engine . b u i l d e r ( Main : : e v a l , g t f ) // Using 10 t h r e a d s f o r e v o l v i n g . . executor ( executor ) . build () ... } 15 See section 1.4.1.2 on the following page. 30 1.4. NUTS AND BOLTS CHAPTER 1. FUNDAMENTALS If no Executor is given, Jenetics uses a common ForkJoinPool16 for concurrency. Sometimes it might be useful to run the evaluation Engine single-threaded, or even execute all operations in the main thread. This can be easily achieved by setting the appropriate Executor. 1 2 3 4 f i n a l Engine e n g i n e = Engine . b u i l d e r ( . . . ) // Doing t h e Engine o p e r a t i o n s i n t h e main t h r e a d . e x e c u t o r ( ( E x e c u t o r ) Runnable : : run ) . build () The code snippet above shows how to do the Engine operations in the main thread. Whereas the snippet below executes the Engine operations in a single thread, other than the main thread. 1 2 3 4 f i n a l Engine e n g i n e = Engine . b u i l d e r ( . . . ) // Doing t h e Engine o p e r a t i o n s i n a s i n g l e t h r e a d . executor ( Executors . newSingleThreadExecutor ( ) ) . build () Such a configuration can be useful for performing reproducible (performance) tests, without the uncertainty of a concurrent execution environment. 1.4.1.2 Concurrency tweaks Jenetics uses different strategies for minimizing the concurrency overhead, depending on the configured Executor. For the ForkJoinPool, the fitness evaluation of the population is done by recursively dividing it into sub-populations using the abstract RecursiveAction class. If a minimal sub-population size is reached, the fitness values for this sub-population are directly evaluated. The default value of this threshold is five and can be controlled via the io.jenetics.concurrency.splitThreshold system property. Besides the splitThreshold, the size of the evaluated sub-population is dynamically determined by the ForkJoinTask.getSurplusQueuedTaskCount() method.17 If this value is greater than three, the fitness values of the current sub-population are also evaluated immediately. The default value can be overridden by the io.jenetics.concurrency.maxSurplusQueuedTaskCount system property. $ java -Dio.jenetics.concurrency.splitThreshold=1 \ -Dio.jenetics.concurrency.maxSurplusQueuedTaskCount=2 \ -cp jenetics-4.3.0.jar:app.jar \ com.foo.bar.MyJeneticsApp You may want to tweak this parameters, if you realize a low CPU utilization during the fitness value evaluation. Long running fitness functions could lead to CPU under-utilization while evaluating the last sub-population. In this case, only one core is busy, while the other cores are idle, because they already finished 16 https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ForkJoinPool. html 17 Excerpt from the Javadoc: Returns an estimate of how many more locally queued tasks are held by the current worker thread than there are other worker threads that might steal them. This value may be useful for heuristic decisions about whether to fork other tasks. In many usages of ForkJoinTasks , at steady state, each worker should aim to maintain a small constant surplus (for example, 3) of tasks, and to process computations locally if this threshold is exceeded. 31 1.4. NUTS AND BOLTS CHAPTER 1. FUNDAMENTALS the fitness evaluation. Since the workload has been already distributed, no work-stealing is possible. Reducing the splitThreshold can help to have a more equal workload distribution between the available CPUs. Reducing the maxSurplusQueuedTaskCount property will create a more uniform workload for fitness function with heavily varying computation cost for different genotype values. The fitness function shouldn’t acquire locks for achieving thread safety. It is also recommended to avoid calls to blocking methods. If such calls are unavoidable, consider using the ForkJoinPool.managedBlock method. Especially if you are using a ForkJoinPool executor, which is the default. If the Engine is using an ExecutorService, a different optimization strategy is used for reducing the concurrency overhead. The original population is divided into a fixed number18 of sub-populations, and the fitness values of each subpopulation are evaluated by one thread. For long running fitness functions, it is better to have smaller sub-populations for a better CPU utilization. With the io.jenetics.concurrency.maxBatchSize system property, it is possible to reduce the sub-population size. The default value is set to Integer.MAX_VALUE. This means, that only the number of CPU cores influences the batch size. $ java -Dio.jenetics.concurrency.maxBatchSize=3 \ -cp jenetics-4.3.0.jar:app.jar \ com.foo.bar.MyJeneticsApp Another source of under-utilized CPUs are lock contentions. It is therefore strongly recommended to avoid locking and blocking calls in your fitness function at all. If blocking calls are unavoidable, consider using the managed block functionality of the ForkJoinPool.19 1.4.2 Randomness In general, GAs heavily depends on pseudo random number generators (PRNG) for creating new individuals and for the selection- and mutation-algorithms. Jenetics uses the Java Random object, respectively sub-types from it, for generating random numbers. To make the random engine pluggable, the Random object is always fetched from the RandomRegistry. This makes it possible to change the implementation of the random engine without changing the client code. The central RandomRegistry also allows to easily change Random engine even for specific parts of the code. The following example shows how to change and restore the Random object. When opening the with scope, changes to the RandomRegistry are only visible within this scope. Once the with scope is left, the original Random object is restored. 18 The number of sub-populations actually depends on the number of available CPU cores, which are determined with Runtime.availableProcessors(). 19 A good introduction on how to use managed blocks, and the motivation behind it, is given in this talk: https://www.youtube.com/watch?v=rUDGQQ83ZtI 32 1.4. NUTS AND BOLTS 1 2 3 4 5 6 7 CHAPTER 1. FUNDAMENTALS L i s t > g e n o t y p e s = RandomRegistry . with (new Random ( 1 2 3 ) , r −> { Genotype . o f ( DoubleChromosome . o f ( 0 . 0 , 1 0 0 . 0 , 1 0 ) ) . instances () . l i m i t (100) . collect ( Collectors . toList () ) }) ; With the previous listing, a random, but reproducible, list of genotypes is created. This might be useful while testing your application or when you want to evaluate the EvolutionStream several times with the same initial population. 1 2 3 4 5 6 Engine e n g i n e = . . . ; // C r e a t e a new e v o l u t i o n stream with t h e g i v e n // i n i t i a l g e n o t y p e s . Phenotype b e s t = e n g i n e . stream ( g e n o t y p e s ) . limit (10) . c o l l e c t ( EvolutionResult . toBestPhenotype ( ) ) ; The example above uses the generated genotypes for creating the EvolutionStream. Each created stream uses the same starting population, but will, most likely, create a different result. This is because the stream evaluation is still non-deterministic. Setting the PRNG to a Random object with a defined seed has the effect, that every evolution stream will produce the same result—in an single threaded environment. The parallel nature of the GA implementation requires the creation of streams ti,j of random numbers which are statistically independent, where the streams are numbered with j = 1, 2, 3, ..., p, p denotes the number of processes. We expect statistical independence between the streams as well. The used PRNG should enable the GA to play fair, which means that the outcome of the GA is strictly independent from the underlying hardware and the number of parallel processes or threads. This is essential for reproducing results in parallel environments where the number of parallel tasks may vary from run to run. The Fair Play property of a PRNG guarantees that the quality of the genetic algorithm (evolution stream) does not depend on the degree of parallelization. When the Random engine is used in an multi-threaded environment, there must be a way to parallelize the sequential PRNG. Usually this is done by taking the elements of the sequence of pseudo-random numbers and distribute them among the threads. There are essentially four different parallelizations techniques used in practice: Random seeding, Parameterization, Block splitting and Leapfrogging. Random seeding Every thread uses the same kind of PRNG but with a different seed. This is the default strategy used by the Jenetics library. The 33 1.4. NUTS AND BOLTS CHAPTER 1. FUNDAMENTALS RandomRegistry is initialized with the ThreadLocalRandom class from the java.util.concurrent package. Random seeding works well for the most problems but without theoretical foundation.20 If you assume that this strategy is responsible for some non-reproducible results, consider using the LCG64ShiftRandom PRNG instead, which uses block splitting as parallelization strategy. Parameterization All threads uses the same kind of PRNG but with different parameters. This requires the PRNG to be parameterizable, which is not the case for the Random object of the JDK. You can use the LCG64ShiftRandom class if you want to use this strategy. The theoretical foundation for these method is weak. In a massive parallel environment you will need a reliable set of parameters for every random stream, which are not trivial to find. Block splitting With this method each thread will be assigned a non-overlapping contiguous block of random numbers, which should be enough for the whole runtime of the process. If the number of threads is not known in advance, the length of each block should be chosen much larger then the maximal expected number of threads. This strategy is used when using the LCG64ShiftRandom.ThreadLocal class. This class assigns every thread a block of 256 ≈ 7, 2 · 1016 random numbers. After 128 threads, the blocks are recycled, but with changed seed. Figure 1.4.2: Block splitting Leapfrog With the leapfrog method each thread t ∈ [0, P ) only consumes the P th random number and jump ahead in the random sequence by the number of threads, P . This method requires the ability to jump very quickly ahead in the sequence of random numbers by a given amount. Figure 1.4.3 on the next page graphically shows the concept of the leapfrog method. LCG64ShiftRandom21 The LCG64ShiftRandom class is a port of the trng::lcg64_shift PRNG class of the TRNG22 library, implemented in C++.[4] It implements additional methods, which allows to implement the block splitting—and also the leapfrog—method. 20 This is also expressed by Donald Knuth’s advice: »Random number generators should not be chosen at random.« 21 The LCG64ShiftRandom PRNG is part of the io.jenetics.prngine module (see section 4.4 on page 99). 22 http://numbercrunch.de/trng/ 34 1.4. NUTS AND BOLTS CHAPTER 1. FUNDAMENTALS Figure 1.4.3: Leapfrogging 1 2 3 4 5 6 public c l a s s LCG64ShiftRandom extends Random { public void s p l i t ( f i n a l i n t p , f i n a l i n t s ) ; public void jump ( f i n a l long s t e p ) ; public void jump2 ( f i n a l i n t s ) ; ... } Listing 1.13: LCG64ShiftRandom class Listing 1.13 shows the interface used for implementing the block splitting and leapfrog parallelizations technique. This methods have the following meaning: split Changes the internal state of the PRNG in a way that future calls to nextLong() will generated the sth sub-stream of pth sub-streams. s must be within the range of [0, p − 1). This method is used for parallelization via leapfrogging. jump Changes the internal state of the PRNG in such a way that the engine jumps s steps ahead. This method is used for parallelization via block splitting. jump2 Changes the internal state of the PRNG in such a way that the engine jumps 2s steps ahead. This method is used for parallelization via block splitting. 1.4.3 Serialization Jenetics supports serialization for a number of classes, most of them are located in the io.jenetics package. Only the concrete implementations of the Gene and the Chromosome interfaces implements the Serializable interface. This gives a greater flexibility when implementing own Genes and Chromosomes. • BitGene • IntegerChromosome • BitChromosome • LongGene • CharacterGene • LongChomosome • CharacterChromosome • DoubleGene • IntegerGene • DoubleChromosome 35 1.4. NUTS AND BOLTS CHAPTER 1. FUNDAMENTALS • EnumGene • Genotype • PermutationChromosome • Phenotype With the serialization mechanism you can write a population to disk and load it into an new EvolutionStream at a later time. It can also be used to transfer populations to evolution engines, running on different hosts, over a network link. The IO class, located in the io.jenetics.util package, supports native Java serialization. 1 2 3 4 // C r e a t i n g r e s u l t p o p u l a t i o n . E v o l u t i o n R e s u l t r e s u l t = stream . l i m i t (100) . c o l l e c t ( toBestEvolutionResult () ) ; 5 6 7 8 // W r i t i n g t h e p o p u l a t i o n t o d i s k . f i n a l F i l e f i l e = new F i l e ( " p o p u l a t i o n . o b j " ) ; IO . o b j e c t . w r i t e ( r e s u l t . g e t P o p u l a t i o n ( ) , f i l e ) ; 9 10 11 12 13 14 15 // Reading t h e p o p u l a t i o n from d i s k . ISeq > p o p u l a t i o n = ( ISeq >)IO . o b j e c t . r e a d ( f i l e ) ; E v o l u t i o n S t r e a m stream = Engine . build ( ff , gtf ) . stream ( p o p u l a t i o n , 1 ) ; 1.4.4 Utility classes The io.jenetics.util and the io.jenetics.stat package of the library contains utility and helper classes which are essential for the implementation of the GA. io.jenetics.util.Seq Most notable are the Seq interfaces and its implementation. They are used, among others, in the Chromosome and Genotype classes and holds the Genes and Chromosomes, respectively. The Seq interface itself represents a fixed-sized, ordered sequence of elements. It is an abstraction over the Java build-in array-type, but much safer to use for generic elements, because there are no casts needed when using nested generic types. Figure 1.4.4 on the next page shows the Seq class diagram with their most important methods. The interfaces MSeq and ISeq are mutable, respectively immutable specializations of the basis interface. Creating instances of the Seq interfaces is possible via the static factory methods of the interfaces. 1 2 3 4 5 // C r e a t e " d i f f e r e n t " s e q u e n c e s . f i n a l Seq a1 = Seq . o f ( 1 , 2 , 3 ) ; f i n a l MSeq a2 = MSeq . o f ( 1 , 2 , 3 ) ; f i n a l ISeq a3 = MSeq . o f ( 1 , 2 , 3 ) . t o I S e q ( ) ; f i n a l MSeq a4 = a3 . copy ( ) ; 6 7 8 9 10 // The ’ e q u a l s ’ method p e r f o r m s element−w i s e c o m p a r i s o n . a s s e r t ( a1 . e q u a l s ( a2 ) && a1 != a2 ) ; a s s e r t ( a2 . e q u a l s ( a3 ) && a2 != a3 ) ; a s s e r t ( a3 . e q u a l s ( a4 ) && a3 != a4 ) ; How to create instances of the three Seq types is shown in the listing above. The Seq classes also allows a more functional programming style. For a full method description refer to the Javadoc. 36 1.4. NUTS AND BOLTS CHAPTER 1. FUNDAMENTALS Figure 1.4.4: Seq class diagram io.jenetics.stat This package contains classes for calculating statistical moments. They are designed to work smoothly with the Java Stream API and are divided into mutable (number) consumers and immutable value classes, which holds the statistical moments. The additional classes calculate the • minimum, • variance, • maximum, • skewness and • sum, • mean, Numeric type int long double • kurtosis value. Consumer class IntMomentStatistics LongMomentStatistics DoubleMomentStatistics Value class IntMoments LongMoments DoubleMoments Table 1.4.1: Statistics classes Table 1.4.1 contains the available statistical moments for the different numeric types. The following code snippet shows an example on how to collect double statistics from an given DoubleGene stream. 1 2 3 4 5 // C o l l e c t i n g i n t o an s t a t i s t i c s o b j e c t . DoubleChromosome chromosome = . . . D o u b l e M o m e n t S t a t i s t i c s s t a t i s t i c s = chromosome . stream ( ) . c o l l e c t ( DoubleMomentStatistics . t o D o u b l e M o m e n t S t a t i s t i c s ( v −> v . d o u b l e V a l u e ( ) ) ) ; 6 7 8 9 // C o l l e c t i n g i n t o an moments o b j e c t . DoubleMoments moments = chromosome . stream ( ) . c o l l e c t ( DoubleMoments . toDoubleMoments ( v −> v . d o u b l e V a l u e ( ) ) ) ; The stat package also contains a class for calculating the quantile23 of a stream of double values. It’s implementing algorithm, which is described in [15], calculates—or estimates—the quantile value on the fly, without storing the consumed 23 https://en.wikipedia.org/wiki/Quantile 37 1.4. NUTS AND BOLTS CHAPTER 1. FUNDAMENTALS double values. This allows to use the Quantile class even for very large number of double values. How to calculate the first quartile of a given, random DoubleStream is shown in the code snipped below. 1 2 3 f i n a l Q u a n t i l e q u a r t i l e = new Q u a n t i l e ( 0 . 2 5 ) ; new Random ( ) . d o u b l e s ( 1 0 _000 ) . f o r E a c h ( q u a r t i l e ) ; f i n a l double v a l u e = q u a r t i l e . g e t V a l u e ( ) ; Be aware, that the calculated quartile is just an estimation. For a sufficient accuracy, the stream size should be sufficiently large (size 100). 38 Chapter 2 Advanced topics This section describes some advanced topics for setting up an evolution Engine or EvolutionStream. It contains some problem encoding examples and how to override the default validation strategy of the given Genotypes. The last section contains a detailed description of the implemented termination strategies. 2.1 Extending Jenetics The Jenetics library was designed to give you a great flexibility in transforming your problem into a structure that can be solved by an GA. It also comes with different implementations for the base data-types (genes and chromosomes) and operators (alterers and selectors). If it is still some functionality missing, this section describes how you can extend the existing classes. Most of the extensible classes are defined by an interface and have an abstract implementation which makes it easier to extend it. 2.1.1 Genes Genes are the starting point in the class hierarchy. They hold the actual information, the alleles, of the problem domain. Beside the classical bit-gene, Jenetics comes with gene implementations for numbers (double-, int- and long values), characters and enumeration types. For implementing your own gene type you have to implement the Gene interface with three methods: (1) the getAllele() method which will return the wrapped data, (2) the newInstance method for creating new, random instances of the gene—must be of the same type and have the same constraint—and (3) the isValid() method which checks if the gene fulfill the expected constraints. The gene constraint might be violated after mutation and/or recombination. If you want to implement a new number-gene, e. g. a gene which holds complex values, you may want extend it from the abstract NumericGene class. Every Gene extends the Serializable interface. For normal genes there is no more work to do for using the Java serialization mechanism. 39 2.1. EXTENDING JENETICS The custom Genes and Random engine available when implementing their to seamlessly change the .setRandom method. CHAPTER 2. ADVANCED TOPICS Chromosomes implementations must use the via the RandomRegistry.getRandom method factory methods. Otherwise it is not possible Random engine by using the RandomRegistry- If you want to support your own allele type, but want to avoid the effort of implementing the Gene interface, you can alternatively use the AnyGene class. It can be created with AnyGene.of(Supplier, Predicate). The given Supplier is responsible for creating new random alleles, similar to the newInstance method in the Gene interface. Additional validity checks are performed by the given Predicate. 1 2 3 4 5 6 7 8 c l a s s LastMonday { // C r e a t e s new random ’ L o c a l D a t e ’ o b j e c t s . private s t a t i c L o c a l D a t e nextMonday ( ) { f i n a l Random random = RandomRegistry . getRandom ( ) ; LocalDate . of (2015 , 1 , 5) . plusWeeks ( random . n e x t I n t ( 1 0 0 0 ) ) ; } 9 // Do some a d d i t i o n a l v a l i d i t y c h e c k . private s t a t i c boolean i s V a l i d ( f i n a l L o c a l D a t e d a t e ) { . . . } 10 11 12 13 14 15 16 17 } // C r e a t e a new gene from t h e random ’ S u p p l i e r ’ and // v a l i d a t i o n ’ P r e d i c a t e ’ . private f i n a l AnyGene gene = AnyGene . o f ( LastMonday : : nextMonday , LastMonday : : i s V a l i d ) ; Listing 2.1: AnyGene example Example listing 2.1 shows the (almost) minimal setup for creating user defined Gene allele types. By convention, the Random engine, used for creating the new LocalDate objects, must be requested from the RandomRegistry. With the optional validation function, isValid, it is possible to reject Genes whose alleles doesn’t conform some criteria. The simple usage of the AnyGene has also its downsides. Since the AnyGene instances are created from function objects, serialization is not supported by the AnyGene class. It is also not possible to use some Alterer implementations with the AnyGene, like: • GaussianMutator, • MeanAlterer and • PartiallyMatchedCrossover 2.1.2 Chromosomes A new gene type normally needs a corresponding chromosome implementation. The most important part of a chromosome is the factory method newInstance, which lets the evolution Engine create a new Chromosome instance from a 40 2.1. EXTENDING JENETICS CHAPTER 2. ADVANCED TOPICS sequence of Genes. This method is used by the Alterers when creating new, combined Chromosomes. It is allowed, that the newly created chromosome has a different length than the original one. The other methods should be selfexplanatory. The chromosome has the same serialization mechanism as the gene. For the minimal case it can extends the Serializable interface.1 Just implementing the Serializable interface is sometimes not enough. You might also need to implement the readObject and writeObject methods for a more concise serialization result. Consider using the serialization proxy pattern, item 90, described in Effective Java [6]. Corresponding to the AnyGene, it is possible to create chromosomes with arbitrary allele types with the AnyChromosome. 1 2 3 4 5 6 7 public c l a s s LastMonday { // The used problem Codec . private s t a t i c f i n a l Codec > CODEC = Codec . o f ( Genotype . o f ( AnyChromosome . o f ( LastMonday : : nextMonday ) ) , g t −> g t . getGene ( ) . g e t A l l e l e ( ) ); 8 // C r e a t e s new random ’ L o c a l D a t e ’ o b j e c t s . private s t a t i c L o c a l D a t e nextMonday ( ) { f i n a l Random random = RandomRegistry . getRandom ( ) ; LocalDate . of (2015 , 1 , 5) . plusWeeks ( random . n e x t I n t ( 1 0 0 0 ) ) ; } 9 10 11 12 13 14 15 16 // The f i t n e s s f u n c t i o n : f i n d a monday a t t h e end o f t h e month . private s t a t i c i n t f i t n e s s ( f i n a l L o c a l D a t e d a t e ) { return d a t e . getDayOfMonth ( ) ; } 17 18 19 20 21 public s t a t i c void main ( f i n a l S t r i n g [ ] a r g s ) { f i n a l Engine , I n t e g e r > e n g i n e = Engine . b u i l d e r ( LastMonday : : f i t n e s s , CODEC) . o f f s p r i n g S e l e c t o r (new R o u l e t t e W h e e l S e l e c t o r <>() ) . build () ; 22 23 24 25 26 27 f i n a l Phenotype , I n t e g e r > b e s t = e n g i n e . stream ( ) . limit (50) . c o l l e c t ( EvolutionResult . toBestPhenotype ( ) ) ; 28 29 30 31 32 33 34 35 } } System . out . p r i n t l n ( b e s t ) ; Listing 2.2: AnyChromosome example Listing 2.2 shows a full usage example of the AnyGene and AnyChromosome class. The example tries to find a Monday with a maximal day of month. An interesting detail is, that an Codec2 definition is used for creating new Genotypes and 1 http://www.oracle.com/technetwork/articles/java/javaserial-1536170.html 2 See section 2.3 on page 51 for a more detailed Codec description. 41 2.1. EXTENDING JENETICS CHAPTER 2. ADVANCED TOPICS for converting them back to LocalDate alleles. The convenient usage of the AnyChromosome has to be payed by the same restriction as for the AnyGene: no serialization support for the chromosome and not usable for all Alterer implementations. 2.1.3 Selectors If you want to implement your own selection strategy you only have to implement the Selector interface with the select method. 1 2 3 4 5 6 7 8 9 10 11 @FunctionalInterface public i n t e r f a c e S e l e c t o r < G extends Gene , G>, C extends Comparable super C> > { public ISeq > s e l e c t ( Seq > p o p u l a t i o n , i n t count , Optimize opt ); } Listing 2.3: Selector interface The first parameter is the original population from which the sub-population is selected. The second parameter, count, is the number of individuals of the returned sub-population. Depending on the selection algorithm, it is possible that the sub-population contains more elements than the original one. The last parameter, opt, determines the optimization strategy which must be used by the selector. This is exactly the point where it is decided whether the GA minimizes or maximizes the fitness function. Before implementing a selector from scratch, consider to extend your selector from the ProbabilitySelector (or any other available Selector implementation). It is worth the effort to try to express your selection strategy in terms of selection property P (i). Another way for re-using existing Selector implementation is by composition. 1 2 3 4 5 6 7 8 public c l a s s E l i t e S e l e c t o r < G extends Gene , G>, C extends Comparable super C> > implements S e l e c t o r { private f i n a l T r u n c a t i o n S e l e c t o r _ e l i t e = new T r u n c a t i o n S e l e c t o r <>() ; 9 10 11 private f i n a l T o u r n a m e n t S e l e c t o r _ r e s t = new T o u r n a m e n t S e l e c t o r <>(3) ; 12 13 14 public E l i t e S e l e c t o r ( ) { } 15 16 17 18 19 20 21 22 @Override public ISeq > s e l e c t ( f i n a l Seq > p o p u l a t i o n , f i n a l i n t count , f i n a l Optimize opt ) { ISeq > r e s u l t ; 42 2.1. EXTENDING JENETICS 23 24 25 26 27 28 29 30 31 32 33 34 } } CHAPTER 2. ADVANCED TOPICS i f ( p o p u l a t i o n . isEmpty ( ) | | count <= 0 ) { r e s u l t = I S e q . empty ( ) ; } else { f i n a l i n t e c = min ( count , _ e l i t e C o u n t ) ; r e s u l t = _ e l i t e . s e l e c t ( p o p u l a t i o n , ec , opt ) ; r e s u l t = r e s u l t . append ( _ r e s t . s e l e c t ( p o p u l a t i o n , max ( 0 , count − e c ) , opt ) ); } return r e s u l t ; Listing 2.4: Elite selector Listing 2.4 on the preceding page shows how an elite selector could be implemented by using the existing Truncation- and TournamentSelector. With elite selection, the quality of the best solution in each generation monotonically increases over time.[3] Although this is not necessary, since the evolution Engine/Stream doesn’t throw away the best solution found during the evolution process. 2.1.4 Alterers For implementing a new alterer class it is necessary to implement the Alterer interface. You might do this if your new Gene type needs a special kind of alterer not available in the Jenetics project. 1 2 3 4 5 6 7 8 9 10 @FunctionalInterface public i n t e r f a c e A l t e r e r < G extends Gene , G>, C extends Comparable super C> > { public A l t e r e r R e s u l t a l t e r ( Seq > p o p u l a t i o n , long g e n e r a t i o n ); } Listing 2.5: Alterer interface The first parameter of the alter method is the population which has to be altered. The second parameter is the generation of the newly created individuals and the return value is the number of genes that has been altered. To maximize the range of application of an Alterer, it is recommended that they can handle Genotypes and Chromosomes with variable length. 2.1.5 Statistics During the developing phase of an application which uses the Jenetics library, additional statistical data about the evolution process is crucial. Such data can help to optimize the parametrization of the evolution Engine. A good starting point is to use the EvolutionStatistics class in the io.jenetics.engine package (see listing 1.10 on page 27). If the data in the EvolutionStatistics 43 2.1. EXTENDING JENETICS CHAPTER 2. ADVANCED TOPICS class doesn’t fit your needs, you simply have to write your own statistics class. It is not possible to derive from the existing EvolutionStatistics class. This is not a real restriction, since you still can use the class by delegation. Just implement the Java Consumer > interface. 2.1.6 Engine The evolution Engine itself can’t be extended, but it is still possible to create an EvolutionStream without using the Engine class.3 Because the EvolutionStream has no direct dependency to the Engine, it is possible to use an different, special evolution Function. 1 2 3 4 public f i n a l c l a s s S p e c i a l E n g i n e { // The Genotype f a c t o r y . private s t a t i c f i n a l Factory > GTF = Genotype . o f ( DoubleChromosome . o f ( 0 , 1 ) ) ; 5 // The f i t n e s s f u n c t i o n . private s t a t i c Double f i t n e s s ( f i n a l Genotype g t ) { return g t . getGene ( ) . g e t A l l e l e ( ) ; } 6 7 8 9 10 // C r e a t e new e v o l u t i o n s t a r t o b j e c t . private s t a t i c E v o l u t i o n S t a r t s t a r t ( f i n a l i n t p o p u l a t i o n S i z e , f i n a l long g e n e r a t i o n ) { f i n a l ISeq > p o p u l a t i o n = GTF . instances () . map( g t −> Phenotype . o f ( gt , g e n e r a t i o n , S p e c i a l E n g i n e : : f i t n e s s ) ) . limit ( populationSize ) . c o l l e c t ( ISeq . toISeq ( ) ) ; 11 12 13 14 15 16 17 18 19 20 21 } 22 return E v o l u t i o n S t a r t . o f ( p o p u l a t i o n , g e n e r a t i o n ) ; 23 // The s p e c i a l e v o l u t i o n f u n c t i o n . private s t a t i c E v o l u t i o n R e s u l t e v o l v e ( f i n a l E v o l u t i o n S t a r t s t a r t ) { return . . . ; // Add i m p l e m e n t a t i o n ! } 24 25 26 27 28 29 public s t a t i c void main ( f i n a l S t r i n g [ ] a r g s ) { f i n a l Genotype b e s t = E v o l u t i o n S t r e a m . o f ( ( ) −> s t a r t ( 5 0 , 0 ) , S p e c i a l E n g i n e : : e v o l v e ) . l i m i t ( Limits . bySteadyFitness (10) ) . l i m i t (100) . c o l l e c t ( E v o l u t i o n R e s u l t . toBestGenotype ( ) ) ; 30 31 32 33 34 35 36 37 38 39 } } System . out . p r i n t l n ( " Best Genotype : " + b e s t ) ) ; Listing 2.6: Special evolution engine Listing 2.6 shows a complete implementation stub for using an own special evolution Function. 3 Also refer to section 1.3.3.3 on page 25 on how to create an EvolutionStream from an evolution Function. 44 2.2. ENCODING 2.2 CHAPTER 2. ADVANCED TOPICS Encoding This section presents some encoding examples for common problems. The encoding should be a complete and minimal expression of a solution to the problem. An encoding is complete if it contains enough information to represent every solution to the problem. An minimal encoding contains only the information needed to represent a solution to the problem. If an encoding contains more information than is needed to uniquely identify solutions to the problem, the search space will be larger than necessary. Whenever possible, the encoding should not be able to represent infeasible solutions. If a genotype can represent an infeasible solution, care must be taken in the fitness function to give partial credit to the genotype for its »good« genetic material while sufficiently penalizing it for being infeasible. Implementing a specialized Chromosome, which won’t create invalid encodings can be a solution to this problem. In general, it is much more desirable to design a representation that can only represent valid solutions so that the fitness function measures only fitness, not validity. An encoding that includes invalid individuals enlarges the search space and makes the search more costly. A deeper analysis of how to create encodings can be found in [27] and [26]. Some of the encodings represented in the following sections has been implemented by Jenetics, using the Codec4 interface, and are available through static factory methods of the io.jenetics.engine.Codecs class. 2.2.1 Real function Jenetics contains three different numeric gene and chromosome implementations, which can be used to encode a real function, f : R → R: • IntegerGene/Chromosome, • LongGene/Chromosome and • DoubleGene/Chromosome. It is quite easy to encode a real function. Only the minimum and maximum value of the function domain must be defined. The DoubleChromosome of length 1 is then wrapped into a Genotype. 1 2 3 Genotype . o f ( DoubleChromosome . o f ( min , max , 1 ) ); Decoding the double value from the Genotype is also straight forward. Just get the first gene from the first chromosome, with the getGene() method, and convert it to a double. 1 2 3 s t a t i c double toDouble ( f i n a l Genotype g t ) { return g t . getGene ( ) . d o u b l e V a l u e ( ) ; } When the Genotype only contains scalar chromosomes5 , it should be clear, that it can’t be altered by every Alterer. That means, that none of the Crossover 4 See section 2.3 on page 51. chromosomes contains only one gene. 5 Scalar 45 2.2. ENCODING CHAPTER 2. ADVANCED TOPICS alterers will be able to create modified Genotypes. For scalars the appropriate alterers would be the MeanAlterer, GaussianAlterer and Mutator. Scalar Chromosomes and/or Genotypes can only be altered by MeanAlterer, GaussianAlterer and Mutator classes. Other alterers are allowed, but will have no effect on the Chromosomes. 2.2.2 Scalar function Optimizing a function f (x1 , ..., xn ) of one or more variable whose range is one-dimensional, we have two possibilities for the Genotype encoding.[31] For the first encoding we expect that all variables, xi , have the same minimum and maximum value. In this case we can simply create a Genotype with a Numeric Chromosome of the desired length n. 1 2 3 Genotype . o f ( DoubleChromosome . o f ( min , max , n ) ); The decoding of the Genotype requires a cast of the first Chromosome to a DoubleChromosome. With a call to the DoubleChromosome.toArray() method we return the variables (x1 , ..., xn ) as double[] array. 1 2 3 s t a t i c double [ ] t o S c a l a r s ( f i n a l Genotype g t ) { return g t . getChromosome ( ) . a s ( DoubleChromosome . c l a s s ) . t o A r r a y ( ) ; } With the first encoding you have the possibility to use all available alterers, including all Crossover alterer classes. The second encoding must be used if the minimum and maximum value of the variables xi can’t be the same for all i. For the different domains, each variable xi is represented by a Numeric Chromosome with length one. The final Genotype will consist of n Chromosomes with length one. 1 2 3 4 5 6 Genotype . o f ( DoubleChromosome . o f ( min1 , max1 , 1 ) , DoubleChromosome . o f ( min2 , max2 , 1 ) , ... DoubleChromosome . o f ( minn , maxn , 1 ) ); With the help of the new Java Stream API, the decoding of the Genotype can be done in a view lines. The DoubleChromosome stream, which is created from the chromosome Seq, is first mapped to double values and then collected into an array. 1 2 3 4 5 s t a t i c double [ ] t o S c a l a r s ( f i n a l Genotype g t ) { return g t . stream ( ) . mapToDouble ( c −> c . getGene ( ) . d o u b l e V a l u e ( ) ) . toArray ( ) ; } As already mentioned, with the use of scalar chromosomes we can only use the MeanAlterer, GaussianAlterer or Mutator alterer class. If there are performance issues in converting the Genotype into a double[] array, or any other numeric array, you can access the Genes directly via the 46 2.2. ENCODING CHAPTER 2. ADVANCED TOPICS Genotype.get(i, j) method and than convert it to the desired numeric value, by calling intValue(), longValue() or doubleValue(). 2.2.3 Vector function A function f (X1 , ..., Xn ), of one to n variables whose range is m-dimensional, is encoded by m DoubleChromosomes of length n.[32] The domain–minimum and maximum values–of one variable Xi are the same in this encoding. 1 2 3 4 5 6 Genotype . o f ( DoubleChromosome . o f ( min1 , max1 , m) , DoubleChromosome . o f ( min2 , max2 , m) , ... DoubleChromosome . o f ( minn , maxn , m) ); The decoding of the vectors is quite easy with the help of the Java Stream API. In the first map we have to cast the Chromosome object to the actual DoubleChromosome. The second map then converts each DoubleChromosome to a double[] array, which is collected to an 2-dimensional double[n ][m ] array afterwards. 1 2 3 4 5 s t a t i c double [ ] [ ] t o V e c t o r s ( f i n a l Genotype g t ) { return g t . stream ( ) . map( dc −> dc . a s ( DoubleChromosome . c l a s s ) . t o A r r a y ( ) ) . t o A r r a y ( double [ ] [ ] : : new) ; } For the special case of n = 1, the decoding of the Genotype can be simplified to the decoding we introduced for scalar functions in section 2.2.2. 1 2 3 s t a t i c double [ ] t o V e c t o r ( f i n a l Genotype g t ) { return g t . getChromosome ( ) . a s ( DoubleChromosome . c l a s s ) . t o A r r a y ( ) ; } 2.2.4 Affine transformation An affine transformation6 7 is usually performed by a matrix multiplication with a transformation matrix—in a homogeneous coordinates system8 . For a transformation in R2 , we can define the matrix A9 : a11 a12 a13 (2.2.1) A = a21 a22 a23 . 0 0 1 A simple representation can be done by creating a Genotype which contains two DoubleChromosomes with a length of 3. 1 2 3 4 Genotype . o f ( DoubleChromosome . o f ( min , max , 3 ) , DoubleChromosome . o f ( min , max , 3 ) ); 6 https://en.wikipedia.org/wiki/Affine_transformation 7 http://mathworld.wolfram.com/AffineTransformation.html 8 https://en.wikipedia.org/wiki/Homogeneous_coordinates 9 https://en.wikipedia.org/wiki/Transformation_matrix 47 2.2. ENCODING CHAPTER 2. ADVANCED TOPICS The drawback with this kind of encoding is, that we will create a lot of invalid (non-affine transformation matrices) during the evolution process, which must be detected and discarded. It is also difficult to find the right parameters for the min and max values of the DoubleChromosomes. A better approach will be to encode the transformation parameters instead of the transformation matrix. The affine transformation can be expressed by the following parameters: • sx – the scale factor in x direction • sy – the scale factor in y direction • tx – the offset in x direction • ty – the offset in y direction • θ – the rotation angle clockwise around origin • kx – shearing parallel to x axis • ky – shearing parallel to y axis This parameters can then be represented by the following Genotype. 1 2 3 4 5 6 7 8 9 10 11 12 13 Genotype . o f ( // S c a l e DoubleChromosome . DoubleChromosome . // T r a n s l a t i o n DoubleChromosome . DoubleChromosome . // R o t a t i o n DoubleChromosome . // Shear DoubleChromosome . DoubleChromosome . ) o f ( sxMin , sxMax ) , o f ( syMin , syMax ) , o f ( txMin , txMax ) , o f ( tyMin , tyMax ) , o f ( thMin , thMax ) , o f ( kxMin , kxMax ) , o f ( kyMin , kxMax ) This encoding ensures that no invalid Genotype will be created during the evolution process, since the crossover will be only performed on the same kind of chromosome (same chromosome index). To convert the Genotype back to the transformation matrix A, the following equations can be used [14]: a11 This corresponds to 1 0 tx 0 1 ty · 0 0 1 = sx cos θ + kx sy sin θ a12 = sy kx cos θ − sx sin θ a13 = tx a21 = ky sx cos θ + sy sin θ a22 = sy cos θ − sx ky sin θ a23 = ty an transformation order of 1 kx 0 sx 0 ky 1 0 · 0 sy 0 0 1 0 0 T · Sh · Sc · R: 0 cos θ − sin θ 0 · sin θ cos θ 1 0 0 (2.2.2) 0 0 . 1 In Java code, the conversion from the Genotype to the transformation matrix, will look like this: 48 2.2. ENCODING 1 2 3 4 5 6 7 8 s t a t i c double [ ] [ ] f i n a l double f i n a l double f i n a l double f i n a l double f i n a l double f i n a l double f i n a l double CHAPTER 2. ADVANCED TOPICS t o M a t r i x ( f i n a l Genotype g t ) { sx = g t . g e t ( 0 , 0 ) . d o u b l e V a l u e ( ) ; sy = g t . g e t ( 1 , 0 ) . d o u b l e V a l u e ( ) ; tx = gt . get ( 2 , 0) . doubleValue ( ) ; ty = gt . get ( 3 , 0) . doubleValue ( ) ; th = g t . g e t ( 4 , 0 ) . d o u b l e V a l u e ( ) ; kx = g t . g e t ( 5 , 0 ) . d o u b l e V a l u e ( ) ; ky = g t . g e t ( 6 , 0 ) . d o u b l e V a l u e ( ) ; 9 final final final final final final 10 11 12 13 14 15 double double double double double double cos_th = c o s ( th ) ; s i n _ t h = s i n ( th ) ; a11 = cos_th ∗ sx + kx ∗ sy ∗ s i n _ t h ; a12 = cos_th ∗ kx ∗ sy − sx ∗ s i n _ t h ; a21 = cos_th ∗ ky ∗ sx + sy ∗ s i n _ t h ; a22 = cos_th ∗ sy − ky ∗ sx ∗ s i n _ t h ; 16 17 18 19 20 21 22 } return new double [ ] [ ] { { a11 , a12 , t x } , { a21 , a22 , t y } , {0.0 , 0.0 , 1.0} }; For the introduced encoding all kind of alterers can be used. Since we have one scalar DoubleChromosome, the rotation angle θ, it is recommended also to add a MeanAlterer or GaussianAlterer to the list of alterers. 2.2.5 Graph A graph can be represented in many different ways. The most known graph representation is the adjacency matrix. The following encoding examples uses adjacency matrices with different characteristics. Undirected graph In an undirected graph the edges between the vertices have no direction. If there is a path between nodes i and j, it is assumed that there is also path from j to i. Figure 2.2.1: Undirected graph and adjacency matrix Figure 2.2.1 shows an undirected graph and its corresponding matrix representation. Since the edges between the nodes have no direction, the values of the lower diagonal matrix are not taken into account. An application which 49 2.2. ENCODING CHAPTER 2. ADVANCED TOPICS optimizes an undirected graph has to ignore this part of the matrix.10 1 2 f i n a l int n = 6 ; f i n a l Genotype g t = Genotype . o f ( BitChromosome . o f ( n ) , n ) ; The code snippet above shows how to create an adjacency matrix for a graph with n = 6 nodes. It creates a genotype which consists of n BitChromosomes of length n each. Whether the node i is connected to node j can be easily checked by calling gt.get(i-1, j-1).booleanValue(). For extracting the whole matrix as int[] array, the following code can be used. 1 2 3 4 5 f i n a l i n t [ ] [ ] a r r a y = g t . t o S e q ( ) . stream ( ) . map( c −> c . t o S e q ( ) . stream ( ) . mapToInt ( BitGene : : o r d i n a l ) . toArray ( ) ) . t o A r r a y ( i n t [ ] [ ] : : new) ; Directed graph A directed graph (digraph) is a graph where the path between the nodes have a direction associated with them. The encoding of a directed graph looks exactly like the encoding of an undirected graph. This time the whole matrix is used and the second diagonal matrix is no longer ignored. Figure 2.2.2: Directed graph and adjacency matrix Figure 2.2.2 shows the adjacency matrix of a digraph. This time the whole matrix is used for representing the graph. Weighted directed graph A weighted graph associates a weight (label) with every path in the graph. Weights are usually real numbers. They may be restricted to rational numbers or integers. The following code snippet shows how the Genotype of the matrix is created. 1 2 3 4 5 f i n a l int n = 6 ; f i n a l double min = −1; f i n a l double max = 2 0 ; f i n a l Genotype g t = Genotype . o f ( DoubleChromosome . o f ( min , max , n ) , n ) ; 10 This property violates the minimal encoding requirement we mentioned at the beginning of section 2.2 on page 45. For simplicity reason this will be ignored for the undirected graph encoding. 50 2.3. CODEC CHAPTER 2. ADVANCED TOPICS Figure 2.2.3: Weighted graph and adjacency matrix For accessing the single matrix elements, you can simply call Genotype.get(i, j).doubleValue(). If the interaction with another library requires a double[][] array, the following code can be used. 1 2 3 f i n a l double [ ] [ ] a r r a y = g t . stream ( ) . map( dc −> dc . a s ( DoubleChromosome . c l a s s ) . t o A r r a y ( ) ) . t o A r r a y ( double [ ] [ ] : : new) ; 2.3 Codec The Codec interface—located in the io.jenetics.engine package—narrows the gap between the fitness Function, which should be maximized/minimized, and the Genotype representation, which can be understand by the evolution Engine. With the Codec interface it is possible to implement the encodings of section 2.2 on page 45 in a more formalized way. Normally, the Engine expects a fitness function which takes a Genotype as input. This Genotype has then to be transformed into an object of the problem domain. The usage Codec interface allows a tighter coupling of the Genotype definition and the transformation code.11 1 2 3 4 5 public i n t e r f a c e Codec > { public Factory > e n c o d i n g ( ) ; public Function , T> d e c o d e r ( ) ; public default T d ec od e ( f i n a l Genotype g t ) { . . . } } Listing 2.7: Codec interface Listing 2.7 shows the Codec interface. The encoding() method returns the Genotype factory, which is used by the Engine for creating new Genotypes. The decoder Function, which is returned by the decoder() method, transforms the Genotype to the argument type of the fitness Function. Without the Codec interface, the implementation of the fitness Function is polluted with code, which transforms the Genotype into the argument type of the actual fitness Function. 1 2 s t a t i c double e v a l ( f i n a l Genotype g t ) { f i n a l double x = g t . getGene ( ) . d o u b l e V a l u e ( ) ; 11 Section 2.2 on page 45 describes some possible encodings for common optimization problems. 51 2.3. CODEC 3 4 5 } CHAPTER 2. ADVANCED TOPICS // Do some c a l c u l a t i o n with ’ x ’ . return . . . The Codec for the example above is quite simple and is shown below. It is not necessary to implement the Codec interface, instead you can use the Codec.of factory method for creating new Codec instances. 1 2 3 4 5 f i n a l DoubleRange domain = DoubleRange . o f ( 0 , 2∗ PI ) ; f i n a l Codec c o d e c = Codec . o f ( Genotype . o f ( DoubleChromosome . o f ( domain ) ) , g t −> g t . getChromosome ( ) . getGene ( ) . g e t A l l e l e ( ) ); When using a Codec instance, the fitness Function solely contains code from your actual problem domain—no dependencies to classes of the Jenetics library. 1 2 3 4 s t a t i c double e v a l ( f i n a l double x ) { // Do some c a l c u l a t i o n with ’ x ’ . return . . . } Jenetics comes with a set of standard encodings, which are created via static factory methods of the io.jenetics.engine.Codecs class. The following subsections shows some of the implementation of this methods. 2.3.1 Scalar codec Listing 2.8 shows the implementation of the Codecs.ofScalar factory method—for Integer scalars. 1 2 3 4 5 6 s t a t i c Codec o f S c a l a r ( IntRange domain ) { return Codec . o f ( Genotype . o f ( IntegerChromosome . o f ( domain ) ) , g t −> g t . getChromosome ( ) . getGene ( ) . g e t A l l e l e ( ) ); } Listing 2.8: Codec factory method: ofScalar The usage of the Codec, created by this factory method, simplifies the implementation of the fitness Function and the creation of the evolution Engine. For scalar types, the saving, in complexity and lines of code, is not that big, but using the factory method is still quite handy. The following listing demonstrates the interaction between Codec, fitness Function and evolution Engine. 1 2 3 4 5 6 7 8 9 10 11 12 c l a s s Main { // F i t n e s s f u n c t i o n d i r e c t l y t a k e s an ’ i n t ’ v a l u e . s t a t i c double f i t n e s s ( i n t a r g ) { return . . . ; } public s t a t i c void main ( S t r i n g [ ] a r g s ) { f i n a l Engine e n g i n e = Engine . b u i l d e r ( Main : : f i t n e s s , o f S c a l a r ( IntRange . o f ( 0 , 1 0 0 ) ) ) . build () ; ... } } 52 2.3. CODEC 2.3.2 CHAPTER 2. ADVANCED TOPICS Vector codec In the listing 2.9, the ofVector factory method returns a Codec for an int[] array. The domain parameter defines the allowed range of the int values and the length defines the length of the encoded int array. 1 2 3 4 5 6 7 8 9 10 11 s t a t i c Codec o f V e c t o r ( IntRange domain , int length ) { return Codec . o f ( Genotype . o f ( IntegerChromosome . o f ( domain , l e n g t h ) ) , g t −> g t . getChromosome ( ) . a s ( IntegerChromosome . c l a s s ) . toArray ( ) ); } Listing 2.9: Codec factory method: ofVector The usage example of the vector Codec is almost the same as for the scalar Codec. As additional parameter, we need to define the length of the desired array and we define our fitness function with an int[] array. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 c l a s s Main { // F i t n e s s f u n c t i o n d i r e c t l y t a k e s an ’ i n t [ ] ’ a r r a y . s t a t i c double f i t n e s s ( i n t [ ] a r g s ) { return . . . ; } public s t a t i c void main ( S t r i n g [ ] a r g s ) { f i n a l Engine e n g i n e = Engine . builder ( Main : : f i t n e s s , o f V e c t o r ( IntRange . o f ( 0 , 1 0 0 ) , 1 0 ) ) . build () ; ... } } 2.3.3 Subset codec There are currently two kinds of subset codecs you can choose from: finding subsets with variable size and with fixed size. Variable-sized subsets A Codec for variable-sized subsets can be easily implemented with the use of a BitChromosome, as shown in listing 2.10. 1 2 3 4 5 6 7 8 9 s t a t i c Codec , BitGene> o f S u b S e t ( ISeq b a s i c S e t ) { return Codec . o f ( Genotype . o f ( BitChromosome . o f ( b a s i c S e t . l e n g t h ( ) ) ) , g t −> g t . getChromosome ( ) . a s ( BitChromosome . c l a s s ) . o n e s ( ) . mapToObj ( b a s i c S e t ) . c o l l e c t ( ISeq . toISeq ( ) ) ); } Listing 2.10: Codec factory method: 53 ofSubSet 2.3. CODEC CHAPTER 2. ADVANCED TOPICS The following usage example of subset Codec shows a simplified version of the Knapsack problem (see section 5.4 on page 110). We try to find a subset, from the given basic SET, where the sum of the values is as big as possible, but smaller or equal than 20. 1 2 3 4 c l a s s Main { // The b a s i c s e t from where t o c h o o s e an ’ o p t i m a l ’ s u b s e t . f i n a l s t a t i c ISeq SET = ISeq . of (1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10) ; 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 } // F i t n e s s f u n c t i o n d i r e c t l y t a k e s an ’ i n t ’ v a l u e . s t a t i c i n t f i t n e s s ( ISeq s u b s e t ) { a s s e r t ( s u b s e t . s i z e ( ) <= SET . s i z e ( ) ) ; f i n a l i n t s i z e = s u b s e t . stream ( ) . c o l l e c t ( C o l l e c t o r s . summingInt ( I n t e g e r : : i n t V a l u e ) ) ; return s i z e <= 20 ? s i z e : 0 ; } public s t a t i c void main ( S t r i n g [ ] a r g s ) { f i n a l Engine e n g i n e = Engine . b u i l d e r ( Main : : f i t n e s s , o f S u b S e t (SET) ) . build () ; ... } Fixed-size subsets The second kind of subset codec allows you to find the best subset of a given, fixed size. A classical usage for this encoding is the Subset sum problem12 : Given a set (or multi-set) of integers, is there a non-empty subset whose sum is zero? For example, given the set {−7, −3, −2, 5, 8}, the answer is yes because the subset {−3, -2, 5} sums to zero. The problem is NP-complete.13 1 2 3 4 5 public c l a s s SubsetSum implements Problem , EnumGene, I n t e g e r > { private f i n a l ISeq _ b a s i c S e t ; private f i n a l i n t _ s i z e ; 6 public SubsetSum ( ISeq b a s i c S e t , i n t s i z e ) { _basicSet = basicSet ; _size = s i z e ; } 7 8 9 10 11 @Override public Function , I n t e g e r > f i t n e s s ( ) { return s u b s e t −> abs ( s u b s e t . stream ( ) . mapToInt ( I n t e g e r : : i n t V a l u e ) . sum ( ) ) ; } 12 13 14 15 16 17 18 19 20 21 22 } @Override public Codec , EnumGene> c o d e c ( ) { return Codecs . o f S u b S e t ( _ b a s i c S e t , _ s i z e ) ; } 12 https://en.wikipedia.org/wiki/Subset_sum_problem 13 https://en.wikipedia.org/wiki/NP-completeness 54 2.3. CODEC 2.3.4 CHAPTER 2. ADVANCED TOPICS Permutation codec This kind of codec can be used for problems where optimal solution depends on the order of the input elements. A classical example for such problems is the Knapsack problem (chapter 5.5 on page 112). 1 2 3 4 5 6 7 8 9 s t a t i c Codec > o f P e r m u t a t i o n (T . . . a l l e l e s ) { return Codec . o f ( Genotype . o f ( PermutationChromosome . o f ( a l l e l e s ) ) , g t −> g t . getChromosome ( ) . stream ( ) . map( EnumGene : : g e t A l l e l e ) . t o A r r a y ( l e n g t h −> (T [ ] ) Array . n e w I n s t a n c e ( a l l e l e s [ 0 ] . getClass () , length ) ) ); } Listing 2.11: Codec factory method: ofPermutation Listing 2.11 shows the implementation of a permutation codec, where the order of the given alleles influences the value of the fitness function. An alternate formulation of the traveling salesman problem is shown in the following listing. It uses the permutation codec in listing 2.11 and uses io.jenetics.jpx.WayPoints, from the JPX 14 project, for representing the city locations. 1 2 3 public c l a s s TSM { // The l o c a t i o n s t o v i s i t . s t a t i c f i n a l ISeq POINTS = I S e q . o f ( . . . ) ; 4 // The p e r m u t a t i o n c o d e c . s t a t i c f i n a l Codec , EnumGene > CODEC = Codecs . o f P e r m u t a t i o n (POINTS) ; 5 6 7 8 // The f i t n e s s f u n c t i o n ( i n t h e problem domain ) . s t a t i c double d i s t ( f i n a l ISeq path ) { return path . stream ( ) . c o l l e c t ( Geoid .DEFAULT. toTourLength ( ) ) . t o ( Length . Unit .METER) ; } 9 10 11 12 13 14 15 // The e v o l u t i o n e n g i n e . s t a t i c f i n a l Engine , Double> ENGINE = Engine . b u i l d e r (TSM : : d i s t , CODEC) . o p t i m i z e ( Optimize .MINIMUM) . build () ; 16 17 18 19 20 21 // Find t h e s o l u t i o n . public s t a t i c void main ( f i n a l S t r i n g [ ] a r g s ) { f i n a l ISeq r e s u l t = CODEC. d ec od e ( ENGINE . stream ( ) . limit (10) . c o l l e c t ( E v o l u t i o n R e s u l t . toBestGenotype ( ) ) ); 22 23 24 25 26 27 28 29 30 31 32 } } System . out . p r i n t l n ( r e s u l t ) ; 14 https://github.com/jenetics/jpx 55 2.3. CODEC 2.3.5 CHAPTER 2. ADVANCED TOPICS Mapping codec This codec is a variation of the permutation codec. Instead of permuting the elements of a given array, it permutes the mapping of elements of a source set to the elements of a target set. The code snippet below shows the method of the Codecs class, which creates a mapping codec from a given source- and target set. 1 2 public s t a t i c Codec