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 +1 -1
- data/README.md +80 -63
- data/lib/darwinning.rb +57 -2
- data/lib/darwinning/config.rb +1 -0
- data/lib/darwinning/evolution_types/reproduction.rb +59 -14
- data/lib/darwinning/gene.rb +17 -3
- data/lib/darwinning/organism.rb +8 -14
- data/lib/darwinning/population.rb +73 -50
- data/lib/darwinning/version.rb +1 -1
- data/spec/classes/chimps.rb +23 -0
- data/spec/classes/new_triple.rb +16 -0
- data/spec/classes/triple.rb +1 -1
- data/spec/darwinning_spec.rb +74 -70
- data/spec/gene_spec.rb +29 -0
- data/spec/organism_spec.rb +35 -0
- data/spec/population_spec.rb +39 -0
- data/spec/spec_helper.rb +4 -0
- metadata +14 -2
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Darwinning
|
2
2
|
==========
|
3
|
-
[![Gem Version](https://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
|
-
|
16
|
+
Usage
|
17
17
|
--------
|
18
18
|
|
19
|
-
|
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
|
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
|
-
|
30
|
+
require 'darwinning'
|
27
31
|
|
28
|
-
|
29
|
-
|
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
|
-
|
48
|
-
|
35
|
+
GENE_RANGES = {
|
36
|
+
first_number: (0..100),
|
37
|
+
second_number: (0..100),
|
38
|
+
third_number: (0..100)
|
39
|
+
}
|
49
40
|
|
50
|
-
|
41
|
+
attr_accessor :first_number, :second_number, :third_number
|
51
42
|
|
52
|
-
|
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
|
-
|
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
|
-
|
56
|
+
Once you have your organism class that includes Darwinning, you can create a population and evolve it:
|
57
57
|
|
58
58
|
```ruby
|
59
|
-
|
60
|
-
|
61
|
-
|
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
|
-
|
79
|
-
organism: Cookie, population_size: 10,
|
80
|
-
fitness_goal: 5, generations_limit: 100
|
81
|
-
)
|
67
|
+
### Inheritance Style:
|
82
68
|
|
83
|
-
|
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
|
-
|
71
|
+
Let's solve the same dumb problem we looked at before...
|
87
72
|
|
88
|
-
|
89
|
-
|
73
|
+
```ruby
|
74
|
+
require 'darwinning'
|
90
75
|
|
91
|
-
|
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
|
-
|
91
|
+
With this `Darwinning::Organism` class, you can now build a population and evolve it:
|
94
92
|
|
95
93
|
```ruby
|
96
|
-
|
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
|
-
|
103
|
+
### Stepping Through Generations Manually
|
99
104
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
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
|
-
|
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
|
data/lib/darwinning/config.rb
CHANGED
@@ -1,9 +1,16 @@
|
|
1
1
|
module Darwinning
|
2
2
|
module EvolutionTypes
|
3
|
-
|
4
3
|
class Reproduction
|
5
|
-
|
6
|
-
|
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(
|
16
|
-
|
17
|
-
|
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.
|
20
|
-
if
|
21
|
-
genotypes1
|
22
|
-
genotypes2
|
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
|
25
|
-
genotypes2
|
55
|
+
genotypes1[gene] = m2.genotypes[gene]
|
56
|
+
genotypes2[gene] = m1.genotypes[gene]
|
26
57
|
end
|
27
58
|
end
|
28
59
|
|
29
|
-
[
|
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
|
data/lib/darwinning/gene.rb
CHANGED
@@ -1,8 +1,6 @@
|
|
1
1
|
module Darwinning
|
2
|
-
|
3
2
|
class Gene
|
4
|
-
|
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
|
data/lib/darwinning/organism.rb
CHANGED
@@ -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
|
-
|
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 =
|
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
|
-
|
5
|
-
|
6
|
-
|
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 <<
|
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 +
|
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
|
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(
|
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
|
data/lib/darwinning/version.rb
CHANGED
@@ -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
|
data/spec/classes/triple.rb
CHANGED
data/spec/darwinning_spec.rb
CHANGED
@@ -1,94 +1,98 @@
|
|
1
|
-
require '
|
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
|
-
|
16
|
-
|
17
|
-
|
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
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
24
|
-
|
25
|
-
|
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
|
-
|
28
|
-
|
29
|
-
|
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
|
-
|
33
|
-
|
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
|
-
|
39
|
-
|
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
|
-
|
43
|
-
|
44
|
-
|
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
|
-
|
47
|
-
|
48
|
-
|
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
|
-
|
51
|
-
|
52
|
-
|
39
|
+
it 'is false if GENE_RANGES not a Hash' do
|
40
|
+
expect(BadGenesChimp.is_evolveable?).to be false
|
41
|
+
end
|
53
42
|
|
54
|
-
|
55
|
-
|
56
|
-
|
43
|
+
it 'is false if the genes are invalid' do
|
44
|
+
expect(NoValuesChimp.is_evolveable?).to be false
|
45
|
+
end
|
57
46
|
|
58
|
-
|
59
|
-
|
60
|
-
|
47
|
+
it 'is false if GENE_RANGES is empty' do
|
48
|
+
expect(EmptyGenesChimp.is_evolveable?).to be false
|
49
|
+
end
|
61
50
|
|
62
|
-
|
63
|
-
|
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
|
-
|
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
|
-
|
69
|
-
|
70
|
-
|
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
|
-
|
76
|
-
|
77
|
-
|
65
|
+
it 'has a genes method' do
|
66
|
+
expect(triple_pop_member.respond_to?(:genes)).to be true
|
67
|
+
end
|
78
68
|
|
79
|
-
|
80
|
-
|
81
|
-
|
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
|
-
|
84
|
-
|
85
|
-
|
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
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
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
|
data/spec/spec_helper.rb
ADDED
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
|
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:
|
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
|