gene_genie 0.0.1 → 0.1.1

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
- 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: []