gegene 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0599f2cbca99f724dacc4eb7f106122174108ea8
4
- data.tar.gz: c32d27d42638c9e12e292f615002663b8eae4581
3
+ metadata.gz: 99c7470e76700e9f8111387771e1fdd0fde4c2ff
4
+ data.tar.gz: f88f83cea4ea0ae391c2e7f0d70457c1c3e99333
5
5
  SHA512:
6
- metadata.gz: 0144049db0f4c7634d21815c3b1c0d3e8b5347fb2167d82219b25e52112d1594f21710948e63e58c4196420ea63148d5fa0fab7ec14ade177982316f0e1c6583
7
- data.tar.gz: 3f54ddadf7ced812bd993cdc939a73ddebc26063302a502f753cef97e4d2d03c212616c241e9ecd1f97d130b9a8119661317b5491deed85006da3ef12a408049
6
+ metadata.gz: 6806f2aece4b0e729c76b1fbca4a28c40bd0e08eb1fee29e3194dd7f7bb5c75bc6bb8f40de46d0b634adb6ff29ab7843dcd803bdac1de5e5f173d944a293b733
7
+ data.tar.gz: dd58f6acc164a80abf371428d829e57c9096ffff152295a37c62ed1a58ec5e96831e0d893f59529e2a6329cb5d76348cc0e1a799bc0e03f68950048f1dd40ffc
data/Gemfile.lock ADDED
@@ -0,0 +1,34 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ gegene (1.2.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ diff-lcs (1.2.4)
10
+ multi_json (1.8.2)
11
+ rspec (2.14.1)
12
+ rspec-core (~> 2.14.0)
13
+ rspec-expectations (~> 2.14.0)
14
+ rspec-mocks (~> 2.14.0)
15
+ rspec-core (2.14.6)
16
+ rspec-expectations (2.14.3)
17
+ diff-lcs (>= 1.1.3, < 2.0)
18
+ rspec-mocks (2.14.4)
19
+ simplecov (0.7.1)
20
+ multi_json (~> 1.0)
21
+ simplecov-html (~> 0.7.1)
22
+ simplecov-html (0.7.1)
23
+
24
+ PLATFORMS
25
+ ruby
26
+
27
+ DEPENDENCIES
28
+ gegene!
29
+ rspec
30
+ rspec-mocks
31
+ simplecov
32
+
33
+ BUNDLED WITH
34
+ 1.12.5
data/README.md ADDED
@@ -0,0 +1,197 @@
1
+ gegene
2
+ ======
3
+ A generic framework for genetic algorithm fast development in Ruby
4
+
5
+ Introduction
6
+ ------------
7
+
8
+ >In the computer science field of artificial intelligence, a genetic algorithm (GA) is a search heuristic that mimics the process of natural selection. This heuristic (also sometimes called a metaheuristic) is routinely used to generate useful solutions to optimization and search problems.[1] Genetic algorithms belong to the larger class of evolutionary algorithms (EA), which generate solutions to optimization problems using techniques inspired by natural evolution, such as inheritance, mutation, selection, and crossover.
9
+
10
+ from http://en.wikipedia.org/wiki/Genetic_algorithm
11
+
12
+ In order to offer a fast prototyping framework for genetic algorithm, I created gegene (French shorter for Eugene).
13
+
14
+ With gegene, all you got to do in order to use genetic algorithms to solve a problem is:
15
+
16
+ 1. define the genome of your solutions in terms of gene type and chromosome organisation (eq: write an array of hashes)
17
+ 2. define the way you would like to evaluate each individuals (eq: write a fitness function which takes a karyotype as parameter)
18
+ 3. create a population of individuals (eq: instantiates a Population object)
19
+ 4. evolve ! (eq: run Population.evolve())
20
+ 5. all your solutions are sorted by fitness value (fittest first) in Population.karyotypes.
21
+
22
+ Overview
23
+ --------
24
+
25
+ ### Example
26
+
27
+ In the following code, we use gegene to find *a* and *b* so _a*a + b = 12_, with *a* included in [0,5], and *b* included in [-5,5]:
28
+ ```Ruby
29
+ require 'gegene'
30
+ FITNESS_TARGET = 1 / 0.001
31
+ population = Population.new(
32
+ 50,
33
+ [{a:Gene.Integer(0, 5)},{b:Gene.Integer(-5,5)}],
34
+ lambda {|k| 1 / (0.001 + (12-(k[:a]**2+k[:b])).abs) }
35
+ )
36
+ population.set_mutation_rate(0.5).set_fitness_target(FITNESS_TARGET).evolve(50)
37
+ bk = population.karyotypes[0]
38
+ warn "a:#{bk[:a]} b:#{bk[:b]} => a*a + b = #{bk[:a]**2+bk[:b]}"
39
+ ```
40
+
41
+ The result could be :
42
+
43
+ a:4 b:-4 => a*a + b = 12
44
+
45
+ or
46
+
47
+ a:3 b:3 => a*a + b = 12
48
+
49
+ _Note: this source code is available 'in example/simple.rb'_
50
+
51
+ ### Features
52
+
53
+ At this very moment, gegene features:
54
+
55
+ * Inheritance
56
+ * Mutation
57
+ * Cross over
58
+ * Non linear selection
59
+ * An optional fitness value cache, to avoid extra computing
60
+ * A pretty cool name
61
+
62
+ ### What will come next ?
63
+
64
+ My next moves will be:
65
+
66
+ * your call
67
+
68
+ Tutorial
69
+ --------
70
+
71
+ *This tutorial is available in 'example/one_max.rb'*
72
+
73
+ Let's consider the one max problem (an explanation can be found here : http://tracer.lcc.uma.es/problems/onemax/onemax.html ), where the goal is maximizing the number of ones of an array of bits.
74
+
75
+ For this example, we'll use an array of 3 bits, named v1, v2 and v3.
76
+ Obviously, the solution to this problem would be:
77
+
78
+ * v1 = 1
79
+ * v2 = 1
80
+ * v3 = 1
81
+
82
+ Let's see if gegene is able to figure this out !
83
+
84
+ ### Setting the genome description
85
+
86
+ First of all, you will need to add the following line to the top of your script:
87
+ ```Ruby
88
+ require 'gegene'
89
+ ```
90
+
91
+ If your lib path contains gegene, your script should still execute well.
92
+
93
+ Then, we have to figure out a way to describe the potential solutions to this problem.
94
+ Obviously, there is three variables to this problem (v1, v2 & v3), so we will use a genome containing 3 genes.
95
+ Each of these gene allow 0 or 1 as allele's value, so we will use an integer gene, whose range of values will be [0,1]. Do not add this source code to your script for the moment:
96
+ ```Ruby
97
+ Gene.Integer(0, 1)
98
+ ```
99
+
100
+ In order to maximize the diversity of combinations, well describe three chromosome descriptions, each of them containing an integer gene and named after the corresponding variable:
101
+ ```Ruby
102
+ # Follow a genome description for the one max problem:
103
+ genome_description = [
104
+ { v1: Gene.Integer(0,1) },
105
+ { v2: Gene.Integer(0,1) },
106
+ { v3: Gene.Integer(0,1) }
107
+ ]
108
+ ```
109
+
110
+ ### Defining a fitness function
111
+
112
+ Next, we have to define the fitness function. This one is a pretty simple one, as we can sum the three values in order to score a karyotype:
113
+ ```Ruby
114
+ # Here is the fitness function of the one max problem:
115
+ def fitness(karyotype)
116
+ karyotype[:v1] + karyotype[:v2] + karyotype[:v3]
117
+ end
118
+ ```
119
+
120
+ _Note that each of the karyotype 's allele can be accessed through its gene name_
121
+ The parameter of the fitness function is a [karyotype]( http://en.wikipedia.org/wiki/Karyotype). It contains all the chromosomes of a specific individual of our population.
122
+
123
+ ### Creating and making a population evolve
124
+
125
+ Now that we are happy with our genome and fitness function, we have to create a population:
126
+ ```Ruby
127
+ # We create a population of six individuals with the previous desc & func:
128
+ population = Population.new(6, genome_description, method(:fitness))
129
+ ````
130
+
131
+ We already know the result of our fitness function for the best solution, so we can set the fitness target of our population. If the fitness target is reached, the population will stop evolving. A good usage to the fitness target is setting it to an acceptable score to avoid endless processing of a population with very small enhancement of the solutions.
132
+ ```Ruby
133
+ # As we known the best solution for the one max problem, we set a
134
+ # fitness target of 3 (1+1+1).
135
+ population.fitness_target = 3
136
+ ````
137
+
138
+ As the population contains only 6 individuals, we set a high mutation rate in order to introduce "new blood" at each iteration:
139
+ ```Ruby
140
+ # As the population is quite small, we add a little more funk to our
141
+ # evolution process by setting a 30% mutation rate
142
+ population.mutation_rate = 0.3
143
+ ```
144
+
145
+ Let me introduce to you.... Evolution !
146
+ ```Ruby
147
+ # Let's go for some darwinist fun !
148
+ population.evolve(10)
149
+ ````
150
+
151
+ In order to examine the best solution found, you can check the array of karyotypes (sorted by fitness value) describing the current population state:
152
+ ```Ruby
153
+ # population.karyotypes is sorted by fitness score, so we can assume that
154
+ # the first element is the fittest
155
+ best_karyotype = population.karyotypes[0]
156
+
157
+ puts "Best karyotype scored #{best_karyotype.fitness}:"
158
+ [:v1, :v2, :v3].each {|x| puts " #{x.to_s}:#{best_karyotype[x]}" }
159
+ ````
160
+
161
+ This code should display:
162
+
163
+ Best karyotype scored 3:
164
+ v1:1
165
+ v2:1
166
+ v3:1
167
+
168
+ Congratulations for your first evolution with gegene !
169
+
170
+ More details on the genes
171
+ -------------------------
172
+
173
+ ### Available Gene types
174
+
175
+ * Gene.Integer(min, max): An integer, randomly selected in the range [min, max].
176
+ * Gene.Float(min, max): A float, randomly selected in the range [min, max].
177
+ * Gene.Enum(values_array): A value from a set of values provided in an array.
178
+
179
+ ### Adding new gene types
180
+
181
+ If needed, you are able to add gene type to any project.
182
+ In order to do so, you have to create a class inheriting from the Gene class.
183
+ This class *must* provide implementation for two methods:
184
+
185
+ ````Ruby
186
+ def random_allele_value()
187
+ end
188
+ ````
189
+ Create a random value, according to the set of rules you choosed (ex: BooleanGene should return true or false, on a random basis)
190
+
191
+ ````Ruby
192
+ def mutate(previous_value)
193
+ end
194
+ ````
195
+ Create a mutated value, which can (but not necessarily) depends on the previous value an allele used to carry.
196
+
197
+ An example of gene type creation is available in 'example/adding_gene_type.rb'.
data/gegene.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ $LOAD_PATH.unshift File.expand_path('../lib', __FILE__)
2
+ require 'gegene/version'
3
+ Gem::Specification.new do |s|
4
+ s.name = 'gegene'
5
+ s.version = Gegene::Version::STRING
6
+ s.platform = Gem::Platform::RUBY
7
+ s.summary = "Genetic algorithm helpers"
8
+ s.description = "Framework for genetic algorithm fast development"
9
+ s.authors = ["Alexandre Ignjatovic"]
10
+ s.email = 'alexandre.ignjatovic@gmail.com'
11
+ s.license = 'MIT'
12
+ s.files = `git ls-files`.split($RS).reject do |file|
13
+ file =~ %r{^(?:
14
+ spec/.*
15
+ |Gemfile
16
+ |Rakefile
17
+ |\.rspec
18
+ |\.gitignore
19
+ |\.rubocop.yml
20
+ |\.rubocop_todo.yml
21
+ |.*\.eps
22
+ )$}x
23
+ end
24
+ s.homepage = 'https://github.com/bankair/gegene'
25
+ end
data/lib/allele.rb CHANGED
@@ -1,16 +1,17 @@
1
+ # Specific value for a specific gene
1
2
  class Allele
2
3
  attr_accessor :value
3
4
  def initialize(gene, value)
4
5
  @gene = gene
5
6
  @value = value
6
7
  end
7
-
8
+
8
9
  def mutate
9
10
  @value = @gene.mutate(@value)
10
11
  end
11
-
12
+
12
13
  def copy
13
14
  # /!\ if @value is a ref, its underlying object won't be copied
14
15
  Allele.new(@gene, @value)
15
16
  end
16
- end
17
+ end
data/lib/chromosome.rb CHANGED
@@ -1,70 +1,59 @@
1
1
  require 'gene'
2
2
  require 'allele'
3
+ require 'forwardable'
3
4
 
4
- # This class represent a chromosome, which contains several allele (gene's values)
5
- # and is able to mutate, and to cross over.
5
+ # This class represent a chromosome, which contains several allele
6
+ # (gene's values) and is able to mutate, and to cross over.
6
7
  class Chromosome
7
-
8
+ extend Forwardable
9
+
10
+ def_delegators :@alleles, :size, :[], :each_with_index, :map
8
11
  # Construct a chromosome from an array of alleles
9
12
  def initialize(alleles)
10
- raise "this constructor expect an array of alleles as input" unless
11
- alleles.is_a?(Array)
13
+ unless alleles.is_a?(Array)
14
+ puts alleles.inspect
15
+ raise 'this constructor expect an array of alleles as input'
16
+ end
12
17
  @alleles = alleles
13
18
  end
14
-
19
+
15
20
  # Copy the current chromosome and all its alleles
16
21
  def copy
17
- Chromosome.new(@alleles.map{|allele| allele.copy })
22
+ Chromosome.new(map(&:copy))
18
23
  end
19
-
24
+
20
25
  # Create a random chromosome from a description
21
- def Chromosome.create_random_from(description)
22
- Chromosome.new(description.map{|gene| gene.create_random() })
26
+ def self.create_random_from(description)
27
+ new(description.map(&:create_random))
23
28
  end
24
-
25
- # Returns the allele at a specific position
26
- def [](gene_position)
27
- @alleles[gene_position]
28
- end
29
-
30
- # Number of underlying alleles
31
- def size
32
- @alleles.size
33
- end
34
-
29
+
35
30
  # Mutate a randomly selected allele of the current chromosome
36
31
  def mutate
37
- allele_index = rand @alleles.size
38
- @alleles[allele_index].mutate()
32
+ @alleles[rand size].mutate
39
33
  end
40
-
34
+
41
35
  # Aggregate all alleles values
42
- def aggregated_alleles()
43
- @alleles.map {|a| a.value}.join(";")
36
+ def aggregated_alleles
37
+ map(&:value).join(';')
44
38
  end
45
-
39
+
46
40
  # Cross over two chromosomes to provide a new one
47
- def Chromosome.cross_over(chromosome_a, chromosome_b)
48
- if rand(2) == 0 then
49
- chromosome_a, chromosome_b =
50
- [chromosome_b, chromosome_a]
51
- end
52
- size = chromosome_a.size
53
- if size < 2 then
54
- return chromosome_a.copy
55
- elsif size == 2 then
56
- return Chromosome.new([chromosome_a[0],chromosome_b[1]])
57
- else
58
- swap_index = rand(size-1)
59
- index = 0
60
- new_alleles = []
61
- while (index < size) do
62
- new_alleles.push(
63
- (index <= swap_index ? chromosome_a : chromosome_b)[index].copy)
64
- index += 1
65
- end
66
- return Chromosome.new(new_alleles)
67
- end
41
+ def self.cross_over(chromo_a, chromo_b)
42
+ chromo_a, chromo_b = randomize_chromosomes(chromo_a, chromo_b)
43
+ size = chromo_a.size
44
+ return chromo_a.copy if size < 2
45
+ return new([chromo_a[0], chromo_b[1]].map!(&:copy)) if size == 2
46
+ cross_over_impl(chromo_a, chromo_b, size)
47
+ end
48
+
49
+ def self.cross_over_impl(chromosome_a, chromosome_b, size)
50
+ swap_index = rand(size - 1)
51
+ new(chromosome_a.each_with_index.map do |from_a, index|
52
+ (index <= swap_index ? from_a : chromosome_b[index]).copy
53
+ end)
54
+ end
55
+
56
+ def self.randomize_chromosomes(*chromosomes)
57
+ chromosomes.sort_by! { rand }
68
58
  end
69
-
70
- end
59
+ end
data/lib/gegene.rb CHANGED
@@ -1,2 +1,3 @@
1
1
  require 'gene'
2
- require 'population'
2
+ require 'population'
3
+ require 'version'
@@ -0,0 +1,13 @@
1
+ # encoding: utf-8
2
+ module Gegene
3
+ # This module holds the Ihasa version information.
4
+ module Version
5
+ STRING = '1.2.0'
6
+
7
+ module_function
8
+
9
+ def version(debug = false)
10
+ STRING
11
+ end
12
+ end
13
+ end
data/lib/gene.rb CHANGED
@@ -1,17 +1,28 @@
1
-
2
- class Gene
3
- def mutate(allele)
4
- raise "the 'mutate' function must be overloaded in the inheriting class."
1
+ # A Gene can take different values, randomly mutate.
2
+ class Gene
3
+ def mutate(_allele)
4
+ raise_missing_impl :mutate
5
5
  end
6
6
 
7
7
  def random_allele_value
8
- raise "the 'random_allele_value' function must be overloaded in the inheriting class."
8
+ raise_missing_impl :random_allele_value
9
9
  end
10
+
10
11
  def create_random
11
- Allele.new(self, self.random_allele_value)
12
+ Allele.new(self, random_allele_value)
13
+ end
14
+
15
+ private
16
+
17
+ MISSING_IMPL_ERR_FMT =
18
+ "the '%s' function must be overloaded in the inheriting class.".freeze
19
+
20
+ def raise_missing_impl(func)
21
+ raise MISSING_IMPL_ERR_FMT % func
12
22
  end
13
23
  end
14
24
 
25
+ # Root class for all numeric genes
15
26
  class NumericGene < Gene
16
27
  attr_accessor :min, :max
17
28
  def initialize(min, max)
@@ -22,51 +33,59 @@ class NumericGene < Gene
22
33
 
23
34
  def mutate(previous_value)
24
35
  next_value = random_allele_value
25
- while next_value == previous_value do
26
- next_value = random_allele_value
27
- end
36
+ next_value = random_allele_value while next_value == previous_value
28
37
  next_value
29
38
  end
30
- end
39
+ end
31
40
 
41
+ # Integer gene class
32
42
  class IntegerGene < NumericGene
33
- def initialize(min, max) super(min, max) end
34
-
35
- def random_allele_value() rand(self.min..self.max) end
43
+ def random_allele_value
44
+ rand(min..max)
45
+ end
36
46
  end
37
47
 
48
+ # Float gene class
38
49
  class FloatGene < NumericGene
39
- def initialize(min, max) super(min, max) end
40
-
41
50
  def random_allele_value
42
- rand() * (self.max - self.min) + self.min
51
+ rand * (max - min) + min
43
52
  end
44
53
  end
45
54
 
55
+ # Enumeration gene class
46
56
  class EnumGene < Gene
47
57
  attr_accessor :enum_values
48
58
 
49
59
  def initialize(enum_values)
50
- raise "EnumGene initialization require an Array" unless enum_values.is_a? Array
51
- raise "EnumGene require at least two values" unless enum_values.size > 1
60
+ unless enum_values.is_a? Array
61
+ raise 'EnumGene initialization require an Array'
62
+ end
63
+ raise 'EnumGene require at least two values' unless enum_values.size > 1
52
64
  @enum_values = enum_values
53
65
  end
54
-
66
+
55
67
  def random_allele_value
56
68
  @enum_values[rand @enum_values.size]
57
69
  end
58
-
70
+
59
71
  def mutate(previous_value)
60
- new_value = self.random_allele_value
61
- while new_value == previous_value
62
- new_value = self.random_allele_value
63
- end
72
+ new_value = random_allele_value
73
+ new_value = random_allele_value while new_value == previous_value
64
74
  new_value
65
75
  end
66
76
  end
67
77
 
78
+ # Gene class build methods
68
79
  class Gene
69
- def self.Integer(min, max) IntegerGene.new(min, max) end
70
- def self.Float(min, max) FloatGene.new(min, max) end
71
- def self.Enum(enum_values) EnumGene.new(enum_values) end
72
- end
80
+ def self.Integer(min, max)
81
+ IntegerGene.new(min, max)
82
+ end
83
+
84
+ def self.Float(min, max)
85
+ FloatGene.new(min, max)
86
+ end
87
+
88
+ def self.Enum(enum_values)
89
+ EnumGene.new(enum_values)
90
+ end
91
+ end
data/lib/genome.rb CHANGED
@@ -3,36 +3,43 @@ require 'Karyotype'
3
3
  # This class stands for the genome. It is a description of all
4
4
  # the gene describing a specific population.
5
5
  class Genome
6
-
7
6
  DEFAULT_CROSS_OVER_RATE = 0.01
8
-
7
+ attr_accessor :gene_positions
8
+ attr_reader :cross_over_rate
9
+
9
10
  def initialize(genome_description)
10
- raise "Genome description MUST be an Array" unless genome_description.is_a? Array
11
- @chromosomes_description = []
12
- @gene_positions = {}
13
- genome_description.each_with_index do |chomosome_hash, chromosome_position|
14
- gene_array = []
15
- chomosome_hash.keys.each_with_index do |gene_name, gene_position|
16
- @gene_positions[gene_name] = [chromosome_position, gene_position]
17
- gene_array << chomosome_hash[gene_name]
18
- end
19
- @chromosomes_description << gene_array
11
+ unless genome_description.is_a? Array
12
+ raise 'Genome description MUST be an Array'
20
13
  end
14
+ initialize_genes_and_chromosomes_from! genome_description
21
15
  @cross_over_rate = DEFAULT_CROSS_OVER_RATE
22
16
  end
23
- attr_accessor :gene_positions
24
- attr_reader :cross_over_rate
17
+
25
18
  def cross_over_rate=(rate)
26
- raise "cross_over_rate must be included in [0,1]" unless rate.between?(0,1)
19
+ raise 'cross_over_rate must be included in [0,1]' unless rate.between?(0, 1)
27
20
  @cross_over_rate = rate
28
21
  end
29
-
22
+
30
23
  def get_gene_position(gene_name)
31
24
  @gene_positions[gene_name]
32
25
  end
33
-
26
+
34
27
  def create_random_karyotype
35
28
  Karyotype.create_random_from(self, @chromosomes_description)
36
29
  end
37
-
38
- end
30
+
31
+ private
32
+
33
+ def initialize_genes_and_chromosomes_from!(genome_description)
34
+ @chromosomes_description = []
35
+ @gene_positions = {}
36
+ genome_description.each_with_index do |chomosome_hash, chromosome_position|
37
+ gene_array = []
38
+ chomosome_hash.keys.each_with_index do |gene_name, gene_position|
39
+ @gene_positions[gene_name] = [chromosome_position, gene_position]
40
+ gene_array << chomosome_hash[gene_name]
41
+ end
42
+ @chromosomes_description << gene_array
43
+ end
44
+ end
45
+ end
data/lib/karyotype.rb CHANGED
@@ -5,34 +5,34 @@ require 'digest/md5'
5
5
  class Karyotype
6
6
  attr_accessor :chromosomes
7
7
  attr_accessor :fitness
8
-
8
+
9
9
  def initialize(genome, chromosomes_description)
10
10
  @genome = genome
11
11
  @chromosomes_description = chromosomes_description
12
12
  end
13
13
  private :initialize
14
-
14
+
15
15
  # Copy self and all its chromosomes to a new karyotype
16
16
  def copy
17
17
  karyotype = Karyotype.new(@genome, @chromosomes_description)
18
- karyotype.chromosomes = self.chromosomes.map{|chromosome| chromosome.copy}
18
+ karyotype.chromosomes = chromosomes.map(&:copy)
19
19
  karyotype
20
20
  end
21
-
21
+
22
22
  def to_s
23
- @genome.gene_positions.keys.map{|gn| "#{gn}=>#{self[gn]}"}.join';'
23
+ @genome.gene_positions.keys.map { |gn| "#{gn}=>#{self[gn]}" }.join';'
24
24
  end
25
-
25
+
26
26
  # Create a random karyotype from a specific genome an its associated
27
27
  # chromosomes description
28
- def Karyotype.create_random_from(genome, chromosomes_description)
28
+ def self.create_random_from(genome, chromosomes_description)
29
29
  karyotype = Karyotype.new(genome, chromosomes_description)
30
30
  karyotype.chromosomes = chromosomes_description.map {|description|
31
31
  Chromosome.create_random_from(description)
32
32
  }
33
33
  karyotype
34
34
  end
35
-
35
+
36
36
  # Return the allele value of a specific named gene
37
37
  # We strongly recommand using symbols as gene name
38
38
  def [](gene_name)
@@ -40,44 +40,44 @@ class Karyotype
40
40
  chromosome_position, gene_position =
41
41
  @genome.get_gene_position(gene_name)
42
42
  return nil if chromosome_position.nil? || gene_position.nil?
43
- return @chromosomes[chromosome_position][gene_position].value
43
+ @chromosomes[chromosome_position][gene_position].value
44
44
  end
45
-
45
+
46
46
  # Breeding function
47
47
  # Create a new karyotype based on self and an other
48
48
  def +(other)
49
49
  child = Karyotype.new(@genome, @chromosomes_description)
50
50
  child.chromosomes = []
51
- other.chromosomes.each_with_index { |chromosome, index|
52
- child_chromosome = nil
53
- # Randomly copy chromosomes from dad or mom to child
54
- if (rand(100) / 100.0) < @genome.cross_over_rate then
55
- # Crossing over required
56
- child_chromosome =
57
- Chromosome.cross_over(chromosome, chromosomes[index])
58
- else
59
- # Standard breeding via random selection
60
- child_chromosome = (rand(2)==0?chromosome : chromosomes[index]).copy
61
- end
62
- child.chromosomes.push(child_chromosome)
63
- }
51
+ other.chromosomes.each_with_index do |chromosome, index|
52
+ child.chromosomes.push(select_chromosome(chromosome, chromosomes[index]))
53
+ end
64
54
  child
65
55
  end
66
-
56
+
67
57
  # Aggregate all the allele into a md5 hash value
68
- def to_md5()
69
- if (@hash_value.nil?) then
70
- @hash_value = @chromosomes.map{ |chromosome|
71
- chromosome.aggregated_alleles
72
- }.join(";")
58
+ def to_md5
59
+ if @hash_value.nil?
60
+ @hash_value = @chromosomes.map(&:aggregated_alleles).join(';')
73
61
  @hash_value = Digest::MD5.hexdigest(@hash_value)
74
62
  end
75
63
  @hash_value
76
64
  end
77
-
65
+
78
66
  # Randomly mutate an allele of a randomly selected chromosome
79
67
  def mutate
80
68
  @chromosomes[rand @chromosomes.size].mutate
81
69
  self
82
70
  end
83
- end
71
+
72
+ private
73
+
74
+ def select_chromosome(chromosome_a, chromosome_b)
75
+ if (rand(100) / 100.0) < @genome.cross_over_rate
76
+ # Crossing over required
77
+ Chromosome.cross_over(chromosome_a, chromosome_b)
78
+ else
79
+ # Standard breeding via random selection
80
+ (rand(2) == 0 ? chromosome_a : chromosome_b).copy
81
+ end
82
+ end
83
+ end
data/lib/population.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  require 'genome'
2
-
2
+ # Stand for a whole population used for an evolution experiment
3
3
  class Population
4
4
  DEFAULT_MUTATION_RATE = 0.01
5
5
  DEFAULT_KEEP_ALIVE_RATE = 0.1
@@ -8,83 +8,99 @@ class Population
8
8
 
9
9
  attr_accessor :fitness_target, :karyotypes, :force_fitness_recalculation
10
10
  attr_reader :mutation_rate, :keep_alive_rate
11
-
11
+
12
12
  # mutation rate setter function
13
13
  # Accept values in the range [0,1]
14
14
  def mutation_rate=(value)
15
- raise "mutation_rate value must be included in [0,1]" unless value.between?(0,1)
16
- @mutation_rate = value
15
+ @mutation_rate = validate!(:mutation_rate, value)
17
16
  end
18
-
17
+
19
18
  # keep alive rate setter function
20
19
  # Accept values in the range [0,1]
21
20
  def keep_alive_rate=(value)
22
- raise "mutation_rate value must be included in [0,1]" unless value.between?(0,1)
23
- @keep_alive_rate = value
24
- end
25
-
26
-
27
- # Run the fitness function for all karyotypes, and sort it by fitness
28
- def evaluate
29
- if @force_fitness_recalculation then
30
- @karyotypes.each do |karyotype|
31
- karyotype.fitness = @fitness_calculator.call(karyotype)
32
- @fitness_hash[karyotype.to_md5()] = karyotype.fitness
33
- end
34
- else
35
- @karyotypes.each do |karyotype|
36
- if karyotype.fitness.nil? then
37
- if @fitness_hash[karyotype.to_md5()].nil? then
38
- karyotype.fitness = @fitness_calculator.call(karyotype)
39
- @fitness_hash[karyotype.to_md5()] = karyotype.fitness
40
- else
41
- karyotype.fitness = @fitness_hash[karyotype.to_md5()]
42
- end
43
- end
44
- end
45
- end
46
- @karyotypes.sort! { |x,y| y.fitness <=> x.fitness }
21
+ @keep_alive_rate = validate!(:keep_alive_rate, value)
47
22
  end
48
23
 
49
- private :evaluate
50
-
51
24
  def initialize(size, genome, fitness_calculator)
52
- raise "size must be strictly positive." if size < 1
25
+ raise 'size must be strictly positive.' if size < 1
53
26
  @fitness_hash = {}
54
27
  @force_fitness_recalculation = DEFAULT_FORCE_FITNESS_RECALCULATION
55
28
  @mutation_rate = DEFAULT_MUTATION_RATE
56
29
  @keep_alive_rate = DEFAULT_KEEP_ALIVE_RATE
57
30
  @genome = Genome.new(genome)
58
31
  @fitness_calculator = fitness_calculator
59
- @karyotypes = Array.new(size){ @genome.create_random_karyotype }
32
+ @karyotypes = Array.new(size) { @genome.create_random_karyotype }
60
33
  evaluate
61
34
  end
62
35
 
63
- def set_mutation_rate(rate)
64
- raise "mutation_rate value must be included in [0,1]" unless rate.between?(0,1)
65
-
66
- @mutation_rate= rate
67
- self
36
+ %i(mutation_rate keep_alive_rate
37
+ fitness_target force_fitness_recalculation).each do |sym|
38
+ define_method("set_#{sym}") { |value| tap { |o| o.send("#{sym}=", value) } }
68
39
  end
69
40
 
70
- def set_keep_alive_rate(rate)
71
- raise "keep_alive_rate must be included in [0,1]" unless rate.between?(0,1)
72
- @keep_alive_rate = rate
73
- self
41
+ def size
42
+ @karyotypes.size
74
43
  end
75
44
 
76
- def set_fitness_target(target)
77
- @fitness_target = target
45
+ # This function make ou population evolving by:
46
+ # * Selecting and breeding the fittest karyotypes
47
+ # * Running the fitness evaluation on all the newly created karyotyopes
48
+ # The selection process include three subprocesses:
49
+ # * Selecting the fittest individuals to keep alive
50
+ # * Mutating randomly (linear) selected individuals
51
+ # * Breeding randomly (fitness weighted) selected individuals
52
+ def evolve(iterations = DEFAULT_EVOLVE_ITERATIONS)
53
+ i = 1
54
+ while (i <= iterations) &&
55
+ (@fitness_target.nil? || @fitness_target > @karyotypes[0].fitness)
56
+ evolve_impl
57
+ i += 1
58
+ end
78
59
  self
79
60
  end
80
61
 
81
- def set_force_fitness_recalculation(target)
82
- @force_fitness_recalculation = target
83
- self
62
+ private
63
+
64
+ NO_FITNESS = :no_fitness
65
+
66
+ # Run the fitness function for all karyotypes, and sort it by fitness
67
+ def evaluate
68
+ if @force_fitness_recalculation
69
+ @karyotypes.each { |karyotype| update!(karyotype, fitness(karyotype)) }
70
+ else
71
+ @karyotypes.each do |karyotype|
72
+ update!(karyotype, cached_fitness(karyotype)) if karyotype.fitness.nil?
73
+ end
74
+ end
75
+ @karyotypes.sort_by! { |karyotype| - karyotype.fitness }
84
76
  end
85
77
 
86
- def size
87
- @karyotypes.size
78
+ def update!(karyotype, fitness)
79
+ karyotype.fitness = fitness
80
+ @fitness_hash[karyotype.to_md5] = karyotype.fitness
81
+ end
82
+
83
+ def fitness(karyotype)
84
+ @fitness_calculator.call(karyotype)
85
+ end
86
+
87
+ def cached_fitness(karyotype)
88
+ @fitness_hash.fetch(karyotype.to_md5) { fitness karyotype }
89
+ end
90
+
91
+ def evolve_impl
92
+ # Keeping alive a specific amount of the best karyotypes
93
+ keep_alive_count = Integer(@karyotypes.size * @keep_alive_rate)
94
+ mutation_count = Integer(@karyotypes.size * @mutation_rate)
95
+ @karyotypes = build_new_karyotypes(keep_alive_count, mutation_count)
96
+ evaluate
97
+ end
98
+
99
+ def build_new_karyotypes(keep_alive_count, mutation_count)
100
+ remaining = @karyotypes.size - mutation_count - keep_alive_count
101
+ @karyotypes[0, keep_alive_count]
102
+ .concat(Array.new(mutation_count) { create_random_mutation })
103
+ .concat(Array.new(remaining) { random_breed })
88
104
  end
89
105
 
90
106
  def linear_random_select
@@ -95,60 +111,19 @@ class Population
95
111
  linear_random_select.copy.mutate
96
112
  end
97
113
 
98
- private :linear_random_select, :create_random_mutation
99
-
100
114
  def fitness_weighted_random_select
101
115
  @karyotypes[
102
116
  @karyotypes.size -
103
- Integer(Math.sqrt(Math.sqrt(1 + rand(@karyotypes.size**4 - 1))))
117
+ Integer(Math.sqrt(Math.sqrt(1 + rand(@karyotypes.size**4 - 1))))
104
118
  ]
105
119
  end
106
-
120
+
107
121
  def random_breed
108
122
  fitness_weighted_random_select + fitness_weighted_random_select
109
123
  end
110
124
 
111
- private :fitness_weighted_random_select, :random_breed
112
-
113
- # This function make ou population evolving by:
114
- # * Selecting and breeding the fittest karyotypes
115
- # * Running the fitness evaluation on all the newly created karyotyopes
116
- # The selection process include three subprocesses:
117
- # * Selecting the fittest individuals to keep alive
118
- # * Mutating randomly (linear) selected individuals
119
- # * Breeding randomly (fitness weighted) selected individuals
120
- def evolve(iterations = DEFAULT_EVOLVE_ITERATIONS)
121
- i = 1
122
- while (i <= iterations) &&
123
- (@fitness_target.nil? || @fitness_target > @karyotypes[0].fitness) do
124
- evolve_impl
125
- i += 1
126
- end
127
- self
128
- end
129
-
130
- def evolve_impl
131
- new_population = []
132
-
133
- # Keeping alive a specific amount of the best karyotypes
134
- keep_alive_count = Integer(@karyotypes.size * @keep_alive_rate)
135
- if keep_alive_count > 0 then
136
- @karyotypes[0, keep_alive_count].each {|karyotype| new_population.push(karyotype)}
137
- end
138
-
139
- mutation_count = Integer(@karyotypes.size * @mutation_rate)
140
- (0..mutation_count-1).each {
141
- new_population.push create_random_mutation
142
- }
143
-
144
- remaining = @karyotypes.size-mutation_count-keep_alive_count
145
- (0..remaining-1).each {
146
- child = random_breed
147
- new_population.push child
148
- }
149
- @karyotypes = new_population
150
- evaluate
125
+ def validate!(label, value)
126
+ raise "#{label} value must be included in [0,1]" unless value.between?(0, 1)
127
+ value
151
128
  end
152
- private :evolve_impl
153
-
154
- end
129
+ end
Binary file
Binary file
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gegene
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexandre Ignjatovic
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-11-01 00:00:00.000000000 Z
11
+ date: 2016-08-03 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Framework for genetic algorithm fast development
14
14
  email: alexandre.ignjatovic@gmail.com
@@ -16,17 +16,23 @@ executables: []
16
16
  extensions: []
17
17
  extra_rdoc_files: []
18
18
  files:
19
+ - Gemfile.lock
20
+ - LICENSE.txt
21
+ - README.md
22
+ - example/adding_gene_type.rb
23
+ - example/one_max.rb
24
+ - example/simple.rb
25
+ - gegene.gemspec
19
26
  - lib/allele.rb
20
27
  - lib/chromosome.rb
21
28
  - lib/gegene.rb
29
+ - lib/gegene/version.rb
22
30
  - lib/gene.rb
23
31
  - lib/genome.rb
24
32
  - lib/karyotype.rb
25
33
  - lib/population.rb
26
- - example/adding_gene_type.rb
27
- - example/one_max.rb
28
- - example/simple.rb
29
- - LICENSE.txt
34
+ - releases/gegene-1.0.0.gem
35
+ - releases/gegene-1.1.0.gem
30
36
  homepage: https://github.com/bankair/gegene
31
37
  licenses:
32
38
  - MIT
@@ -47,7 +53,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
47
53
  version: '0'
48
54
  requirements: []
49
55
  rubyforge_project:
50
- rubygems_version: 2.1.10
56
+ rubygems_version: 2.6.5
51
57
  signing_key:
52
58
  specification_version: 4
53
59
  summary: Genetic algorithm helpers