evolvable 0.1.3 → 1.0.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.
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evolvable::Goal
4
+ class Maximize
5
+ include Evolvable::Goal
6
+
7
+ def value
8
+ @value ||= Float::INFINITY
9
+ end
10
+
11
+ def evaluate(instance)
12
+ instance.value
13
+ end
14
+
15
+ def met?(instance)
16
+ instance.value >= value
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evolvable::Goal
4
+ class Minimize
5
+ include Evolvable::Goal
6
+
7
+ def value
8
+ @value ||= -Float::INFINITY
9
+ end
10
+
11
+ def evaluate(instance)
12
+ -instance.value
13
+ end
14
+
15
+ def met?(instance)
16
+ instance.value <= value
17
+ end
18
+ end
19
+ end
@@ -1,64 +1,42 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Evolvable
2
4
  class Mutation
3
5
  extend Forwardable
4
6
 
5
- def initialize(rate: 0.03)
6
- @rate = rate
7
+ def initialize(probability: nil, rate: nil)
8
+ @probability = probability || (rate ? 1 : 0.03)
9
+ @rate = rate || 0
7
10
  end
8
11
 
9
- attr_accessor :rate
10
-
11
- def_delegators :@evolvable_class,
12
- :evolvable_genes_count,
13
- :evolvable_gene_pool_size,
14
- :evolvable_random_genes
12
+ attr_accessor :probability,
13
+ :rate
15
14
 
16
- def call!(objects)
17
- @evolvable_class = objects.first.class
18
- mutations_count = find_mutations_count(objects)
19
- return if mutations_count.zero?
15
+ def call(population)
16
+ return population if probability.zero?
20
17
 
21
- mutant_genes = generate_mutant_genes(mutations_count)
22
- object_mutations_count = mutations_count / objects.count
23
- object_mutations_count = 1 if object_mutations_count.zero?
24
-
25
- mutant_genes.each_slice(object_mutations_count).with_index do |m_genes, index|
26
- object = objects[index] || objects.sample
27
- genes = object.genes
28
- genes.merge!(m_genes.to_h)
29
- rm_genes_count = genes.count - evolvable_genes_count
30
- genes.keys.sample(rm_genes_count).each { |key| genes.delete(key) }
18
+ population.instances.each do |instance|
19
+ mutate_instance(instance) if rand <= probability
31
20
  end
32
- end
33
-
34
- def inspect
35
- "#<#{self.class.name} #{as_json.map { |a| a.join(': ') }.join(', ')} >"
36
- end
37
-
38
- def as_json
39
- { type: self.class.name,
40
- rate: @rate }
21
+ population
41
22
  end
42
23
 
43
24
  private
44
25
 
45
- def find_mutations_count(objects)
46
- return 0 if @rate.zero?
26
+ def mutate_instance(instance)
27
+ genes_count = instance.genes.count
28
+ return if genes_count.zero?
47
29
 
48
- count = (objects.count * evolvable_genes_count * @rate)
49
- return count.to_i if count >= 1
30
+ return mutate_gene(instance, rand(genes_count)) if rate.zero?
50
31
 
51
- rand <= count ? 1 : 0
32
+ genes_count.times { |index| mutate_gene(instance, index) if rand <= rate }
52
33
  end
53
34
 
54
- def generate_mutant_genes(mutations_count)
55
- gene_pool_size = evolvable_gene_pool_size
56
- mutant_genes = []
57
- while mutant_genes.count < mutations_count
58
- genes_count = [gene_pool_size, mutations_count - mutant_genes.count].min
59
- mutant_genes.concat evolvable_random_genes(genes_count).to_a
60
- end
61
- mutant_genes
35
+ def mutate_gene(instance, gene_index)
36
+ gene = instance.genes[gene_index]
37
+ mutant_gene = gene.class.new
38
+ mutant_gene.key = gene.key
39
+ instance.genes[gene_index] = mutant_gene
62
40
  end
63
41
  end
64
42
  end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evolvable
4
+ class PointCrossover
5
+ def initialize(points_count: 1)
6
+ @points_count = points_count
7
+ end
8
+
9
+ attr_accessor :points_count
10
+
11
+ def call(population)
12
+ population.instances = initialize_offspring(population)
13
+ population
14
+ end
15
+
16
+ private
17
+
18
+ def initialize_offspring(population)
19
+ parent_genes = population.instances.map!(&:genes)
20
+ parent_gene_couples = parent_genes.combination(2).cycle
21
+ offspring = []
22
+ population_index = 0
23
+ loop do
24
+ genes_1, genes_2 = parent_gene_couples.next
25
+ crossover_genes(genes_1, genes_2).each do |genes|
26
+ offspring << population.new_instance(genes: genes, population_index: population_index)
27
+ population_index += 1
28
+ return offspring if population_index == population.size
29
+ end
30
+ end
31
+ end
32
+
33
+ def crossover_genes(genes_1, genes_2)
34
+ offspring_genes = [[], []]
35
+ generate_ranges(genes_1.length).each do |range|
36
+ offspring_genes.reverse!
37
+ offspring_genes[0][range] = genes_1[range]
38
+ offspring_genes[1][range] = genes_2[range]
39
+ end
40
+ offspring_genes
41
+ end
42
+
43
+ def generate_ranges(genes_count)
44
+ current_point = rand(genes_count)
45
+ range_slices = [0...current_point]
46
+ (points_count - 1).times do
47
+ new_point = rand(current_point...genes_count)
48
+ break if new_point.nil?
49
+
50
+ range_slices << (current_point...new_point)
51
+ current_point = new_point
52
+ end
53
+ range_slices << (current_point..-1)
54
+ range_slices
55
+ end
56
+ end
57
+ end
@@ -4,115 +4,94 @@ module Evolvable
4
4
  class Population
5
5
  extend Forwardable
6
6
 
7
- def initialize(evolvable_class:,
8
- size: 20,
9
- selection_count: 2,
10
- crossover: Crossover.new,
11
- mutation: Mutation.new,
12
- generation_count: 0,
13
- log_progress: false,
14
- objects: [])
7
+ def initialize(id: nil,
8
+ evolvable_class:,
9
+ name: nil,
10
+ size: 40,
11
+ evolutions_count: 0,
12
+ gene_space: nil,
13
+ evolution: Evolution.new,
14
+ evaluation: Evaluation.new,
15
+ instances: [])
16
+ @id = id
15
17
  @evolvable_class = evolvable_class
18
+ @name = name
16
19
  @size = size
17
- @selection_count = selection_count
18
- @crossover = crossover
19
- @mutation = mutation
20
- @generation_count = generation_count
21
- @log_progress = log_progress
22
- assign_objects(objects)
20
+ @evolutions_count = evolutions_count
21
+ @gene_space = initialize_gene_space(gene_space)
22
+ @evolution = evolution
23
+ @evaluation = evaluation || Evaluation.new
24
+ initialize_instances(instances)
23
25
  end
24
26
 
25
- attr_accessor :evolvable_class,
27
+ attr_accessor :id,
28
+ :evolvable_class,
29
+ :name,
26
30
  :size,
27
- :selection_count,
28
- :crossover,
29
- :mutation,
30
- :generation_count,
31
- :log_progress,
32
- :objects
33
-
34
- def_delegators :@evolvable_class,
35
- :evolvable_evaluate!,
36
- :evolvable_initialize,
37
- :evolvable_random_genes,
38
- :evolvable_before_evolution,
39
- :evolvable_after_select,
40
- :evolvable_after_evolution
41
-
42
- def evolve!(generations_count: 1, fitness_goal: nil)
43
- @fitness_goal = fitness_goal
44
- generations_count.times do
45
- @generation_count += 1
46
- evolvable_before_evolution(self)
47
- evaluate_objects!
48
- log_evolvable_progress if log_progress
49
- break if fitness_goal_met?
50
-
51
- select_objects!
52
- evolvable_after_select(self)
53
- crossover_objects!
54
- mutate_objects!
55
- evolvable_after_evolution(self)
56
- end
57
- end
58
-
59
- def strongest_object
60
- objects.max_by(&:fitness)
61
- end
62
-
63
- def evaluate_objects!
64
- evolvable_evaluate!(@objects)
65
- if @fitness_goal
66
- @objects.sort_by! { |i| -(i.fitness - @fitness_goal).abs }
67
- else
68
- @objects.sort_by!(&:fitness)
31
+ :evolutions_count,
32
+ :gene_space,
33
+ :evolution,
34
+ :evaluation,
35
+ :instances
36
+
37
+ def_delegators :evolvable_class,
38
+ :before_evaluation,
39
+ :before_evolution,
40
+ :after_evolution
41
+
42
+ def_delegators :evolution,
43
+ :selection,
44
+ :selection=,
45
+ :crossover,
46
+ :crossover=,
47
+ :mutation,
48
+ :mutation=
49
+
50
+ def_delegators :evaluation,
51
+ :goal,
52
+ :goal=
53
+
54
+ def evolve(count: Float::INFINITY, goal_value: nil)
55
+ goal.value = goal_value if goal_value
56
+ (1..count).each do
57
+ before_evaluation(self)
58
+ evaluation.call(self)
59
+ before_evolution(self)
60
+ break if met_goal?
61
+
62
+ evolution.call(self)
63
+ self.evolutions_count += 1
64
+ after_evolution(self)
69
65
  end
70
66
  end
71
67
 
72
- def log_evolvable_progress
73
- @objects.last.evolvable_progress
68
+ def best_instance
69
+ evaluation.best_instance(self)
74
70
  end
75
71
 
76
- def fitness_goal_met?
77
- @fitness_goal && @objects.last.fitness >= @fitness_goal
72
+ def met_goal?
73
+ evaluation.met_goal?(self)
78
74
  end
79
75
 
80
- def select_objects!
81
- @objects.slice!(0..-1 - @selection_count)
76
+ def new_instance(genes: [], population_index: nil)
77
+ evolvable_class.new_instance(population: self,
78
+ genes: genes,
79
+ population_index: population_index)
82
80
  end
83
81
 
84
- def crossover_objects!
85
- parent_genes = @objects.map(&:genes)
86
- offspring_genes = @crossover.call(parent_genes, @size)
87
- @objects = offspring_genes.map.with_index do |genes, i|
88
- evolvable_initialize(genes, self, i)
89
- end
90
- end
91
-
92
- def mutate_objects!
93
- @mutation.call!(@objects)
94
- end
82
+ private
95
83
 
96
- def inspect
97
- "#<#{self.class.name} #{as_json} >"
98
- end
84
+ def initialize_gene_space(gene_space)
85
+ return GeneSpace.build(gene_space) if gene_space
99
86
 
100
- def as_json
101
- { evolvable_class: @evolvable_class.name,
102
- size: @size,
103
- selection_count: @selection_count,
104
- crossover: @crossover.as_json,
105
- mutation: @mutation.as_json,
106
- generation_count: @generation_count }
87
+ evolvable_class.new_gene_space
107
88
  end
108
89
 
109
- private
110
-
111
- def assign_objects(objects)
112
- @objects = objects || []
113
- (@size - objects.count).times do |n|
114
- genes = evolvable_random_genes
115
- @objects << evolvable_initialize(genes, self, n)
90
+ def initialize_instances(instances)
91
+ @instances = instances || []
92
+ (@size - instances.count).times do |n|
93
+ genes = gene_space.new_genes
94
+ @instances << new_instance(genes: genes, population_index: n)
116
95
  end
117
96
  end
118
97
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evolvable
4
+ class Selection
5
+ extend Forwardable
6
+
7
+ def initialize(size: 2)
8
+ @size = size
9
+ end
10
+
11
+ attr_accessor :size
12
+
13
+ def call(population)
14
+ population.instances.slice!(0..-1 - @size)
15
+ population
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evolvable
4
+ class UniformCrossover
5
+ def call(population)
6
+ population.instances = initialize_offspring(population)
7
+ population
8
+ end
9
+
10
+ private
11
+
12
+ def initialize_offspring(population)
13
+ parent_genes = population.instances.map!(&:genes)
14
+ parent_gene_couples = parent_genes.combination(2).cycle
15
+ Array.new(population.size) do |index|
16
+ genes_1, genes_2 = parent_gene_couples.next
17
+ genes = genes_1.zip(genes_2).map!(&:sample)
18
+ population.new_instance(genes: genes, population_index: index)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Evolvable
4
- VERSION = '0.1.3'
4
+ VERSION = '1.0.0'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: evolvable
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Ruzicka
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-03-11 00:00:00.000000000 Z
11
+ date: 2020-09-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -67,13 +67,24 @@ files:
67
67
  - bin/console
68
68
  - bin/setup
69
69
  - evolvable.gemspec
70
+ - examples/evolvable_string.rb
71
+ - examples/evolvable_string/char_gene.rb
70
72
  - lib/evolvable.rb
71
- - lib/evolvable/crossover.rb
72
- - lib/evolvable/errors/not_implemented.rb
73
- - lib/evolvable/helper_methods.rb
74
- - lib/evolvable/hooks.rb
73
+ - lib/evolvable/error/undefined_method.rb
74
+ - lib/evolvable/evaluation.rb
75
+ - lib/evolvable/evolution.rb
76
+ - lib/evolvable/gene.rb
77
+ - lib/evolvable/gene_crossover.rb
78
+ - lib/evolvable/gene_space.rb
79
+ - lib/evolvable/goal.rb
80
+ - lib/evolvable/goal/equalize.rb
81
+ - lib/evolvable/goal/maximize.rb
82
+ - lib/evolvable/goal/minimize.rb
75
83
  - lib/evolvable/mutation.rb
84
+ - lib/evolvable/point_crossover.rb
76
85
  - lib/evolvable/population.rb
86
+ - lib/evolvable/selection.rb
87
+ - lib/evolvable/uniform_crossover.rb
77
88
  - lib/evolvable/version.rb
78
89
  homepage: https://github.com/mattruzicka/evolvable
79
90
  licenses:
@@ -97,7 +108,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
97
108
  - !ruby/object:Gem::Version
98
109
  version: '0'
99
110
  requirements: []
100
- rubygems_version: 3.0.3
111
+ rubygems_version: 3.1.2
101
112
  signing_key:
102
113
  specification_version: 4
103
114
  summary: Add evolutionary behavior to any Ruby object