evolvable 1.2.0 → 2.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.
- checksums.yaml +4 -4
- data/.yardopts +2 -0
- data/CHANGELOG.md +24 -0
- data/Gemfile +2 -2
- data/Gemfile.lock +44 -25
- data/README.md +498 -190
- data/README_YARD.md +85 -166
- data/bin/console +10 -19
- data/docs/Evolvable/ClassMethods.html +1233 -0
- data/docs/Evolvable/Community/ClassMethods.html +708 -0
- data/docs/Evolvable/Community.html +1342 -0
- data/docs/Evolvable/CountGene.html +886 -0
- data/docs/Evolvable/EqualizeGoal.html +347 -0
- data/docs/Evolvable/Error.html +134 -0
- data/docs/Evolvable/Evaluation.html +773 -0
- data/docs/Evolvable/Evolution.html +616 -0
- data/docs/Evolvable/Gene/ClassMethods.html +413 -0
- data/docs/Evolvable/Gene.html +522 -0
- data/docs/Evolvable/GeneCluster/ClassMethods.html +431 -0
- data/docs/Evolvable/GeneCluster.html +280 -0
- data/docs/Evolvable/GeneCombination.html +515 -0
- data/docs/Evolvable/GeneSpace.html +619 -0
- data/docs/Evolvable/Genome.html +1070 -0
- data/docs/Evolvable/Goal.html +500 -0
- data/docs/Evolvable/MaximizeGoal.html +348 -0
- data/docs/Evolvable/MinimizeGoal.html +348 -0
- data/docs/Evolvable/Mutation.html +729 -0
- data/docs/Evolvable/PointCrossover.html +444 -0
- data/docs/Evolvable/Population.html +2826 -0
- data/docs/Evolvable/RigidCountGene.html +501 -0
- data/docs/Evolvable/Selection.html +594 -0
- data/docs/Evolvable/Serializer.html +293 -0
- data/docs/Evolvable/UniformCrossover.html +286 -0
- data/docs/Evolvable.html +1619 -0
- data/docs/_index.html +341 -0
- data/docs/class_list.html +54 -0
- data/docs/css/common.css +1 -0
- data/docs/css/full_list.css +58 -0
- data/docs/css/style.css +503 -0
- data/docs/file.README.html +750 -0
- data/docs/file_list.html +59 -0
- data/docs/frames.html +22 -0
- data/docs/index.html +750 -0
- data/docs/js/app.js +344 -0
- data/docs/js/full_list.js +242 -0
- data/docs/js/jquery.js +4 -0
- data/docs/method_list.html +1302 -0
- data/docs/top-level-namespace.html +110 -0
- data/evolvable.gemspec +6 -6
- data/examples/ascii_art.rb +5 -9
- data/examples/stickman.rb +25 -33
- data/{examples/hello_world.rb → exe/hello_evolvable_world} +46 -30
- data/lib/evolvable/community.rb +190 -0
- data/lib/evolvable/count_gene.rb +65 -0
- data/lib/evolvable/equalize_goal.rb +2 -9
- data/lib/evolvable/evaluation.rb +113 -14
- data/lib/evolvable/evolution.rb +38 -15
- data/lib/evolvable/gene.rb +124 -25
- data/lib/evolvable/gene_cluster.rb +106 -0
- data/lib/evolvable/gene_combination.rb +57 -16
- data/lib/evolvable/gene_space.rb +111 -0
- data/lib/evolvable/genome.rb +32 -4
- data/lib/evolvable/goal.rb +19 -24
- data/lib/evolvable/maximize_goal.rb +2 -9
- data/lib/evolvable/minimize_goal.rb +3 -9
- data/lib/evolvable/mutation.rb +87 -41
- data/lib/evolvable/point_crossover.rb +24 -4
- data/lib/evolvable/population.rb +258 -84
- data/lib/evolvable/rigid_count_gene.rb +36 -0
- data/lib/evolvable/selection.rb +68 -14
- data/lib/evolvable/serializer.rb +46 -0
- data/lib/evolvable/uniform_crossover.rb +30 -6
- data/lib/evolvable/version.rb +2 -1
- data/lib/evolvable.rb +268 -107
- metadata +57 -36
- data/examples/images/diagram.png +0 -0
- data/exe/hello +0 -16
- data/lib/evolvable/error/undefined_method.rb +0 -7
- data/lib/evolvable/search_space.rb +0 -181
@@ -0,0 +1,110 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<meta charset="utf-8">
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6
|
+
<title>
|
7
|
+
Top Level Namespace
|
8
|
+
|
9
|
+
— Documentation by YARD 0.9.37
|
10
|
+
|
11
|
+
</title>
|
12
|
+
|
13
|
+
<link rel="stylesheet" href="css/style.css" type="text/css" />
|
14
|
+
|
15
|
+
<link rel="stylesheet" href="css/common.css" type="text/css" />
|
16
|
+
|
17
|
+
<script type="text/javascript">
|
18
|
+
pathId = "";
|
19
|
+
relpath = '';
|
20
|
+
</script>
|
21
|
+
|
22
|
+
|
23
|
+
<script type="text/javascript" charset="utf-8" src="js/jquery.js"></script>
|
24
|
+
|
25
|
+
<script type="text/javascript" charset="utf-8" src="js/app.js"></script>
|
26
|
+
|
27
|
+
|
28
|
+
</head>
|
29
|
+
<body>
|
30
|
+
<div class="nav_wrap">
|
31
|
+
<iframe id="nav" src="class_list.html?1"></iframe>
|
32
|
+
<div id="resizer"></div>
|
33
|
+
</div>
|
34
|
+
|
35
|
+
<div id="main" tabindex="-1">
|
36
|
+
<div id="header">
|
37
|
+
<div id="menu">
|
38
|
+
|
39
|
+
<a href="_index.html">Index</a> »
|
40
|
+
|
41
|
+
|
42
|
+
<span class="title">Top Level Namespace</span>
|
43
|
+
|
44
|
+
</div>
|
45
|
+
|
46
|
+
<div id="search">
|
47
|
+
|
48
|
+
<a class="full_list_link" id="class_list_link"
|
49
|
+
href="class_list.html">
|
50
|
+
|
51
|
+
<svg width="24" height="24">
|
52
|
+
<rect x="0" y="4" width="24" height="4" rx="1" ry="1"></rect>
|
53
|
+
<rect x="0" y="12" width="24" height="4" rx="1" ry="1"></rect>
|
54
|
+
<rect x="0" y="20" width="24" height="4" rx="1" ry="1"></rect>
|
55
|
+
</svg>
|
56
|
+
</a>
|
57
|
+
|
58
|
+
</div>
|
59
|
+
<div class="clear"></div>
|
60
|
+
</div>
|
61
|
+
|
62
|
+
<div id="content"><h1>Top Level Namespace
|
63
|
+
|
64
|
+
|
65
|
+
|
66
|
+
</h1>
|
67
|
+
<div class="box_info">
|
68
|
+
|
69
|
+
|
70
|
+
|
71
|
+
|
72
|
+
|
73
|
+
|
74
|
+
|
75
|
+
|
76
|
+
|
77
|
+
|
78
|
+
|
79
|
+
</div>
|
80
|
+
|
81
|
+
<h2>Defined Under Namespace</h2>
|
82
|
+
<p class="children">
|
83
|
+
|
84
|
+
|
85
|
+
<strong class="modules">Modules:</strong> <span class='object_link'><a href="Evolvable.html" title="Evolvable (module)">Evolvable</a></span>
|
86
|
+
|
87
|
+
|
88
|
+
|
89
|
+
|
90
|
+
</p>
|
91
|
+
|
92
|
+
|
93
|
+
|
94
|
+
|
95
|
+
|
96
|
+
|
97
|
+
|
98
|
+
|
99
|
+
|
100
|
+
</div>
|
101
|
+
|
102
|
+
<div id="footer">
|
103
|
+
Generated on Sat May 10 18:39:22 2025 by
|
104
|
+
<a href="https://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
|
105
|
+
0.9.37 (ruby-3.4.2).
|
106
|
+
</div>
|
107
|
+
|
108
|
+
</div>
|
109
|
+
</body>
|
110
|
+
</html>
|
data/evolvable.gemspec
CHANGED
@@ -10,11 +10,12 @@ Gem::Specification.new do |spec|
|
|
10
10
|
spec.summary = 'Add evolutionary behavior to any Ruby object'
|
11
11
|
spec.homepage = 'https://github.com/mattruzicka/evolvable'
|
12
12
|
spec.license = 'MIT'
|
13
|
+
spec.required_ruby_version = ">= 2.6"
|
13
14
|
|
14
15
|
spec.metadata['homepage_uri'] = spec.homepage
|
15
|
-
spec.metadata['source_code_uri'] =
|
16
|
-
spec.metadata['changelog_uri'] = 'https://github.com/mattruzicka/evolvable'
|
17
|
-
|
16
|
+
spec.metadata['source_code_uri'] = spec.homepage
|
17
|
+
spec.metadata['changelog_uri'] = 'https://github.com/mattruzicka/evolvable/blob/main/CHANGELOG.md'
|
18
|
+
spec.metadata["documentation_uri"] = Evolvable::DOC_URL
|
18
19
|
|
19
20
|
# Specify which files should be added to the gem when it is released.
|
20
21
|
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
@@ -25,7 +26,6 @@ Gem::Specification.new do |spec|
|
|
25
26
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
26
27
|
spec.require_paths = ['lib']
|
27
28
|
|
28
|
-
spec.add_development_dependency '
|
29
|
-
spec.add_development_dependency 'readme_yard'
|
30
|
-
spec.add_development_dependency 'yard'
|
29
|
+
spec.add_development_dependency 'irb', '>= 1.0'
|
30
|
+
spec.add_development_dependency 'readme_yard', '>= 0.5'
|
31
31
|
end
|
data/examples/ascii_art.rb
CHANGED
@@ -3,14 +3,10 @@ require './examples/ascii_gene'
|
|
3
3
|
class AsciiArt
|
4
4
|
include Evolvable
|
5
5
|
|
6
|
-
|
7
|
-
def search_space
|
8
|
-
{ chars: { type: 'AsciiGene', count: 136 } }
|
9
|
-
end
|
6
|
+
gene :chars, type: 'AsciiGene', count: 136
|
10
7
|
|
11
|
-
|
12
|
-
|
13
|
-
end
|
8
|
+
def self.before_evaluation(population)
|
9
|
+
population.best_evolvable.to_terminal
|
14
10
|
end
|
15
11
|
|
16
12
|
CLEAR_SEQUENCE = ("\e[1A\r\033[2K" * 14).freeze
|
@@ -18,7 +14,7 @@ class AsciiArt
|
|
18
14
|
def to_terminal
|
19
15
|
print(CLEAR_SEQUENCE) unless population.evolutions_count.zero?
|
20
16
|
lines = genes.each_slice(17).flat_map(&:join)
|
21
|
-
lines[0] = "\n #{lines[0]} #{green_text('Minimalism Score:')} #{
|
17
|
+
lines[0] = "\n #{lines[0]} #{green_text('Minimalism Score:')} #{fitness}"
|
22
18
|
lines[1] << " #{green_text('Generation:')} #{population.evolutions_count}"
|
23
19
|
print "\n\n#{lines.join("\n ")}\n\n\n\n #{green_text('Use Ctrl-C to stop')} "
|
24
20
|
end
|
@@ -27,7 +23,7 @@ class AsciiArt
|
|
27
23
|
@minimalism_score ||= essence_score + spacial_score - clutter_score
|
28
24
|
end
|
29
25
|
|
30
|
-
alias
|
26
|
+
alias fitness minimalism_score
|
31
27
|
|
32
28
|
private
|
33
29
|
|
data/examples/stickman.rb
CHANGED
@@ -21,49 +21,41 @@ class Stickman
|
|
21
21
|
end
|
22
22
|
end
|
23
23
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
body: { type: 'AsciiGene', count: 1 },
|
28
|
-
appendages: { type: 'AppendageGene', count: 4 } }
|
29
|
-
end
|
30
|
-
|
31
|
-
CLEAR_SEQUENCE = ("\e[1A\r\033[2K" * 8).freeze
|
24
|
+
gene :head, type: 'HeadGene', count: 1
|
25
|
+
gene :body, type: 'AsciiGene', count: 1
|
26
|
+
gene :appendages, type: 'AppendageGene', count: 4
|
32
27
|
|
33
|
-
|
34
|
-
population.evolvables.each do |evolvable|
|
35
|
-
puts "\n\n#{evolvable.draw}\n\n"
|
36
|
-
print green_text(" Rate Stickman #{population.evolutions_count}.#{evolvable.generation_index + 1}: ")
|
37
|
-
evolvable.value = gets.to_i
|
38
|
-
print CLEAR_SEQUENCE
|
39
|
-
end
|
28
|
+
CLEAR_SEQUENCE = ("\e[1A\r\033[2K" * 8).freeze
|
40
29
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
gets
|
30
|
+
def self.before_evaluation(population)
|
31
|
+
population.evolvables.each do |evolvable|
|
32
|
+
puts "\n\n#{evolvable.draw}\n\n"
|
33
|
+
print green_text(" Rate Stickman #{population.evolutions_count}.#{evolvable.generation_index + 1}: ")
|
34
|
+
evolvable.fitness = gets.to_i
|
46
35
|
print CLEAR_SEQUENCE
|
47
36
|
end
|
48
37
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
print green_text(" Generation: #{index}\r\n ")
|
53
|
-
sleep 0.12
|
54
|
-
print "#{CLEAR_SEQUENCE}"
|
55
|
-
end
|
56
|
-
end
|
38
|
+
(@best_evolvables ||= []) << population.best_evolvable
|
39
|
+
animate_best_evolvables if @best_evolvables.count > 1
|
40
|
+
end
|
57
41
|
|
58
|
-
|
59
|
-
|
42
|
+
def self.animate_best_evolvables
|
43
|
+
@best_evolvables.each_with_index do |evolvable, index|
|
44
|
+
puts "\n\n#{green_text(evolvable.draw)}\n\n"
|
45
|
+
print green_text(" Generation: #{index}\r\n ")
|
46
|
+
sleep 0.5
|
47
|
+
print "#{CLEAR_SEQUENCE}"
|
60
48
|
end
|
61
49
|
end
|
62
50
|
|
51
|
+
def self.green_text(text)
|
52
|
+
"\e[32m#{text}\e[0m"
|
53
|
+
end
|
54
|
+
|
63
55
|
def draw
|
64
|
-
canvas.sub!('o',
|
65
|
-
canvas.sub!('0',
|
66
|
-
|
56
|
+
canvas.sub!('o', head.to_s)
|
57
|
+
canvas.sub!('0', body.to_s)
|
58
|
+
appendages.each { |a| canvas.sub!('-', a.to_s) }
|
67
59
|
canvas
|
68
60
|
end
|
69
61
|
|
@@ -1,27 +1,37 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Hello World Command Line Demo
|
4
|
+
#
|
5
|
+
# Make sure you have the gem installed: `gem install evolvable``
|
6
|
+
# Run this file directly: `bundle exec hello_evolvable_world`
|
7
|
+
#
|
8
|
+
|
9
|
+
require 'bundler/setup'
|
10
|
+
require 'evolvable'
|
11
|
+
require 'irb'
|
12
|
+
|
13
|
+
class CharGene
|
14
|
+
include Evolvable::Gene
|
15
|
+
|
16
|
+
def self.chars
|
17
|
+
@chars ||= 32.upto(126).map(&:chr)
|
18
|
+
end
|
10
19
|
|
11
|
-
|
12
|
-
|
13
|
-
|
20
|
+
def self.add_missing_chars(input_string)
|
21
|
+
@chars.concat(input_string.chars - @chars)
|
22
|
+
end
|
14
23
|
|
15
|
-
|
16
|
-
|
17
|
-
end
|
24
|
+
def to_s
|
25
|
+
@to_s ||= self.class.chars.sample
|
18
26
|
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class HelloWorld
|
30
|
+
include Evolvable
|
19
31
|
|
20
32
|
MAX_STRING_LENGTH = 40
|
21
33
|
|
22
|
-
|
23
|
-
{ char_genes: { type: 'CharGene', count: 1..MAX_STRING_LENGTH } }
|
24
|
-
end
|
34
|
+
gene :char_genes, type: 'CharGene', count: 1..MAX_STRING_LENGTH
|
25
35
|
|
26
36
|
def self.start_loop(population)
|
27
37
|
loop do
|
@@ -29,7 +39,7 @@ class HelloWorld
|
|
29
39
|
prepare_to_exit_loop && break if exit_loop?
|
30
40
|
|
31
41
|
population.reset_evolvables
|
32
|
-
population.
|
42
|
+
population.evolve_to_goal
|
33
43
|
end
|
34
44
|
end
|
35
45
|
|
@@ -48,17 +58,15 @@ class HelloWorld
|
|
48
58
|
self.target = gets.strip!
|
49
59
|
end
|
50
60
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
val.slice(0...MAX_STRING_LENGTH)
|
57
|
-
end
|
61
|
+
DEFAULT_TARGET = 'Hello Evolvable World'
|
62
|
+
|
63
|
+
def self.target=(input_string)
|
64
|
+
CharGene.add_missing_chars(input_string)
|
65
|
+
@target = input_string.empty? ? DEFAULT_TARGET : input_string.slice(0...MAX_STRING_LENGTH)
|
58
66
|
end
|
59
67
|
|
60
68
|
def self.target
|
61
|
-
@target ||=
|
69
|
+
@target ||= DEFAULT_TARGET
|
62
70
|
end
|
63
71
|
|
64
72
|
def self.before_evolution(population)
|
@@ -75,13 +83,13 @@ class HelloWorld
|
|
75
83
|
@to_s ||= genes.join
|
76
84
|
end
|
77
85
|
|
78
|
-
def
|
79
|
-
@
|
86
|
+
def fitness
|
87
|
+
@fitness ||= compute_fitness
|
80
88
|
end
|
81
89
|
|
82
90
|
private
|
83
91
|
|
84
|
-
def
|
92
|
+
def compute_fitness
|
85
93
|
string = to_s
|
86
94
|
target = self.class.target
|
87
95
|
target_length = target.length
|
@@ -89,3 +97,11 @@ class HelloWorld
|
|
89
97
|
(target_length - char_matches) + (target_length - string.length).abs
|
90
98
|
end
|
91
99
|
end
|
100
|
+
|
101
|
+
population = HelloWorld.new_population(size: 100,
|
102
|
+
evaluation: { equalize: 0 },
|
103
|
+
mutation: { probability: 0.6 })
|
104
|
+
population.evolve_to_goal
|
105
|
+
HelloWorld.start_loop(population)
|
106
|
+
|
107
|
+
binding.irb
|
@@ -0,0 +1,190 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Evolvable
|
4
|
+
#
|
5
|
+
# @readme
|
6
|
+
# The `Community` module provides a framework for coordinating multiple evolvable populations
|
7
|
+
# under a unified interface. Each population represents a distinct type of evolvable, and
|
8
|
+
# each key returns a single evolvable instance drawn from its corresponding population.
|
9
|
+
#
|
10
|
+
# Communities are ideal for simulations or systems where different components evolve
|
11
|
+
# in parallel but interact as part of a larger whole - such as ecosystems, design systems,
|
12
|
+
# or modular agents. Evolvables from different populations can co-evolve, influencing each other's fitness.
|
13
|
+
#
|
14
|
+
# Use the `evolvable_community` macro to declare the set of named populations in the community.
|
15
|
+
# Each population will have a corresponding method (e.g., `fish_1`, `plant`, `shrimp`) that
|
16
|
+
# returns a single evolvable instance. You can evolve all populations together using the
|
17
|
+
# `evolve` method, or per population.
|
18
|
+
#
|
19
|
+
# **Key Features**
|
20
|
+
# - Define a community composed of named populations
|
21
|
+
# - Automatically generate accessors for each evolvable instance
|
22
|
+
# - Coordinate evolution across populations through a shared interface
|
23
|
+
# - Evolve all populations in a single call with `evolve(...)`
|
24
|
+
#
|
25
|
+
# This `FishTank` example sets up a community with four named populations:
|
26
|
+
#
|
27
|
+
# ```ruby
|
28
|
+
# class FishTank
|
29
|
+
# include Evolvable::Community
|
30
|
+
#
|
31
|
+
# evolvable_community fish_1: Fish,
|
32
|
+
# fish_2: Fish,
|
33
|
+
# plant: AquariumPlant,
|
34
|
+
# shrimp: CleanerShrimp
|
35
|
+
#
|
36
|
+
# def describe_tank
|
37
|
+
# puts "🐟 Fish 1: #{fish_1.name} (#{fish_1.color})"
|
38
|
+
# puts "🐟 Fish 2: #{fish_2.name} (#{fish_2.color})"
|
39
|
+
# puts "🌿 Plant: #{plant.name} (#{plant.color})"
|
40
|
+
# puts "🦐 Shrimp: #{shrimp.name} (#{shrimp.color})"
|
41
|
+
# end
|
42
|
+
# end
|
43
|
+
# ```
|
44
|
+
#
|
45
|
+
# Initialize the community, describe the tank, and evolve each population:
|
46
|
+
#
|
47
|
+
# ```ruby
|
48
|
+
# tank = FishTank.new_community
|
49
|
+
# tank.describe_tank
|
50
|
+
# tank.evolve
|
51
|
+
# ```
|
52
|
+
#
|
53
|
+
module Community
|
54
|
+
def self.included(base)
|
55
|
+
base.extend(ClassMethods)
|
56
|
+
end
|
57
|
+
|
58
|
+
module ClassMethods
|
59
|
+
# Creates a new instance of the community
|
60
|
+
# @return [Object] A new community instance
|
61
|
+
def new_community
|
62
|
+
initialize_community
|
63
|
+
end
|
64
|
+
|
65
|
+
# Initializes a new community instance
|
66
|
+
# This method exists as a hook for classes that include this module to override
|
67
|
+
# and customize the community instantiation process.
|
68
|
+
# @return [Object] A new community instance
|
69
|
+
def initialize_community
|
70
|
+
new
|
71
|
+
end
|
72
|
+
|
73
|
+
def load_community(data)
|
74
|
+
Serializer.load(data)
|
75
|
+
end
|
76
|
+
|
77
|
+
attr_accessor :community_config
|
78
|
+
|
79
|
+
# Configures a class as an evolvable community with specified populations
|
80
|
+
# @param community_config [Hash] A hash mapping names to population classes
|
81
|
+
# @return [void]
|
82
|
+
def evolvable_community(community_config)
|
83
|
+
self.community_config = community_config
|
84
|
+
community_config.each_key do |name|
|
85
|
+
method_name = name.to_sym
|
86
|
+
define_method(method_name) do
|
87
|
+
instance_variable_get("@#{method_name}") || instance_variable_set("@#{method_name}", find_or_new_evolvable(method_name))
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Returns a hash of population instances by name
|
93
|
+
# @return [Hash] A hash mapping names to population instances
|
94
|
+
def populations_by_name
|
95
|
+
@populations_by_name ||= community_config.inject({}) do |hash, (name, population_klass)|
|
96
|
+
hash[name.to_sym] = population_klass.new_population(size: 0)
|
97
|
+
hash
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Adds a population to the community
|
102
|
+
# @param name [String, Symbol] The name of the population
|
103
|
+
# @param population [Object] The population instance
|
104
|
+
# @return [Object] The added population
|
105
|
+
def add_population(name, population)
|
106
|
+
populations_by_name[name.to_sym] = population
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Returns a hash of population instances by name
|
111
|
+
# @return [Hash] A hash mapping names to population instances
|
112
|
+
def populations_by_name
|
113
|
+
@populations_by_name ||= self.class.populations_by_name.dup
|
114
|
+
end
|
115
|
+
|
116
|
+
# Returns an array of population instances
|
117
|
+
# @return [Array] An array of population instances
|
118
|
+
def populations
|
119
|
+
populations_by_name.values
|
120
|
+
end
|
121
|
+
|
122
|
+
# Evolves all populations in the community
|
123
|
+
# @return [void]
|
124
|
+
def evolve(...)
|
125
|
+
populations.each { |population| population.evolve(...) }
|
126
|
+
end
|
127
|
+
|
128
|
+
# Resets the populations hash to an empty hash
|
129
|
+
# @return [Hash] An empty hash
|
130
|
+
def reset_populations
|
131
|
+
@populations_by_name = {}
|
132
|
+
end
|
133
|
+
|
134
|
+
# Adds a population to the community
|
135
|
+
# @param name [String, Symbol] The name of the population
|
136
|
+
# @param population [Object] The population instance
|
137
|
+
# @return [Object] The added population
|
138
|
+
def add_population(name, population)
|
139
|
+
populations_by_name[name.to_sym] = population
|
140
|
+
end
|
141
|
+
|
142
|
+
# Finds a population by name
|
143
|
+
# @param name [String, Symbol] The name of the population
|
144
|
+
# @return [Object, nil] The population instance or nil if not found
|
145
|
+
def find_population(name)
|
146
|
+
populations_by_name[name.to_sym]
|
147
|
+
end
|
148
|
+
|
149
|
+
# Finds an evolvable instance by name
|
150
|
+
# @param name [String, Symbol] The name of the evolvable
|
151
|
+
# @return [Object, nil] The evolvable instance or nil if not found
|
152
|
+
def find_evolvable(name)
|
153
|
+
evolvables_by_name[name.to_sym]
|
154
|
+
end
|
155
|
+
|
156
|
+
# Finds an existing evolvable instance by name or creates a new one
|
157
|
+
# @param name [String, Symbol] The name of the evolvable
|
158
|
+
# @return [Object] The existing or new evolvable instance
|
159
|
+
def find_or_new_evolvable(name)
|
160
|
+
find_evolvable(name) || new_evolvable(name)
|
161
|
+
end
|
162
|
+
|
163
|
+
# Creates a new evolvable instance from the corresponding population
|
164
|
+
# @param name [String, Symbol] The name of the evolvable
|
165
|
+
# @return [Object] The new evolvable instance
|
166
|
+
def new_evolvable(name)
|
167
|
+
evolvables_by_name[name.to_sym] = find_population(name).new_evolvable
|
168
|
+
end
|
169
|
+
|
170
|
+
# Adds an evolvable instance to the community
|
171
|
+
# @param name [String, Symbol] The name of the evolvable
|
172
|
+
# @param evolvable [Object] The evolvable instance
|
173
|
+
# @return [Object] The added evolvable
|
174
|
+
def add_evolvable(name, evolvable)
|
175
|
+
evolvables_by_name[name.to_sym] = evolvable
|
176
|
+
end
|
177
|
+
|
178
|
+
# Returns a hash of evolvable instances by name
|
179
|
+
# @return [Hash] A hash mapping names to evolvable instances
|
180
|
+
def evolvables_by_name
|
181
|
+
@evolvables_by_name ||= {}
|
182
|
+
end
|
183
|
+
|
184
|
+
# Resets the evolvables hash to an empty hash
|
185
|
+
# @return [Hash] An empty hash
|
186
|
+
def reset_evolvables
|
187
|
+
@evolvables_by_name = {}
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
data/lib/evolvable/count_gene.rb
CHANGED
@@ -1,10 +1,36 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Evolvable
|
4
|
+
#
|
5
|
+
# The CountGene class handles the dynamic count of genes in evolvable instances.
|
6
|
+
# When a gene is defined with a range for `count:` (e.g., `count: 2..8`), a CountGene
|
7
|
+
# is created to manage this count, allowing the number of genes to change over
|
8
|
+
# successive generations.
|
9
|
+
#
|
10
|
+
# @example Define a melody with a variable number of notes (4 to 16)
|
11
|
+
# class Melody
|
12
|
+
# include Evolvable
|
13
|
+
#
|
14
|
+
# gene :notes, type: NoteGene, count: 4..16
|
15
|
+
#
|
16
|
+
# # The actual number of notes can change during evolution
|
17
|
+
# def play
|
18
|
+
# puts "Playing melody with #{notes.count} notes"
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
#
|
4
22
|
class CountGene
|
5
23
|
include Gene
|
6
24
|
|
7
25
|
class << self
|
26
|
+
#
|
27
|
+
# Combines two count genes to produce a new count gene.
|
28
|
+
# The combination strategy is randomly selected from LAMBDAS.
|
29
|
+
#
|
30
|
+
# @param gene_a [CountGene] The first count gene
|
31
|
+
# @param gene_b [CountGene] The second count gene
|
32
|
+
# @return [CountGene] A new count gene with a combined count value
|
33
|
+
#
|
8
34
|
def combine(gene_a, gene_b)
|
9
35
|
min = gene_a.min_count
|
10
36
|
max = gene_a.max_count
|
@@ -12,29 +38,68 @@ module Evolvable
|
|
12
38
|
new(range: gene_a.range, count: count)
|
13
39
|
end
|
14
40
|
|
41
|
+
#
|
42
|
+
# Available strategies for combining count genes.
|
43
|
+
# These lambdas determine how two count genes are merged during evolution.
|
44
|
+
#
|
45
|
+
# 1. Random selection with slight mutation (-1, 0, or +1)
|
46
|
+
# 2. Average of the two counts
|
47
|
+
#
|
15
48
|
LAMBDAS = [->(a, b) { [a, b].sample.count + rand(-1..1) },
|
16
49
|
->(a, b) { a.count + b.count / 2 }].freeze
|
17
50
|
|
51
|
+
#
|
52
|
+
# Selects a random combination strategy from LAMBDAS.
|
53
|
+
#
|
54
|
+
# @return [Proc] A lambda function for combining two count genes
|
55
|
+
#
|
18
56
|
def combination
|
19
57
|
LAMBDAS.sample
|
20
58
|
end
|
21
59
|
end
|
22
60
|
|
61
|
+
#
|
62
|
+
# Initializes a new CountGene instance.
|
63
|
+
#
|
64
|
+
# @param range [Range] The valid range for the count value
|
65
|
+
# @param count [Integer, nil] Optional initial count value, randomly selected from range if nil
|
66
|
+
#
|
23
67
|
def initialize(range:, count: nil)
|
24
68
|
@range = range
|
25
69
|
@count = count
|
26
70
|
end
|
27
71
|
|
72
|
+
#
|
73
|
+
# The valid range for the count value.
|
74
|
+
#
|
75
|
+
# @return [Range] The range of possible count values
|
76
|
+
#
|
28
77
|
attr_reader :range
|
29
78
|
|
79
|
+
#
|
80
|
+
# Returns the current count value.
|
81
|
+
# If not already set, randomly selects a value from the range.
|
82
|
+
#
|
83
|
+
# @return [Integer] The number of genes to create
|
84
|
+
#
|
30
85
|
def count
|
31
86
|
@count ||= rand(@range)
|
32
87
|
end
|
33
88
|
|
89
|
+
#
|
90
|
+
# The minimum allowed count value.
|
91
|
+
#
|
92
|
+
# @return [Integer] The minimum value from the range
|
93
|
+
#
|
34
94
|
def min_count
|
35
95
|
@range.min
|
36
96
|
end
|
37
97
|
|
98
|
+
#
|
99
|
+
# The maximum allowed count value.
|
100
|
+
#
|
101
|
+
# @return [Integer] The maximum value from the range
|
102
|
+
#
|
38
103
|
def max_count
|
39
104
|
@range.max
|
40
105
|
end
|
@@ -12,18 +12,11 @@ module Evolvable
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def evaluate(evolvable)
|
15
|
-
-(evolvable.
|
15
|
+
-(evolvable.fitness - value).abs
|
16
16
|
end
|
17
17
|
|
18
18
|
def met?(evolvable)
|
19
|
-
evolvable.
|
19
|
+
evolvable.fitness == value
|
20
20
|
end
|
21
21
|
end
|
22
|
-
|
23
|
-
#
|
24
|
-
# @deprecated
|
25
|
-
# Will be removed in 2.0.
|
26
|
-
# Use {EqualizeGoal} instead
|
27
|
-
#
|
28
|
-
class Goal::Equalize < EqualizeGoal; end
|
29
22
|
end
|