darwinning 0.0.3 → 0.1.0
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.
- 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
|
-
[](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
|