The basic module of the Jenetics library contains all classes for solving optimization via Genetic Algorithm.
The minimum evolution Engine setup needs a genotype factory, Factory<Genotype<G>>, and a fitness Function. The Genotype implements the Factory interface and can therefore be used as prototype for creating the initial Population and for creating new random Genotypes.
import io.jenetics.BitChromosome;
import io.jenetics.BitGene;
import io.jenetics.Genotype;
import io.jenetics.engine.Engine;
import io.jenetics.engine.EvolutionResult;
import io.jenetics.util.Factory;
public class HelloWorld {
// 2.) Definition of the fitness function.
private static Integer eval(Genotype<BitGene> gt) {
return gt.chromosome()
.as(BitChromosome.class)
.bitCount();
}
public static void main(String[] args) {
// 1.) Define the genotype (factory) suitable
// for the problem.
Factory<Genotype<BitGene>> gtf =
Genotype.of(BitChromosome.of(10, 0.5));
// 3.) Create the execution environment.
Engine<BitGene, Integer> engine = Engine
.builder(HelloWorld::eval, gtf)
.build();
// 4.) Start the execution (evolution) and
// collect the result.
Genotype<BitGene> result = engine.stream()
.limit(100)
.collect(EvolutionResult.toBestGenotype());
System.out.println("Hello World:\n" + result);
}
}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 streaming API. Now let’s have a closer look at listing above and discuss this simple program step by step:
-
The probably most challenging part, when setting up a new evolution
Engine, is to transform the problem domain into a appropriateGenotype(factory) representation. In our example we want to count the number of ones of aBitChromosome. Since we are counting only the ones of one chromosome, we are adding only oneBitChromosometo ourGenotype. In general, theGenotypecan be created with 1 to n chromosomes. -
Once this is done, the fitness function which should be maximized, can be defined. Utilizing the new language features introduced in Java 8, we simply 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 theChromosome<BitGene>class to the actual usedBitChromosomeclass. Since we know for sure that we created the Genotype with aBitChromosome, this can be done safely. A reference to the eval method is then used as fitness function and passed to theEngine.buildmethod. -
In the third step we are creating the evolution
Engine, which is responsible for changing, respectively evolving, a given population. TheEngineis 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. By changing the usedExecutorservice, you control the number of threads, the Engine is allowed to use. An newEngineinstance can only be created via its builder, which is created by calling theEngine.buildermethod. -
In the last step, we can create a new
EvolutionStreamfrom ourEngine. TheEvolutionStreamis 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, theEvolutionStreamwill not terminate and run forever. Since theEvolutionStreamextends thejava.util.stream.Streaminterface, it integrates smoothly with the rest of the Java Stream API. The final result, the bestGenotypein our example, is then collected with one of the predefined collectors of theEvolutionResultclass.