gene_genie 0.0.1 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 1f634d03e4f8bce6759898a2048719471aff1e0b
4
- data.tar.gz: ff4ebfd191e17b182149935e1c544b51ceb0f46f
2
+ SHA256:
3
+ metadata.gz: ea71d46d33da67b5dff6976fbcf4aa6dfd73eecd0fd1c2a0fce2fe70ff02de52
4
+ data.tar.gz: 18034001ed08324f708e0395d1970aae79880f24949c1e954b453737333b04b5
5
5
  SHA512:
6
- metadata.gz: a918e63beb5265d671b2e4b46b28358fb7e57727c590f9b37300eae2b0b57fc7d19abd30b90d0d0a5fea815c6a98bbf1ddb2e65e0b0a2f1e9915fbe88467fa21
7
- data.tar.gz: 86719523c06e1fd0e35c242385a833c236745d3fe43973494d15cdab567bb43467f34bfda9211f5db1d6f1e00f820109c18f66139115e1252875357e0b2c5346
6
+ metadata.gz: 19139fef4bb51b4ca40d6f949f716bba442b5fc3490ccc26c0cd555fe0eb0db09ad4bdb8366a1562b63bdc9072a4425bd71989a3af0c4cb7fd22f9ddc247eb78
7
+ data.tar.gz: 1c7686c788280f0cf40f8abfdb2fee8f267e1b5cbfc087b913042553cde7245fc2f24ce9778c481bee6b19b03e99483add0f1f9d5484be4542f80ab5d318cd47
data/README.md CHANGED
@@ -1,13 +1,12 @@
1
- [![Build Status](https://travis-ci.org/MEHColeman/gene_genie.svg?branch=master)](https://travis-ci.org/MEHColeman/gene_genie)
2
1
  [![Gem Version](https://badge.fury.io/rb/gene_genie.svg)](http://badge.fury.io/rb/gene_genie)
3
2
  [![Code Climate](https://codeclimate.com/github/MEHColeman/gene_genie.png)](https://codeclimate.com/github/MEHColeman/gene_genie)
4
3
 
5
4
  # Gene Genie
6
5
 
7
6
  Hey, I wrote a genetic algorithm gem. Goals:
8
- * Have fun
9
- * Be easy and intuitive to use
10
- * Be open to extension and experimentation
7
+ * Have fun
8
+ * Be easy and intuitive to use
9
+ * Be open to extension and experimentation
11
10
 
12
11
  ## Installation
13
12
 
@@ -24,31 +23,105 @@ Or install it yourself as:
24
23
  $ gem install gene_genie
25
24
 
26
25
  ## Usage
27
- Basic usage is designed to be as simple as possible. You provide two things: an exemplar and an evaluator.
28
- An exemplar is a list of variables along with their possible range of values.
26
+ Basic usage is designed to be as simple as possible. You provide two things: a
27
+ template and an evaluator.
28
+
29
+ A template is an array of hashes, representing a list of variables along with
30
+ their possible range of values that you wish to optimise.
31
+
29
32
  An evaluator implements a fitness method that returns a numeric value.
30
- The genetic algorithm will then search for the set of values that maximises the fitness.
31
33
 
32
- ```ruby
34
+ The genetic algorithm will then search for the set of values that maximises the
35
+ fitness.
36
+
37
+ ~~~ruby
33
38
  require 'gene_genie'
34
39
 
35
- exemplar = {
40
+ template = [{
36
41
  range_of_ints: 1..10,
42
+ more_ints: 3..100
43
+ }]
44
+ ~~~
45
+
46
+ <!---
37
47
  range_of_floats: 1.0..4.5,
38
48
  set_of_items: [:apple, :banana, :orange],
39
49
  ordered_set_of_items: [:one, :two, :three],
40
50
  circular_ordered_set: [:early_morning, :morning, :noon, :afternoon,
41
51
  :evening, :midnight]
42
- }
43
- ```
44
-
45
- If you use the simple genie interface, the genetic algorithm will come up with a reasonable best-guesses for various algorthm parameters, but you can dive under the covers to give yourself more flexibility.
52
+ -->
53
+ Typically, your fitness function will create a model represented by the values
54
+ specified in the template, evaluate the performance of that model, and return a
55
+ fitness score. But, it can be as complicated or simple as you need.
56
+
57
+ A fitness function should return a float or integer
58
+ ~~~ruby
59
+ class Summer
60
+ def fitness(params)
61
+ params.inject(0) {|acc, values| acc + values.each_value.inject(&:+)}
62
+ end
63
+ end
64
+ ~~~
65
+ Then, simply create a Genie, and optimise:
66
+ ~~~ruby
67
+ genie = GeneGenie::Genie.new(template, Summer.new)
68
+ genie.optimise
69
+
70
+ puts genie.best.inspect
71
+ ~~~
72
+ If you want to monitor progress of the optimisation algorithm, you can register
73
+ a listener:
74
+ ~~~ruby
75
+ genie.register_listener(Proc.new do |g|
76
+ puts "Best score: '#{g.best.to_hashes.to_s}', Score: #{g.best_fitness}"
77
+ end)
78
+ ~~~
79
+ See the examples directory for more details.
80
+
81
+ If you use the simple `genie` interface, the genetic algorithm will come up with
82
+ reasonable best-guesses for various algorithm parameters, but you can dive under
83
+ the covers to give yourself more flexibility.
46
84
  * Population size
47
85
  * Gene pools
48
- * Initialisation
86
+ * Initialization
49
87
  * Optimisation Criteria
50
88
 
51
- Custom objects for crossover, gene selection, etc.
89
+
90
+ ## Advanced Use
91
+
92
+ If you want more control over your algorithm, you can skip the `Genie`, and use
93
+ `GenePool` directly.
94
+
95
+ This allows you to create objects that are used to control the methods used for
96
+ mutation, crossover, gene selection, gene pool parameters like population size
97
+ and convergence criteria.
98
+ ~~~ruby
99
+ gene_mutator = CustomMutator.new(gm_args)
100
+ gene_factory = CustomGeneFactory.new(gf_args)
101
+
102
+ template_evaluator = CustomTemplateEvaluator.new(template)
103
+ size = template_evaluator.recommended_size
104
+
105
+ GenePool.new(template: template,
106
+ fitness_evaluator: fitness_evaluator,
107
+ gene_factory: gene_factory,
108
+ size: size,
109
+ mutator: gene_mutator)
110
+ ~~~
111
+ The `mutator` operates on a `Gene` to alter it slightly, mimicking natural gene
112
+ mutations.
113
+
114
+ The `gene_factory` creates a population of genes of a given size. Genes are
115
+ typically generated randomly across the parameter space, but this allows you to
116
+ have more control over how genes are distributed across this space.
117
+
118
+ The `template_evaluator` is used to provide other configuration options to the
119
+ GenePool, such as recommended size.
120
+
121
+ ---
122
+ Note:
123
+ Due to the non-deterministic nature of the algorithm, some of the tests don't
124
+ pass every time at the moment! This is a known issue.
52
125
 
53
126
  ## Contributing
54
127
 
@@ -0,0 +1,34 @@
1
+ module GeneGenie
2
+ module Combiner
3
+ # Picks alleles from each Gene randomly
4
+ class OnePointCombiner
5
+ def call(gene_a, gene_b)
6
+ if rand >= 0.5
7
+ first_gene = gene_a
8
+ second_gene = gene_b
9
+ else
10
+ first_gene = gene_b
11
+ second_gene = gene_a
12
+ end
13
+ first_gene_hashes = first_gene.to_hashes
14
+ second_gene_hashes = second_gene.to_hashes
15
+
16
+ total_length = first_gene_hashes.map(&:size).reduce(:+)
17
+ crossover_point = rand(0..(total_length - 1))
18
+
19
+ count = 0
20
+ new_information = first_gene_hashes.map.with_index do |part, index|
21
+ new_hash = {}
22
+ part.each do |k, v|
23
+ new_hash[k] = (count >= crossover_point) ? second_gene_hashes[index][k] : v
24
+ count += 1
25
+ end
26
+ new_hash
27
+ end
28
+ Gene.new(information: new_information,
29
+ fitness_evaluator: first_gene.fitness_evaluator,
30
+ gene_combiner: self)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,21 @@
1
+ module GeneGenie
2
+ module Combiner
3
+ # Picks alleles from each Gene randomly
4
+ class UniformCombiner
5
+ def call(first_gene, second_gene)
6
+ first_gene_hashes = first_gene.to_hashes
7
+ second_gene_hashes = second_gene.to_hashes
8
+ new_information = first_gene_hashes.map.with_index do |part, index|
9
+ new_hash = {}
10
+ part.each do |k, v|
11
+ new_hash[k] = (rand > 0.5) ? v : second_gene_hashes[index][k]
12
+ end
13
+ new_hash
14
+ end
15
+ Gene.new(information: new_information,
16
+ fitness_evaluator: first_gene.fitness_evaluator,
17
+ gene_combiner: self)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,26 @@
1
+ module GeneGenie
2
+ module Combiner
3
+ # Creates new allele value by creating a (random) weighted
4
+ # average of the two parent genes. Good for genes that represent numeric
5
+ # scalar values, but not for genes representing discrete info.
6
+ class WeightedAverageCombiner
7
+ def call(first_gene, second_gene)
8
+ first_gene_hashes = first_gene.to_hashes
9
+ second_gene_hashes = second_gene.to_hashes
10
+ new_information = first_gene_hashes.map.with_index do |part, index|
11
+ new_hash = {}
12
+ part.each do |k, v|
13
+ p_first = rand(0.0..100.0)
14
+ p_second = 100 - p_first
15
+ new_hash[k] = (((p_first * v) +
16
+ (p_second * second_gene_hashes[index][k]))/100).round
17
+ end
18
+ new_hash
19
+ end
20
+ Gene.new(information: new_information,
21
+ fitness_evaluator: first_gene.fitness_evaluator,
22
+ gene_combiner: self)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -1,15 +1,23 @@
1
+ require_relative 'combiner/one_point_combiner'
2
+
1
3
  module GeneGenie
2
4
  # A Gene is the basic unit of the genetic algorithm. Genes hold the
3
5
  # information used to evaluate their fitness.
4
6
  # They are combined into new Genes during the optimisation process.
5
7
  # @since 0.0.1
6
8
  class Gene
7
- def initialize(information, fitness_evaluator)
9
+ def initialize(information:,
10
+ fitness_evaluator:,
11
+ gene_combiner: GeneGenie::Combiner::OnePointCombiner.new)
12
+ fail ArgumentError, 'information must be Array' unless information.kind_of? Array
13
+ fail ArgumentError, 'information must be Array of Hashes' unless information[0].kind_of? Hash
14
+
8
15
  @information = information
9
16
  @fitness_evaluator = fitness_evaluator
17
+ @combiner = gene_combiner
10
18
  end
11
19
 
12
- def to_hash
20
+ def to_hashes
13
21
  @information
14
22
  end
15
23
 
@@ -17,22 +25,27 @@ module GeneGenie
17
25
  @fitness ||= @fitness_evaluator.fitness(@information)
18
26
  end
19
27
 
28
+ def fitness_evaluator
29
+ @fitness_evaluator
30
+ end
31
+
32
+ def normalised_fitness(minimum)
33
+ @normalised_fitness ||= fitness - minimum
34
+ end
35
+
20
36
  def mutate(mutator)
21
37
  @information = mutator.call @information
38
+ @fitness = nil
39
+ @normalised_fitness = nil
22
40
  self
23
41
  end
24
42
 
25
43
  def combine(other_gene)
26
- other_gene_hash = other_gene.to_hash
27
- new_hash = {}
28
- @information.each do | k, v |
29
- new_hash[k] = (rand > 0.5) ? @information[k] : other_gene_hash[k]
30
- end
31
- Gene.new(new_hash, @fitness_evaluator)
44
+ @combiner.call(self, other_gene)
32
45
  end
33
46
 
34
- def <=>(gene)
35
- fitness <=> gene.fitness
47
+ def <=>(other)
48
+ fitness <=> other.fitness
36
49
  end
37
50
  end
38
51
  end
@@ -1,4 +1,5 @@
1
1
  require_relative 'gene'
2
+ require_relative 'combiner/weighted_average_combiner'
2
3
 
3
4
  module GeneGenie
4
5
  # GeneFactory
@@ -7,29 +8,36 @@ module GeneGenie
7
8
  # The default implementation will produce random genes, but other approaches
8
9
  # could be taken.
9
10
  class GeneFactory
10
- def initialize(template, fitness_evaluator)
11
+ def initialize(template, fitness_evaluator, gene_combiner=nil)
11
12
  @template = template
12
13
  @fitness_evaluator = fitness_evaluator
14
+ @combiner = gene_combiner || GeneGenie::Combiner::WeightedAverageCombiner.new
13
15
  end
14
16
 
15
17
  def create(size = 1)
16
18
  genes = []
17
19
  size.times do
18
- hash = create_hash_from_template
19
- genes << Gene.new(hash, @fitness_evaluator)
20
+ genes << create_gene_from_template
20
21
  end
21
-
22
22
  genes
23
23
  end
24
24
 
25
25
  private
26
26
 
27
- def create_hash_from_template
27
+ def create_gene_from_template
28
+ gene_array = @template.map do |part|
29
+ create_hash_from_template_part(part)
30
+ end
31
+ Gene.new(information: gene_array,
32
+ fitness_evaluator: @fitness_evaluator,
33
+ gene_combiner: @combiner)
34
+ end
35
+
36
+ def create_hash_from_template_part(part)
28
37
  new_hash = {}
29
- @template.each do |k, v|
38
+ part.each do |k, v|
30
39
  new_hash[k] = rand(v)
31
40
  end
32
-
33
41
  new_hash
34
42
  end
35
43
  end
@@ -1,13 +1,19 @@
1
1
  require_relative 'gene_factory'
2
- require_relative 'mutator/simple_gene_mutator'
2
+ require_relative 'mutator/nudge_mutator'
3
3
  require_relative 'mutator/null_mutator'
4
+ require_relative 'selector/proportional_selector'
5
+ require_relative 'template_evaluator'
4
6
 
5
7
  module GeneGenie
6
8
  class GenePool
7
- def initialize(template, fitness_evaluator, gene_factory,
8
- mutator = NullMutator.new)
9
- unless template.instance_of? Hash
10
- fail ArgumentError, 'template must be a hash of ranges'
9
+ def initialize(template:,
10
+ fitness_evaluator:,
11
+ gene_factory:,
12
+ size: 10,
13
+ mutator: NullMutator.new,
14
+ selector: ProportionalSelector.new)
15
+ unless (template.instance_of? Array) && (template[0].instance_of? Hash)
16
+ fail ArgumentError, 'template must be an array of hashes of ranges'
11
17
  end
12
18
  unless fitness_evaluator.respond_to?(:fitness)
13
19
  fail ArgumentError, 'fitness_evaluator must respond to fitness'
@@ -16,19 +22,32 @@ module GeneGenie
16
22
  @template = template
17
23
  @fitness_evaluator = fitness_evaluator
18
24
  @mutator = mutator
19
-
20
- #size = template_evaluator.recommended_size
21
- size ||= 10
25
+ @selector = selector
22
26
  @pool = gene_factory.create(size)
27
+ @generation = 0
28
+ @listeners = []
23
29
  end
24
30
 
25
31
  # build a GenePool with a reasonable set of defaults.
26
32
  # You only need to specily the minimum no. of parameters
27
33
  def self.build(template, fitness_evaluator)
28
- gene_mutator = SimpleGeneMutator.new(template)
34
+ unless (template.instance_of? Array) && (template[0].instance_of? Hash)
35
+ fail ArgumentError, 'template must be an array of hashes of ranges'
36
+ end
37
+ gene_mutator = NudgeMutator.new(template, 0.01)
29
38
  gene_factory = GeneFactory.new(template, fitness_evaluator)
30
- GenePool.new(template, fitness_evaluator, gene_factory,
31
- gene_mutator)
39
+
40
+ template_evaluator = TemplateEvaluator.new(template)
41
+ size = template_evaluator.recommended_size
42
+ GenePool.new(template: template,
43
+ fitness_evaluator: fitness_evaluator,
44
+ gene_factory: gene_factory,
45
+ size: size,
46
+ mutator: gene_mutator)
47
+ end
48
+
49
+ def register_listener(listener)
50
+ @listeners << listener
32
51
  end
33
52
 
34
53
  def size
@@ -36,46 +55,88 @@ module GeneGenie
36
55
  end
37
56
 
38
57
  def best
39
- @pool.max_by { |gene| gene.fitness }
58
+ @best ||= @pool.max_by(&:fitness)
59
+ end
60
+
61
+ def best_fitness
62
+ best.fitness
63
+ end
64
+
65
+ def worst
66
+ @worst ||= @pool.min_by(&:fitness)
67
+ end
68
+
69
+ def worst_fitness
70
+ worst.fitness
71
+ end
72
+
73
+ def best_ever
74
+ @best_ever ||= best
40
75
  end
41
76
 
42
77
  def evolve
43
78
  old_best_fitness = best.fitness
44
79
  new_pool = []
45
80
  size.times do
46
- first_gene, second_gene = select_genes
47
- new_gene = combine_genes(first_gene, second_gene)
48
- new_pool << new_gene.mutate(@mutator)
81
+ new_pool << select_genes_combine_and_mutate
49
82
  end
50
83
  @pool = new_pool
84
+ update_stats
85
+ @generation += 1
86
+
87
+ @listeners.each { |l| l.call(self) }
88
+
51
89
  best.fitness > old_best_fitness
52
90
  end
53
91
 
92
+ def generation
93
+ @generation
94
+ end
95
+
96
+ def average_fitness
97
+ @average_fitness ||= total_fitness / @pool.size
98
+ end
99
+
100
+ def total_fitness
101
+ @total_fitness ||= fitness_values.reduce(:+)
102
+ end
103
+
104
+ def total_normalised_fitness
105
+ @total_normalised_fitness ||= normalised_fitness_values.reduce(:+)
106
+ end
107
+
108
+ def genes
109
+ @pool
110
+ end
111
+
54
112
  private
55
- # a very simple selection - pick by sorted order
56
- # pick two different genes
113
+
114
+ def update_stats
115
+ @best = nil
116
+ @worst = nil
117
+ @total_fitness = nil
118
+ @total_normalised_fitness = nil
119
+ @average_fitness = nil
120
+
121
+ @best_ever = best if best.fitness > best_ever.fitness
122
+ end
123
+
57
124
  def select_genes
58
- selectees = @pool.sort.reverse
59
- first, second = nil, nil
60
- probability = [(( 1.0/size ) * 3), 0.8].min
61
- while !first || !second do
62
- selectees.each do |s|
63
- if rand < probability
64
- selectees.delete(s)
65
- if !first
66
- first = s
67
- break
68
- else
69
- second = s
70
- end
71
- end
72
- end
73
- end
74
- [first, second]
125
+ @selector.call(self)
126
+ end
127
+
128
+ def fitness_values
129
+ @pool.map(&:fitness)
130
+ end
131
+
132
+ def normalised_fitness_values
133
+ @pool.map{ |gene| gene.normalised_fitness(worst_fitness) }
75
134
  end
76
135
 
77
- def combine_genes(first, second)
78
- first.combine(second)
136
+ def select_genes_combine_and_mutate
137
+ first_gene, second_gene = select_genes
138
+ new_gene = first_gene.combine(second_gene)
139
+ new_gene.mutate(@mutator)
79
140
  end
80
141
  end
81
142
  end
@@ -1,15 +1,17 @@
1
1
  require_relative 'gene_pool'
2
+ require_relative 'listener/logging_listener'
2
3
 
3
4
  # Namespace for GeneGenie genetic algorithm optimisation gem
4
5
  # @since 0.0.1
5
6
  module GeneGenie
6
-
7
7
  # Top level, basic interface for GA optimisation.
8
8
  # Genie will attempt to optimise based on best-guess defaults if none are
9
9
  # provided
10
+ # Genie is basically a wrapper around GenePool that lets you get going as
11
+ # quickly as possible by providing a reasonable set of defaults.
12
+ # For more control and customisation, go straight to using GenePoo
10
13
  # @since 0.0.1
11
14
  class Genie
12
-
13
15
  DEFAULT_NO_OF_GENERATIONS = 50
14
16
  IMPROVEMENT_THRESHOLD = 0.1 # %
15
17
 
@@ -33,34 +35,37 @@ module GeneGenie
33
35
  end
34
36
 
35
37
  @best_fitness = @fitness_evaluator.fitness(best)
36
-
37
38
  @best_fitness > previous_best
38
39
  end
39
40
  alias_method :optimize, :optimise
40
41
 
42
+ def register_listener(listener)
43
+ @gene_pool.register_listener(listener)
44
+ end
45
+
41
46
  def best
42
- @gene_pool.best.to_hash
47
+ @gene_pool.best_ever.to_hashes
43
48
  end
44
49
 
45
50
  def best_fitness
46
- @gene_pool.best.fitness
51
+ @gene_pool.best_ever.fitness
47
52
  end
48
53
 
49
54
  private
55
+
50
56
  def evolve_n_times(n)
51
57
  n.times { @gene_pool.evolve }
52
58
  end
53
59
 
54
60
  def optimise_by_strategy
55
61
  DEFAULT_NO_OF_GENERATIONS.times do
56
- current_fitness = best_fitness
57
62
  @gene_pool.evolve
58
63
  end
59
64
  DEFAULT_NO_OF_GENERATIONS.times do
60
65
  current_fitness = best_fitness
61
66
  @gene_pool.evolve
62
- break if best_fitness < current_fitness *
63
- (1 + (IMPROVEMENT_THRESHOLD / 100 ))
67
+ break if (best_fitness < current_fitness *
68
+ (1 + (IMPROVEMENT_THRESHOLD / 100)) && best_fitness > 0)
64
69
  end
65
70
  end
66
71
  end
@@ -0,0 +1,18 @@
1
+ module GeneGenie
2
+ module Listener
3
+ class LoggingListener
4
+ def initialize(logger)
5
+ @logger = logger
6
+ @last_time = Time.now
7
+ end
8
+
9
+ def call(pool)
10
+ @logger.info "Pool Generation ##{pool.generation}"
11
+ @logger.info "Average Fitness: #{pool.average_fitness}"
12
+ @logger.info "Best Fitness: #{pool.best_fitness}"
13
+ @logger.info "Time elapsed: #{Time.now - @last_time}"
14
+ @last_time = Time.now
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,29 @@
1
+ module GeneGenie
2
+ # A NudgeMutator is very similar to a simple nutator, except that it will
3
+ # only change a value by a small amount, rather than to any valid amount.
4
+ # So, an allele with a rather specified in the template of 1..100, with a
5
+ # current value of 50 might change in the range 45..55 instead of 1..100.
6
+ # @since 0.2.0
7
+ class NudgeMutator
8
+ def initialize(template, mutation_rate = 0.04)
9
+ @template = template
10
+ @mutation_rate = mutation_rate * 1
11
+ end
12
+
13
+ def call(genes)
14
+ genes.each_with_index do |hash, index|
15
+ hash.each do |k, v|
16
+ if rand < @mutation_rate
17
+ nudge_max = (@template[index][k].size * 0.05).ceil
18
+ hash[k] = rand(
19
+ [@template[index][k].min, (v - nudge_max).floor].max..
20
+ [@template[index][k].max, (v + nudge_max).ceil].min
21
+ )
22
+ end
23
+ end
24
+ end
25
+ genes
26
+ end
27
+ end
28
+ end
29
+
@@ -9,13 +9,15 @@ module GeneGenie
9
9
  @mutation_rate = mutation_rate
10
10
  end
11
11
 
12
- def call(hash)
13
- hash.each do |k, v|
14
- if rand < @mutation_rate
15
- hash[k] = rand(@template[k])
12
+ def call(genes)
13
+ genes.each_with_index do |hash, index|
14
+ hash.each do |k, v|
15
+ if rand < @mutation_rate
16
+ hash[k] = rand(@template[index][k])
17
+ end
16
18
  end
17
19
  end
18
- hash
20
+ genes
19
21
  end
20
22
  end
21
23
  end
@@ -0,0 +1,31 @@
1
+ module GeneGenie
2
+ # A simple gene selection algorithm.
3
+ # Genes are ordered by score.
4
+ # Effectively, a loaded coin is tossed. If it's heads, the top gene is
5
+ # selected, otherwise, continue down the list until a head comes up.
6
+ # The coin is loaded, so that there is a reasonable spread throughout the
7
+ # gene population, but the better the gene, the more likely it is to turn up.
8
+ class CoinFlipSelector
9
+ def call(pool)
10
+ # a very simple selection - pick by sorted order
11
+ # pick two different genes
12
+ selectees = pool.genes.sort.reverse
13
+ first, second = nil, nil
14
+ probability = [((1.0 / pool.size) * 3), 0.8].min
15
+ while !first || !second do
16
+ selectees.each do |s|
17
+ if rand < probability
18
+ selectees.delete(s)
19
+ if !first
20
+ first = s
21
+ break
22
+ else
23
+ second = s
24
+ end
25
+ end
26
+ end
27
+ end
28
+ [first, second]
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,20 @@
1
+ module GeneGenie
2
+ # A proportional gene selection algorithm.
3
+ # Genes are picked in proportion to thier normalised score.
4
+ class ProportionalSelector
5
+ def call(pool)
6
+ [pick_one(pool), pick_one(pool)]
7
+ end
8
+
9
+ private
10
+
11
+ def pick_one(pool)
12
+ proportional_index = rand(pool.total_normalised_fitness)
13
+ total = 0
14
+ pool.genes.each_with_index do |gene, index|
15
+ total += gene.normalised_fitness(pool.worst_fitness)
16
+ return gene if total >= proportional_index || index == (pool.size - 1)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,47 @@
1
+ module GeneGenie
2
+ # A Template Evaluator provides certain analysis and useful information
3
+ # about templates.
4
+ # A template is always treated internally as an Array of Hashes.
5
+ # @since 0.0.2
6
+ class TemplateEvaluator
7
+ def initialize(template)
8
+ @template = template
9
+ end
10
+
11
+ def permutations
12
+ @permutations ||= @template.map { |hash|
13
+ hash.map { |_, v| v.size }.reduce(:*)
14
+ }.reduce(:*)
15
+ end
16
+
17
+ # Suggests a recommended GenePool size.
18
+ # returns a minimum of 6 unless the total number of permutations
19
+ # is below that.
20
+ # Otherwise, returns 1/1000th of the number of permutations up to a
21
+ # maximum of 20000
22
+ def recommended_size
23
+ [
24
+ [((Math.log(permutations))**2).ceil, 20000].min,
25
+ [6, permutations].min,
26
+ 3
27
+ ].max
28
+ end
29
+
30
+ # Verifies that the given hash conforms to the constraints specified in the
31
+ # hash template
32
+ def hash_valid?(hash_under_test)
33
+ begin
34
+ @template.each_with_index do |h, index|
35
+ h.each do |k, v|
36
+ return false unless hash_under_test[index][k]
37
+ return false unless hash_under_test[index][k] >= v.min
38
+ return false unless hash_under_test[index][k] <= v.max
39
+ end
40
+ end
41
+ rescue
42
+ return false
43
+ end
44
+ return true
45
+ end
46
+ end
47
+ end
@@ -1,3 +1,3 @@
1
1
  module GeneGenie
2
- VERSION = '0.0.1'
2
+ VERSION = '0.1.1'
3
3
  end
metadata CHANGED
@@ -1,31 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gene_genie
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mark Coleman
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-08-28 00:00:00.000000000 Z
11
+ date: 2022-10-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - "~>"
18
- - !ruby/object:Gem::Version
19
- version: '1.6'
20
- type: :development
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - "~>"
25
- - !ruby/object:Gem::Version
26
- version: '1.6'
27
- - !ruby/object:Gem::Dependency
28
- name: rake
29
15
  requirement: !ruby/object:Gem::Requirement
30
16
  requirements:
31
17
  - - ">="
@@ -39,7 +25,7 @@ dependencies:
39
25
  - !ruby/object:Gem::Version
40
26
  version: '0'
41
27
  - !ruby/object:Gem::Dependency
42
- name: minitest
28
+ name: rake
43
29
  requirement: !ruby/object:Gem::Requirement
44
30
  requirements:
45
31
  - - ">="
@@ -53,7 +39,7 @@ dependencies:
53
39
  - !ruby/object:Gem::Version
54
40
  version: '0'
55
41
  - !ruby/object:Gem::Dependency
56
- name: minitest-spec
42
+ name: rspec
57
43
  requirement: !ruby/object:Gem::Requirement
58
44
  requirements:
59
45
  - - ">="
@@ -66,8 +52,7 @@ dependencies:
66
52
  - - ">="
67
53
  - !ruby/object:Gem::Version
68
54
  version: '0'
69
- description: JUST A PROTOTYPE WORK IN PROGRESS! Optimise anything that responds to
70
- 'fitness' and takes a hash
55
+ description: Optimise anything that responds to 'fitness' and takes a hash
71
56
  email:
72
57
  - m@rkcoleman.co.uk
73
58
  executables: []
@@ -76,18 +61,26 @@ extra_rdoc_files: []
76
61
  files:
77
62
  - README.md
78
63
  - lib/gene_genie.rb
64
+ - lib/gene_genie/combiner/one_point_combiner.rb
65
+ - lib/gene_genie/combiner/uniform_combiner.rb
66
+ - lib/gene_genie/combiner/weighted_average_combiner.rb
79
67
  - lib/gene_genie/gene.rb
80
68
  - lib/gene_genie/gene_factory.rb
81
69
  - lib/gene_genie/gene_pool.rb
82
70
  - lib/gene_genie/genie.rb
71
+ - lib/gene_genie/listener/logging_listener.rb
72
+ - lib/gene_genie/mutator/nudge_mutator.rb
83
73
  - lib/gene_genie/mutator/null_mutator.rb
84
74
  - lib/gene_genie/mutator/simple_gene_mutator.rb
75
+ - lib/gene_genie/selector/coin_flip_selector.rb
76
+ - lib/gene_genie/selector/proportional_selector.rb
77
+ - lib/gene_genie/template_evaluator.rb
85
78
  - lib/gene_genie/version.rb
86
79
  homepage: https://github.com/MEHColeman/gene_genie
87
80
  licenses:
88
81
  - MIT
89
82
  metadata: {}
90
- post_install_message:
83
+ post_install_message:
91
84
  rdoc_options: []
92
85
  require_paths:
93
86
  - lib
@@ -95,16 +88,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
95
88
  requirements:
96
89
  - - ">="
97
90
  - !ruby/object:Gem::Version
98
- version: '0'
91
+ version: 2.0.0
99
92
  required_rubygems_version: !ruby/object:Gem::Requirement
100
93
  requirements:
101
94
  - - ">="
102
95
  - !ruby/object:Gem::Version
103
96
  version: '0'
104
97
  requirements: []
105
- rubyforge_project:
106
- rubygems_version: 2.4.5
107
- signing_key:
98
+ rubygems_version: 3.1.6
99
+ signing_key:
108
100
  specification_version: 4
109
101
  summary: Genetic algorithm optimisation gem
110
102
  test_files: []