darwinning 0.0.3 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -3,7 +3,7 @@ source "http://rubygems.org"
3
3
  gemspec
4
4
 
5
5
  group :test do
6
- gem 'rspec', '~> 2.14.0'
6
+ gem 'rspec'
7
7
  end
8
8
 
9
9
  group :development, :test do
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  Darwinning
2
2
  ==========
3
- [![Gem Version](https://badge.fury.io/rb/darwinning.png)](http://badge.fury.io/rb/darwinning)
3
+ [![Gem Version](https://badge.fury.io/rb/darwinning.svg)](http://badge.fury.io/rb/darwinning)
4
4
 
5
5
  [gem]: https://rubygems.org/gems/darwinning
6
6
 
@@ -13,99 +13,116 @@ Installation
13
13
  gem install darwinning
14
14
  ```
15
15
 
16
- Examples
16
+ Usage
17
17
  --------
18
18
 
19
- ### Fifteen
19
+ There are two ways of using Darwinning. You can either start with a class that is a subclass of `Darwinning::Organism` or you can include `Darwinning` in an existing class that you would like to make evolveable.
20
+
21
+ ### Include Style:
22
+
23
+ *This style is useful if you want to take existing objects and use Darwinning on them when needed.*
20
24
 
21
25
  Here's an dumb example of how you might use Darwinning to solve a pointless problem:
22
26
 
23
- Let's say for some reason you need a set of 3 number that add up to 15. This is a strange problem to have, but let's solve it anyway.
27
+ Let's say for some reason you need a set of 3 numbers (within a range of 0-100) that add up to 100. This is a strange problem to have, but let's solve it anyway.
24
28
 
25
29
  ```ruby
26
- class Triple < Darwinning::Organism
30
+ require 'darwinning'
27
31
 
28
- @name = "Triple"
29
- @genes = [
30
- Darwinning::Gene.new(name: "first digit", value_range: (0..9)),
31
- Darwinning::Gene.new(name: "second digit", value_range: (0..9)),
32
- Darwinning::Gene.new(name: "third digit", value_range: (0..9))
33
- ]
34
-
35
- def fitness
36
- # Try to get the sum of the 3 digits to add up to 15
37
- (genotypes.inject{ |sum, x| sum + x } - 15).abs
38
- end
39
- end
40
-
41
- p = Darwinning::Population.new(
42
- organism: Triple, population_size: 10,
43
- fitness_goal: 0, generations_limit: 100
44
- )
45
- p.evolve!
32
+ class Triple
33
+ include Darwinning
46
34
 
47
- p.best_member.nice_print # prints the member representing the solution
48
- ```
35
+ GENE_RANGES = {
36
+ first_number: (0..100),
37
+ second_number: (0..100),
38
+ third_number: (0..100)
39
+ }
49
40
 
50
- This code declares an organism class that inherits from Darwinning's Organism parent class to represent solutions. Then we create a population of these solution organisms and evolve the population until a solution meets the fitness threshold or the generation limit is met.
41
+ attr_accessor :first_number, :second_number, :third_number
51
42
 
52
- ### Cookies
43
+ def fitness
44
+ # Try to get the sum of the 3 digits to add up to 100
45
+ (first_number + second_number + third_number - 100).abs
46
+ end
47
+ end
48
+ ```
53
49
 
54
- Or let's say you want to find the perfect chocolate chip cookie recipie. Sure you could ask your grandmother, but why not let a genetic algorithm do all the work for you? Some baking may be required for this one.
50
+ In order to use Darwinning this way you must:
51
+ - `include Darwinning` in your class
52
+ - define a `GENE_RANGES` constant with your relivant value names as keys
53
+ - define `attr_accessor`s for all your values
54
+ - define a `fitness` method
55
55
 
56
- Define a cookie Organism class, generate an initial population, bake a batch of each and have your friends rate each batch. Use that rating as the fitness value for each recipie and then generate the next generation of cookie recipies. Repeat until you have optimized the recipie or you are sick from eating too many cookies.
56
+ Once you have your organism class that includes Darwinning, you can create a population and evolve it:
57
57
 
58
58
  ```ruby
59
- class Cookie < Darwinning::Organism
60
-
61
- @name = "Chocolate Chip Cookie"
62
- @genes = [
63
- Darwinning::Gene.new(name: "white sugar", value_range: (0..1), units: "cup"),
64
- Darwinning::Gene.new(name: "brown sugar", value_range: (0..1), units: "cup"),
65
- Darwinning::Gene.new(name: "flour", value_range: (0..3), units: "cup"),
66
- Darwinning::Gene.new(name: "eggs", value_range: (0..3)),
67
- Darwinning::Gene.new(name: "baking powder", value_range: (0..2), units: "teaspoon"),
68
- Darwinning::Gene.new(name: "salt", value_range: (0..2), units: "teaspoon"),
69
- Darwinning::Gene.new(name: "butter", value_range: (0..2), units: "cup"),
70
- Darwinning::Gene.new(name: "vanilla extract", value_range: (0..2), units: "teaspoon"),
71
- Darwinning::Gene.new(name: "chocolate chips", value_range: (0..20), units: "ounce"),
72
- Darwinning::Gene.new(name: "oven temp", value_range: (300..400), units: "degrees F"),
73
- Darwinning::Gene.new(name: "cook time", value_range: (5..20), units: "minute")
74
- ]
59
+ if Triple.is_evolveable?
60
+ triple_pop = Triple.build_population(0, 10, 100)
61
+ triple_pop.evolve! # evolve until fitness goal is or generations limit is met
75
62
 
63
+ pp "Best member: #{triple_pop.best_member}"
76
64
  end
65
+ ```
77
66
 
78
- p = Darwinning::Population.new(
79
- organism: Cookie, population_size: 10,
80
- fitness_goal: 5, generations_limit: 100
81
- )
67
+ ### Inheritance Style:
82
68
 
83
- first_gen_ratings = [1.5, 4, 3, 3.5, 2, 1, 1.5, 3, 2.5, 0.5]
84
- p.set_members_fitness!(first_gen_ratings)
69
+ *This style is good when you just want to set up some quick objects to only use in Darwinning tasks.*
85
70
 
86
- p.make_next_generation!
71
+ Let's solve the same dumb problem we looked at before...
87
72
 
88
- p.members.each { |m| m.nice_print } # print second generation of cookie recipies
89
- ```
73
+ ```ruby
74
+ require 'darwinning'
90
75
 
91
- ### Binary String Organism
76
+ class Triple < Darwinning::Organism
77
+ @name = "Triple"
78
+ @genes = [
79
+ Darwinning::Gene.new(name: "first digit", value_range: (0..100)),
80
+ Darwinning::Gene.new(name: "second digit", value_range: (0..100)),
81
+ Darwinning::Gene.new(name: "third digit", value_range: (0..100))
82
+ ]
83
+
84
+ def fitness
85
+ # Try to get the sum of the 3 digits to add up to 100
86
+ (genotypes.values.inject { |sum, x| sum + x } - 100).abs
87
+ end
88
+ end
89
+ ```
92
90
 
93
- A simple binary string representation of an organism can be easily created thusly:
91
+ With this `Darwinning::Organism` class, you can now build a population and evolve it:
94
92
 
95
93
  ```ruby
96
- class BinaryOrganism < Darwinning::Organism
94
+ triple_pop = Darwinning::Population.new(
95
+ organism: Triple, population_size: 10,
96
+ fitness_goal: 0, generations_limit: 100
97
+ )
98
+ triple_pop.evolve!
99
+
100
+ pp "Best member: #{triple_pop.best_member}"
101
+ ```
97
102
 
98
- 10.times { |s| @genes << Darwinning::Gene.new("", [0,1]) }
103
+ ### Stepping Through Generations Manually
99
104
 
100
- def fitness
101
- # whatever makes sense here
102
- end
103
- end
105
+ ```ruby
106
+ # pop is a Darwinning::Population of organisms
107
+ puts pop.generation
108
+ # 0
109
+ pop.make_next_generation!
110
+ puts pop.generation
111
+ # 1
112
+
113
+ # you can view a history of population's evolion
114
+ puts pop.history
115
+ # [[gen0_member1, gen0_member2,...], [gen1_member1, gen1_member2,...]]
104
116
  ```
105
117
 
118
+ ### More Examples
119
+
120
+ Check out the `/examples` folder for more examples. That seems like a good place to put examples, right?
121
+
106
122
  ## Built by:
107
123
  * [Dave Schwantes](https://github.com/dorkrawk "dorkrawk")
108
124
 
109
125
  ### With help from:
110
126
  * [Cameron Dutro](https://github.com/camertron "camertron")
111
- * [Maurizio Del Corno](https://github.com/druzn3k "druzn3k")
127
+ * [Maurizio Del Corno](https://github.com/druzn3k "druzn3k")
128
+ * [Evan Brynne](https://github.com/ebrynne "ebrynne")
data/lib/darwinning.rb CHANGED
@@ -1,9 +1,64 @@
1
1
  require_relative 'darwinning/gene'
2
2
  require_relative 'darwinning/organism'
3
- require_relative 'darwinning/evolution_types'
3
+ require_relative 'darwinning/evolution_types/mutation'
4
+ require_relative 'darwinning/evolution_types/reproduction'
4
5
  require_relative 'darwinning/population'
5
6
  require_relative 'darwinning/config'
6
7
 
7
8
  module Darwinning
8
9
  extend Config
9
- end
10
+
11
+ def self.included(base)
12
+ def base.genes
13
+ gene_ranges.map { |k,v| Gene.new(name: k, value_range: v) }
14
+ end
15
+
16
+ def base.build_population(fitness_goal, population_size = 10, generations_limit = 100,
17
+ evolution_types = Population::DEFAULT_EVOLUTION_TYPES)
18
+ Population.new(organism: self, population_size: population_size,
19
+ generations_limit: generations_limit, fitness_goal: fitness_goal,
20
+ evolution_types: evolution_types)
21
+ end
22
+
23
+ def base.is_evolveable?
24
+ has_gene_ranges? &&
25
+ gene_ranges.is_a?(Hash) &&
26
+ gene_ranges.any? &&
27
+ valid_genes? &&
28
+ has_fitness_method?
29
+ end
30
+
31
+ private
32
+
33
+ def base.has_gene_ranges?
34
+ self.constants.include?(:GENE_RANGES)
35
+ end
36
+
37
+ def base.has_fitness_method?
38
+ self.instance_methods.include?(:fitness)
39
+ end
40
+
41
+ def base.gene_ranges
42
+ self::GENE_RANGES
43
+ end
44
+
45
+ def base.valid_genes?
46
+ genes.each do |gene|
47
+ return false unless self.method_defined? gene.name
48
+ end
49
+ true
50
+ end
51
+ end
52
+
53
+ def genes
54
+ self.class.genes
55
+ end
56
+
57
+ def genotypes
58
+ gt = {}
59
+ genes.each do |gene|
60
+ gt[gene] = self.send(gene.name)
61
+ end
62
+ gt
63
+ end
64
+ end
@@ -2,5 +2,6 @@ module Darwinning
2
2
  module Config
3
3
  # crossover mask?
4
4
  # ordered vs weighted fitness
5
+ CROSSOVER_METHOD = :alternating_swap
5
6
  end
6
7
  end
@@ -1,9 +1,16 @@
1
1
  module Darwinning
2
2
  module EvolutionTypes
3
-
4
3
  class Reproduction
5
- def evolve(organism, m1, m2)
6
- sexytimes(organism, m1, m2)
4
+
5
+ # Available crossover_methods:
6
+ # :alternating_swap
7
+ # :random_swap
8
+ def initialize(options = {})
9
+ @crossover_method = options.fetch(:crossover_method, :alternating_swap)
10
+ end
11
+
12
+ def evolve(m1, m2)
13
+ sexytimes(m1, m2)
7
14
  end
8
15
 
9
16
  def pairwise?
@@ -12,23 +19,61 @@ module Darwinning
12
19
 
13
20
  protected
14
21
 
15
- def sexytimes(organism, m1, m2)
16
- genotypes1 = []
17
- genotypes2 = []
22
+ def sexytimes(m1, m2)
23
+ raise "Only organisms of the same type can breed" unless m1.class == m2.class
24
+
25
+ new_genotypes = send(@crossover_method, m1, m2)
26
+
27
+ organism_klass = m1.class
28
+ organism1 = new_member_from_genotypes(organism_klass, new_genotypes.first)
29
+ organism2 = new_member_from_genotypes(organism_klass, new_genotypes.last)
30
+
31
+ [organism1, organism2]
32
+ end
33
+
34
+ def new_member_from_genotypes(organism_klass, genotypes)
35
+ new_member = organism_klass.new
36
+ if organism_klass.superclass.to_s == "Darwinning::Organism"
37
+ new_member.genotypes = genotypes
38
+ else
39
+ new_member.genes.each do |gene|
40
+ new_member.send("#{gene.name}=", genotypes[gene])
41
+ end
42
+ end
43
+ new_member
44
+ end
45
+
46
+ def alternating_swap(m1, m2)
47
+ genotypes1 = {}
48
+ genotypes2 = {}
18
49
 
19
- m1.genotypes.zip(m2.genotypes).each do |g|
20
- if m1.genotypes.index(g[0]) % 2 == 0
21
- genotypes1 << g[0]
22
- genotypes2 << g[1]
50
+ m1.genes.each_with_index do |gene, i|
51
+ if i % 2 == 0
52
+ genotypes1[gene] = m1.genotypes[gene]
53
+ genotypes2[gene] = m2.genotypes[gene]
23
54
  else
24
- genotypes1 << g[1]
25
- genotypes2 << g[0]
55
+ genotypes1[gene] = m2.genotypes[gene]
56
+ genotypes2[gene] = m1.genotypes[gene]
26
57
  end
27
58
  end
28
59
 
29
- [organism.new(genotypes1), organism.new(genotypes2)]
60
+ [genotypes1, genotypes2]
30
61
  end
31
- end
32
62
 
63
+ def random_swap(m1, m2)
64
+ genotypes1 = {}
65
+ genotypes2 = {}
66
+
67
+ m1.genes.each do |gene|
68
+ g1_parent = [m1,m2].sample
69
+ g2_parent = [m1,m2].sample
70
+
71
+ genotypes1[gene] = g1_parent.genotypes[gene]
72
+ genotypes2[gene] = g2_parent.genotypes[gene]
73
+ end
74
+
75
+ [genotypes1, genotypes2]
76
+ end
77
+ end
33
78
  end
34
79
  end
@@ -1,8 +1,6 @@
1
1
  module Darwinning
2
-
3
2
  class Gene
4
- attr_accessor :name, :value_range, :invalid_values, :units
5
- attr_accessor :value
3
+ attr_reader :name, :value_range, :invalid_values, :units, :value
6
4
 
7
5
  def initialize(options = {})
8
6
  @name = options.fetch(:name, '')
@@ -11,6 +9,22 @@ module Darwinning
11
9
  @units = options.fetch(:units, '')
12
10
  end
13
11
 
12
+ def ==(o)
13
+ o.class == self.class &&
14
+ o.name == name &&
15
+ o.value_range == value_range &&
16
+ o.invalid_values == invalid_values &&
17
+ o.units == units
18
+ end
19
+
20
+ def eql?(o)
21
+ o == self
22
+ end
23
+
24
+ def hash
25
+ name.hash ^ value_range.hash ^ invalid_values.hash ^ units.hash ^ value.hash
26
+ end
27
+
14
28
  def express
15
29
  (value_range - invalid_values).sample
16
30
  end
@@ -26,7 +26,6 @@ module ClassLevelInheritableAttributes
26
26
  end
27
27
 
28
28
  module Darwinning
29
-
30
29
  class Organism
31
30
  include ClassLevelInheritableAttributes
32
31
  inheritable_attributes :genes, :name
@@ -35,26 +34,21 @@ module Darwinning
35
34
  @genes = [] # Gene instances
36
35
  @name = ""
37
36
 
38
- def initialize(genotypes = [])
39
- #TODO: catch errors if genotype.length != @genotypes.length
40
- # catch if genotype[x] is not a valid value for @gene[x]
41
-
42
- if genotypes == []
37
+ def initialize(genotypes = {})
38
+ if genotypes == {}
43
39
  # fill genotypes with expressed Genes
44
- @genotypes = self.class.genes.map { |g| g.express } # Gene expressions
40
+ @genotypes = {}
41
+ genes.each do |g|
42
+ # make genotypes a hash with gene objects as keys
43
+ @genotypes[g] = g.express
44
+ end
45
45
  else
46
46
  @genotypes = genotypes
47
47
  end
48
48
 
49
49
  @fitness = -1
50
50
  end
51
-
52
- def nice_print
53
- puts self.class.name == "" ? "[no name]" : self.class.name
54
- self.class.genes.to_enum.each_with_index { |g, i| puts " #{g.name}: #{@genotypes[i]} #{g.units}" }
55
- puts " fitness: #{fitness}"
56
- end
57
-
51
+
58
52
  def name
59
53
  self.class.name
60
54
  end
@@ -1,12 +1,14 @@
1
1
  module Darwinning
2
-
3
2
  class Population
4
- attr_accessor :members, :generations_limit, :fitness_goal
5
- attr_accessor :organism, :generation, :population_size
6
- attr_accessor :evolution_types
3
+
4
+ EPSILON = 0.01
5
+
6
+ attr_reader :members, :generations_limit, :fitness_goal,
7
+ :organism, :population_size, :generation,
8
+ :evolution_types, :history
7
9
 
8
10
  DEFAULT_EVOLUTION_TYPES = [
9
- Darwinning::EvolutionTypes::Reproduction.new,
11
+ Darwinning::EvolutionTypes::Reproduction.new(crossover_method: :alternating_swap),
10
12
  Darwinning::EvolutionTypes::Mutation.new(mutation_rate: 0.10)
11
13
  ]
12
14
 
@@ -18,13 +20,15 @@ module Darwinning
18
20
  @evolution_types = options.fetch(:evolution_types, DEFAULT_EVOLUTION_TYPES)
19
21
  @members = []
20
22
  @generation = 0 # initial population is generation 0
23
+ @history = []
21
24
 
22
25
  build_population(@population_size)
26
+ @history << @members
23
27
  end
24
28
 
25
29
  def build_population(population_size)
26
30
  population_size.times do |i|
27
- @members << organism.new
31
+ @members << build_member
28
32
  end
29
33
  end
30
34
 
@@ -34,14 +38,73 @@ module Darwinning
34
38
  end
35
39
  end
36
40
 
41
+ def set_members_fitness!(fitness_values)
42
+ throw "Invaid number of fitness values for population size" if fitness_values.size != members.size
43
+ members.to_enum.each_with_index { |m, i| m.fitness = fitness_values[i] }
44
+ end
45
+
46
+ def make_next_generation!
47
+ temp_members = members
48
+ new_members = []
49
+
50
+ until new_members.length == members.length
51
+ m1 = weighted_select(members)
52
+ m2 = weighted_select(members)
53
+
54
+ new_members += apply_pairwise_evolutions(m1, m2)
55
+ end
56
+
57
+ @members = apply_non_pairwise_evolutions(new_members)
58
+ @history << @members
59
+ @generation += 1
60
+ end
61
+
62
+ def evolution_over?
63
+ # check if the fitness goal or generation limit has been met
64
+ if generations_limit > 0
65
+ generation == generations_limit || best_member.fitness == fitness_goal
66
+ else
67
+ best_member.fitness == fitness_goal
68
+ end
69
+ end
70
+
71
+ def best_member
72
+ @members.sort_by { |m| m.fitness }.first
73
+ end
74
+
75
+ def size
76
+ @members.length
77
+ end
78
+
79
+ def organism_klass
80
+ real_organism = @organism
81
+ fitness_function = @fitness_function
82
+ klass = Class.new(Darwinning::Organism) do
83
+ @name = real_organism.name
84
+ @genes = real_organism.genes
85
+ end
86
+ end
87
+
88
+ private
89
+
90
+ def build_member
91
+ member = organism.new
92
+ unless member.class.superclass.to_s == "Darwinning::Organism"
93
+ member.class.genes.each do |gene|
94
+ gene_expression = gene.express
95
+ member.send("#{gene.name}=", gene_expression)
96
+ end
97
+ end
98
+ member
99
+ end
100
+
37
101
  def weighted_select(members)
38
- e = 0.01
39
102
  fitness_sum = members.inject(0) { |sum, m| sum + m.fitness }
40
103
 
41
104
  weighted_members = members.sort_by do |m|
42
105
  (m.fitness - fitness_goal).abs
43
106
  end.map do |m|
44
- [m, fitness_sum / ((m.fitness - fitness_goal).abs + e)]
107
+ [m, fitness_sum / ((m.fitness - fitness_goal).abs + EPSILON)]
45
108
  end
46
109
 
47
110
  weight_sum = weighted_members.inject(0) { |sum, m| sum + m[1] }
@@ -58,33 +121,10 @@ module Darwinning
58
121
  selected_member.first
59
122
  end
60
123
 
61
- def set_members_fitness!(fitness_values)
62
- members.to_enum.each_with_index { |m, i| m.fitness = fitness_values[i] }
63
- end
64
-
65
- def make_next_generation!
66
- temp_members = members
67
- used_members = []
68
- new_members = []
69
-
70
- until new_members.length == members.length / 2
71
- m1 = weighted_select(members - used_members)
72
- used_members << m1
73
- m2 = weighted_select(members - used_members)
74
- used_members << m2
75
-
76
- new_members << apply_pairwise_evolutions(organism, m1, m2)
77
- end
78
-
79
- new_members.flatten!
80
- @members = apply_non_pairwise_evolutions(new_members)
81
- @generation += 1
82
- end
83
-
84
- def apply_pairwise_evolutions(organism, m1, m2)
124
+ def apply_pairwise_evolutions(m1, m2)
85
125
  evolution_types.inject([m1, m2]) do |ret, evolution_type|
86
126
  if evolution_type.pairwise?
87
- evolution_type.evolve(organism, *ret)
127
+ evolution_type.evolve(*ret)
88
128
  else
89
129
  ret
90
130
  end
@@ -101,22 +141,5 @@ module Darwinning
101
141
  end
102
142
  end
103
143
 
104
- def evolution_over?
105
- # check if the fiteness goal or generation limit has been met
106
- if generations_limit > 0
107
- generation == generations_limit || best_member.fitness == fitness_goal
108
- else
109
- generation == generations_limit || best_member.fitness == fitness_goal
110
- end
111
- end
112
-
113
- def best_member
114
- @members.sort_by { |m| m.fitness }.first
115
- end
116
-
117
- def size
118
- @members.length
119
- end
120
144
  end
121
-
122
145
  end
@@ -1,3 +1,3 @@
1
1
  module Darwinning
2
- VERSION = "0.0.3"
2
+ VERSION = "0.1.0"
3
3
  end
@@ -0,0 +1,23 @@
1
+ class GenelessChimp
2
+ include Darwinning
3
+ end
4
+
5
+ class BadGenesChimp
6
+ include Darwinning
7
+
8
+ GENE_RANGES = "throw poop?"
9
+ end
10
+
11
+ class EmptyGenesChimp
12
+ include Darwinning
13
+
14
+ GENE_RANGES = {}
15
+ end
16
+
17
+ class NoValuesChimp
18
+ include Darwinning
19
+
20
+ GENE_RANGES = {
21
+ throwing_range: (0..100)
22
+ }
23
+ end
@@ -0,0 +1,16 @@
1
+ class NewTriple
2
+ include Darwinning
3
+
4
+ GENE_RANGES = {
5
+ first_digit: (0..9),
6
+ second_digit: (0..9),
7
+ third_digit: (0..9)
8
+ }
9
+
10
+ attr_accessor :first_digit, :second_digit, :third_digit
11
+
12
+ def fitness
13
+ # Try to get the sum of the 3 digits to add up to 15
14
+ (first_digit + second_digit + third_digit - 15).abs
15
+ end
16
+ end
@@ -8,6 +8,6 @@ class Triple < Darwinning::Organism
8
8
 
9
9
  def fitness
10
10
  # Try to get the sum of the 3 digits to add up to 15
11
- (genotypes.inject{ |sum, x| sum + x } - 15).abs
11
+ (genotypes.values.inject{ |sum, x| sum + x } - 15).abs
12
12
  end
13
13
  end
@@ -1,94 +1,98 @@
1
- require 'darwinning'
2
- require './spec/classes/triple'
3
-
4
- describe Darwinning::Gene do
5
- before do
6
- @digit = Darwinning::Gene.new(name: "digit", value_range: (0..9))
7
- @day_hour = Darwinning::Gene.new(
8
- name: "hour",
9
- value_range: (0..23),
10
- invalid_values: [0, 1, 2, 3, 4, 20, 21, 22, 23],
11
- units: "o'clock"
12
- )
13
- end
1
+ require 'spec_helper'
14
2
 
15
- it "name should be set" do
16
- @digit.name.should == "digit"
17
- end
3
+ describe Darwinning do
4
+ let(:triple_pop) { NewTriple.build_population(0, 20, 1000) }
5
+ let(:triple_pop_member) { triple_pop.members.first }
18
6
 
19
- it "value range should be set" do
20
- @digit.value_range.should == (0..9).to_a
21
- end
7
+ describe '#build_population' do
8
+ it 'creates a population of the correct size' do
9
+ expect(triple_pop.size).to eq 20
10
+ end
22
11
 
23
- it "invalid values should be set" do
24
- @day_hour.invalid_values.should == [0, 1, 2, 3, 4, 20, 21, 22, 23]
25
- end
12
+ it 'creates a population with the correct generation limit' do
13
+ expect(triple_pop.generations_limit).to eq 1000
14
+ end
26
15
 
27
- it "units should be set" do
28
- @day_hour.units.should == "o'clock"
29
- end
30
- end
16
+ it 'uses the fitness function defined by #fitness' do
17
+ a_triple = triple_pop.members.first
18
+ manual_fitness = (a_triple.first_digit + a_triple.second_digit + a_triple.third_digit - 15).abs
31
19
 
32
- describe Darwinning::Organism do
33
- before do
34
- @org = Darwinning::Organism.new
35
- @triple = Triple.new
20
+ expect(a_triple.fitness).to eq manual_fitness
21
+ end
36
22
  end
37
23
 
38
- it "name should default to blank" do
39
- @org.name.should == ""
24
+ describe '#genes' do
25
+ it 'creates a gene for every value in GENE_RANGES' do
26
+ expect(NewTriple.genes.size).to eq NewTriple::GENE_RANGES.size
27
+ end
40
28
  end
41
29
 
42
- it "genes should default to empty array" do
43
- @org.genes.should == []
44
- end
30
+ describe '#is_evolveable?' do
31
+ it 'is true for a class that implments the necessary Darwinning features' do
32
+ expect(NewTriple.is_evolveable?).to be true
33
+ end
45
34
 
46
- it "genotypes should initialize to empty array if genes is empty" do
47
- @org.genotypes.should == []
48
- end
35
+ it 'is false if the class is missing get GENE_RANGES constant' do
36
+ expect(GenelessChimp.is_evolveable?).to be false
37
+ end
49
38
 
50
- it "fitness should default to -1" do
51
- @org.fitness.should == -1
52
- end
39
+ it 'is false if GENE_RANGES not a Hash' do
40
+ expect(BadGenesChimp.is_evolveable?).to be false
41
+ end
53
42
 
54
- it "child class should set name" do
55
- @triple.name.should == "Triple"
56
- end
43
+ it 'is false if the genes are invalid' do
44
+ expect(NoValuesChimp.is_evolveable?).to be false
45
+ end
57
46
 
58
- it "child class should set genes" do
59
- @triple.genes.length.should == 3 # not the best test...
60
- end
47
+ it 'is false if GENE_RANGES is empty' do
48
+ expect(EmptyGenesChimp.is_evolveable?).to be false
49
+ end
61
50
 
62
- it "child class should initialize genotypes from genes" do
63
- @triple.genotypes.length.should == 3 # not the best test...
51
+ it 'is false if the class is missing a fitness method' do
52
+ expect(GenelessChimp.is_evolveable?).to be false
53
+ end
64
54
  end
65
55
 
66
- end
56
+ describe 'population member' do
57
+ it 'is of the parent class' do
58
+ expect(triple_pop_member.class.name).to eq "NewTriple"
59
+ end
67
60
 
68
- describe Darwinning::Population do
69
- before do
70
- @pop_triple = Darwinning::Population.new(
71
- organism: Triple, population_size: 10, fitness_goal: 0
72
- )
73
- end
61
+ it 'has genotype values validly set' do
62
+ expect(NewTriple.genes.first.value_range).to include(triple_pop_member.send(NewTriple.genes.first.name))
63
+ end
74
64
 
75
- it "fitness goal should be set to 0" do
76
- @pop_triple.fitness_goal.should == 0
77
- end
65
+ it 'has a genes method' do
66
+ expect(triple_pop_member.respond_to?(:genes)).to be true
67
+ end
78
68
 
79
- it "population size should be 10" do
80
- @pop_triple.members.length.should == 10
81
- end
69
+ it 'has genes that are Darwinning::Genes' do
70
+ gene_classes = triple_pop_member.genes.map { |g| g.class.name }.uniq.compact
71
+ expect(gene_classes).to eq ["Darwinning::Gene"]
72
+ end
82
73
 
83
- it "population should start on generation 0" do
84
- @pop_triple.generation.should == 0
85
- end
74
+ it 'has gene for each of the class defined genes' do
75
+ expect(triple_pop_member.genes).to eq NewTriple.genes
76
+ end
77
+
78
+ it 'has a genotypes method' do
79
+ expect(triple_pop_member.respond_to?(:genotypes)).to be true
80
+ end
86
81
 
87
- it "make_next_generation! should evolve population by one generation" do
88
- old_members = @pop_triple.members
89
- @pop_triple.make_next_generation!
90
- @pop_triple.generation.should == 1
91
- @pop_triple.members.should_not == old_members
82
+ it 'has genotype values for every gene' do
83
+ genotype_values_equal = triple_pop_member.genotypes.map { |k, v| v == triple_pop_member.send(k.name) }.uniq.compact
84
+ expect(genotype_values_equal).to eq [true]
85
+ end
92
86
  end
93
87
 
88
+ describe 'population evolution' do
89
+ let(:evolving_triple_pop) { NewTriple.build_population(0, 10, 100) }
90
+
91
+ it 'sets the genotypes of the next generation' do
92
+ evolving_triple_pop.make_next_generation!
93
+ pop_member = evolving_triple_pop.members.first
94
+ genotype_values_equal = pop_member.genotypes.map { |k, v| v != nil }.uniq.compact
95
+ expect(genotype_values_equal).to eq [true]
96
+ end
97
+ end
94
98
  end
data/spec/gene_spec.rb ADDED
@@ -0,0 +1,29 @@
1
+ require 'spec_helper'
2
+
3
+ describe Darwinning::Gene do
4
+ let(:digit) { Darwinning::Gene.new(name: "digit", value_range: (0..9)) }
5
+ let(:day_hour) {
6
+ Darwinning::Gene.new(
7
+ name: "hour",
8
+ value_range: (0..23),
9
+ invalid_values: [0, 1, 2, 3, 4, 20, 21, 22, 23],
10
+ units: "o'clock"
11
+ )
12
+ }
13
+
14
+ it "name should be set" do
15
+ expect(digit.name).to eq "digit"
16
+ end
17
+
18
+ it "value range should be set" do
19
+ expect(digit.value_range).to eq (0..9).to_a
20
+ end
21
+
22
+ it "invalid values should be set" do
23
+ expect(day_hour.invalid_values).to eq [0, 1, 2, 3, 4, 20, 21, 22, 23]
24
+ end
25
+
26
+ it "units should be set" do
27
+ expect(day_hour.units).to eq "o'clock"
28
+ end
29
+ end
@@ -0,0 +1,35 @@
1
+ require 'spec_helper'
2
+
3
+ describe Darwinning::Organism do
4
+ let(:org) { Darwinning::Organism.new }
5
+ let(:triple) { Triple.new }
6
+
7
+ it "name should default to blank" do
8
+ expect(org.name).to eq ""
9
+ end
10
+
11
+ it "genes should default to empty array" do
12
+ expect(org.genes).to eq []
13
+ end
14
+
15
+ it "genotypes should initialize to empty array if genes is empty" do
16
+ expect(org.genotypes).to be {}
17
+ end
18
+
19
+ it "fitness should default to -1" do
20
+ expect(org.fitness).to eq -1
21
+ end
22
+
23
+ it "child class should set name" do
24
+ expect(triple.name).to eq "Triple"
25
+ end
26
+
27
+ it "child class should set genes" do
28
+ expect(triple.genes.length).to eq 3
29
+ end
30
+
31
+ it "child class should initialize genotypes from genes" do
32
+ expect(triple.genotypes.length).to eq 3
33
+ end
34
+
35
+ end
@@ -0,0 +1,39 @@
1
+ require 'spec_helper'
2
+
3
+ describe Darwinning::Population do
4
+ let(:pop_triple) {
5
+ Darwinning::Population.new(
6
+ organism: Triple, population_size: 10, fitness_goal: 0
7
+ )
8
+ }
9
+
10
+ it "fitness goal should be set to 0" do
11
+ expect(pop_triple.fitness_goal).to eq 0
12
+ end
13
+
14
+ it "population size should be 10" do
15
+ expect(pop_triple.members.length).to eq 10
16
+ end
17
+
18
+ it "population should start on generation 0" do
19
+ expect(pop_triple.generation).to eq 0
20
+ end
21
+
22
+ it "make_next_generation! should evolve population by one generation" do
23
+ old_members = pop_triple.members
24
+ pop_triple.make_next_generation!
25
+
26
+ expect(pop_triple.generation).to eq 1
27
+ expect(pop_triple.members).not_to eq old_members
28
+ end
29
+
30
+ describe "#history" do
31
+
32
+ it "should be generations + 1 in size" do
33
+ pop_triple.evolve!
34
+ expect(pop_triple.history.size).to eq pop_triple.generation + 1
35
+ end
36
+
37
+ end
38
+
39
+ end
@@ -0,0 +1,4 @@
1
+ require 'darwinning'
2
+ require './spec/classes/triple'
3
+ require './spec/classes/new_triple'
4
+ require './spec/classes/chimps'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: darwinning
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2014-12-02 00:00:00.000000000 Z
12
+ date: 2015-09-27 00:00:00.000000000 Z
13
13
  dependencies: []
14
14
  description: Darwinning provides tools to build genetic algorithm solutions using
15
15
  a Gene, Organism, and Population structure.
@@ -30,8 +30,14 @@ files:
30
30
  - Gemfile
31
31
  - Rakefile
32
32
  - README.md
33
+ - spec/classes/chimps.rb
34
+ - spec/classes/new_triple.rb
33
35
  - spec/classes/triple.rb
34
36
  - spec/darwinning_spec.rb
37
+ - spec/gene_spec.rb
38
+ - spec/organism_spec.rb
39
+ - spec/population_spec.rb
40
+ - spec/spec_helper.rb
35
41
  homepage: https://github.com/dorkrawk/darwinning
36
42
  licenses: []
37
43
  post_install_message:
@@ -57,5 +63,11 @@ signing_key:
57
63
  specification_version: 3
58
64
  summary: A Ruby gem to aid in the use of genetic algorithms.
59
65
  test_files:
66
+ - spec/classes/chimps.rb
67
+ - spec/classes/new_triple.rb
60
68
  - spec/classes/triple.rb
61
69
  - spec/darwinning_spec.rb
70
+ - spec/gene_spec.rb
71
+ - spec/organism_spec.rb
72
+ - spec/population_spec.rb
73
+ - spec/spec_helper.rb