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.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +2 -0
  3. data/CHANGELOG.md +24 -0
  4. data/Gemfile +2 -2
  5. data/Gemfile.lock +44 -25
  6. data/README.md +498 -190
  7. data/README_YARD.md +85 -166
  8. data/bin/console +10 -19
  9. data/docs/Evolvable/ClassMethods.html +1233 -0
  10. data/docs/Evolvable/Community/ClassMethods.html +708 -0
  11. data/docs/Evolvable/Community.html +1342 -0
  12. data/docs/Evolvable/CountGene.html +886 -0
  13. data/docs/Evolvable/EqualizeGoal.html +347 -0
  14. data/docs/Evolvable/Error.html +134 -0
  15. data/docs/Evolvable/Evaluation.html +773 -0
  16. data/docs/Evolvable/Evolution.html +616 -0
  17. data/docs/Evolvable/Gene/ClassMethods.html +413 -0
  18. data/docs/Evolvable/Gene.html +522 -0
  19. data/docs/Evolvable/GeneCluster/ClassMethods.html +431 -0
  20. data/docs/Evolvable/GeneCluster.html +280 -0
  21. data/docs/Evolvable/GeneCombination.html +515 -0
  22. data/docs/Evolvable/GeneSpace.html +619 -0
  23. data/docs/Evolvable/Genome.html +1070 -0
  24. data/docs/Evolvable/Goal.html +500 -0
  25. data/docs/Evolvable/MaximizeGoal.html +348 -0
  26. data/docs/Evolvable/MinimizeGoal.html +348 -0
  27. data/docs/Evolvable/Mutation.html +729 -0
  28. data/docs/Evolvable/PointCrossover.html +444 -0
  29. data/docs/Evolvable/Population.html +2826 -0
  30. data/docs/Evolvable/RigidCountGene.html +501 -0
  31. data/docs/Evolvable/Selection.html +594 -0
  32. data/docs/Evolvable/Serializer.html +293 -0
  33. data/docs/Evolvable/UniformCrossover.html +286 -0
  34. data/docs/Evolvable.html +1619 -0
  35. data/docs/_index.html +341 -0
  36. data/docs/class_list.html +54 -0
  37. data/docs/css/common.css +1 -0
  38. data/docs/css/full_list.css +58 -0
  39. data/docs/css/style.css +503 -0
  40. data/docs/file.README.html +750 -0
  41. data/docs/file_list.html +59 -0
  42. data/docs/frames.html +22 -0
  43. data/docs/index.html +750 -0
  44. data/docs/js/app.js +344 -0
  45. data/docs/js/full_list.js +242 -0
  46. data/docs/js/jquery.js +4 -0
  47. data/docs/method_list.html +1302 -0
  48. data/docs/top-level-namespace.html +110 -0
  49. data/evolvable.gemspec +6 -6
  50. data/examples/ascii_art.rb +5 -9
  51. data/examples/stickman.rb +25 -33
  52. data/{examples/hello_world.rb → exe/hello_evolvable_world} +46 -30
  53. data/lib/evolvable/community.rb +190 -0
  54. data/lib/evolvable/count_gene.rb +65 -0
  55. data/lib/evolvable/equalize_goal.rb +2 -9
  56. data/lib/evolvable/evaluation.rb +113 -14
  57. data/lib/evolvable/evolution.rb +38 -15
  58. data/lib/evolvable/gene.rb +124 -25
  59. data/lib/evolvable/gene_cluster.rb +106 -0
  60. data/lib/evolvable/gene_combination.rb +57 -16
  61. data/lib/evolvable/gene_space.rb +111 -0
  62. data/lib/evolvable/genome.rb +32 -4
  63. data/lib/evolvable/goal.rb +19 -24
  64. data/lib/evolvable/maximize_goal.rb +2 -9
  65. data/lib/evolvable/minimize_goal.rb +3 -9
  66. data/lib/evolvable/mutation.rb +87 -41
  67. data/lib/evolvable/point_crossover.rb +24 -4
  68. data/lib/evolvable/population.rb +258 -84
  69. data/lib/evolvable/rigid_count_gene.rb +36 -0
  70. data/lib/evolvable/selection.rb +68 -14
  71. data/lib/evolvable/serializer.rb +46 -0
  72. data/lib/evolvable/uniform_crossover.rb +30 -6
  73. data/lib/evolvable/version.rb +2 -1
  74. data/lib/evolvable.rb +268 -107
  75. metadata +57 -36
  76. data/examples/images/diagram.png +0 -0
  77. data/exe/hello +0 -16
  78. data/lib/evolvable/error/undefined_method.rb +0 -7
  79. data/lib/evolvable/search_space.rb +0 -181
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evolvable
4
+ #
5
+ # The gene space defines the structure of an evolvable's genome.
6
+ # It acts as a blueprint that describes what kinds of genes each
7
+ # evolvable instance should have—and how many.
8
+ #
9
+ # At runtime, Evolvable::GeneSpace is responsible for interpreting this blueprint,
10
+ # constructing genomes, and managing gene configurations. You typically won’t need to
11
+ # interact with GeneSpace directly.
12
+ #
13
+ # @see Evolvable::Gene
14
+ # @see Evolvable::GeneCluster
15
+ # @see Evolvable::GeneCombination
16
+ #
17
+ class GeneSpace
18
+ class << self
19
+ def build(config, evolvable_class = nil)
20
+ if config.is_a?(GeneSpace)
21
+ config.evolvable_class = evolvable_class if evolvable_class
22
+ config
23
+ else
24
+ new(config: config, evolvable_class: evolvable_class)
25
+ end
26
+ end
27
+ end
28
+
29
+ def initialize(config: {}, evolvable_class: nil)
30
+ @evolvable_class = evolvable_class
31
+ @config = normalize_config(config)
32
+ end
33
+
34
+ attr_accessor :evolvable_class, :config
35
+
36
+ def new_genome
37
+ Genome.new(config: new_genome_config)
38
+ end
39
+
40
+ def merge_gene_space(val)
41
+ val = val.config if val.is_a?(self.class)
42
+ @config.merge normalize_config(val)
43
+ end
44
+
45
+ def merge_gene_space!(val)
46
+ val = val.config if val.is_a?(self.class)
47
+ @config.merge! normalize_config(val)
48
+ end
49
+
50
+ private
51
+
52
+ def normalize_config(config)
53
+ normalize_hash_config(config)
54
+ end
55
+
56
+ def normalize_hash_config(config)
57
+ config.each do |gene_key, gene_config|
58
+ next unless gene_config[:type]
59
+
60
+ gene_class = lookup_gene_class(gene_config[:type])
61
+ gene_class.key = gene_key
62
+ gene_config[:class] = gene_class
63
+ end
64
+ end
65
+
66
+ def lookup_gene_class(class_name)
67
+ return class_name if class_name.is_a?(Class)
68
+
69
+ (@evolvable_class || Object).const_get(class_name)
70
+ end
71
+
72
+ def new_genome_config
73
+ genome_config = {}
74
+ config.each do |gene_key, gene_config|
75
+ genome_config[gene_key] = new_gene_config(gene_config)
76
+ end
77
+ genome_config
78
+ end
79
+
80
+ def new_gene_config(gene_config)
81
+ count_gene = init_count_gene(gene_config)
82
+ gene_class = gene_config[:class]
83
+ genes = Array.new(count_gene.count) { gene_class.new }
84
+ { count_gene: count_gene, genes: genes }
85
+ end
86
+
87
+ def init_count_gene(gene_config)
88
+ min = gene_config[:min_count]
89
+ max = gene_config[:max_count]
90
+ return init_min_max_count_gene(min, max) if min || max
91
+
92
+ count = gene_config[:count] || 1
93
+ case count
94
+ when Numeric
95
+ RigidCountGene.new(count)
96
+ when Range
97
+ CountGene.new(range: count)
98
+ when String
99
+ Object.const_get(gene_config[:count]).new
100
+ when Class
101
+ gene_config[:count].new
102
+ end
103
+ end
104
+
105
+ def init_min_max_count_gene(min, max)
106
+ min ||= 1
107
+ max ||= min * 10
108
+ CountGene.new(range: min..max)
109
+ end
110
+ end
111
+ end
@@ -2,8 +2,22 @@
2
2
 
3
3
  module Evolvable
4
4
  #
5
- # @readme
6
- # TODO...
5
+ # The Genome class represents the fully instantiated genetic blueprint of an evolvable instance.
6
+ # It stores all gene data in a structured, accessible form and provides methods to inspect,
7
+ # manipulate, and serialize that genetic information.
8
+ #
9
+ # A genome consists of:
10
+ # - A hash of gene configurations organized by key
11
+ # - Count genes that determine how many instances of each gene type are present
12
+ # - The actual gene instances used by the evolvable
13
+ #
14
+ # The genome acts as the bridge between the gene space (definition) and the
15
+ # evolvable instance (implementation), enabling flexible gene access and
16
+ # supporting dynamic mutation or crossover behavior.
17
+ #
18
+ # @see Evolvable::GeneSpace
19
+ # @see Evolvable::GeneCombination
20
+ # @see Evolvable::Gene
7
21
  #
8
22
  class Genome
9
23
  extend Forwardable
@@ -18,8 +32,15 @@ module Evolvable
18
32
 
19
33
  attr_reader :config
20
34
 
35
+ alias to_h config
36
+
21
37
  #
22
- # Returns the first gene with the given key. In the Melody example above, the instrument gene has the key `:instrument` so we might write something like: `instrument_gene = melody.find_gene(instrument)`
38
+ # Returns the first gene with the given key. In the Melody example above, the instrument
39
+ # gene has the key `:instrument` so we might write something like:
40
+ #
41
+ # ```ruby
42
+ # instrument_gene = melody.find_gene(instrument)
43
+ # ```
23
44
  #
24
45
  # @param [<Type>] key <description>
25
46
  #
@@ -30,7 +51,10 @@ module Evolvable
30
51
  end
31
52
 
32
53
  #
33
- # Returns an array of genes that have the given key. Gene keys are defined in the [EvolvableClass.search_space](#evolvableclasssearch_space) method. In the Melody example above, the key for the note genes would be `:notes`. The following would return an array of them: `note_genes = melody.find_genes(:notes)`
54
+ # Returns an array of genes that have the given key. Gene keys are defined using the
55
+ # [EvolvableClass.gene](https://mattruzicka.github.io/evolvable/Evolvable/ClassMethods#gene-instance_method)
56
+ # macro method. In the Melody example above, the key for the note genes would be `:notes`.
57
+ # The following would return an array of them: `note_genes = melody.find_genes(:notes)`
34
58
  #
35
59
  # @param [<Type>] *keys <description>
36
60
  #
@@ -75,6 +99,10 @@ module Evolvable
75
99
  @config.flat_map { |_gene_key, gene_config| gene_config[:genes] }
76
100
  end
77
101
 
102
+ def merge!(other_genome)
103
+ @config.merge!(other_genome.config)
104
+ end
105
+
78
106
  def inspect
79
107
  self.class.name
80
108
  end
@@ -1,36 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Evolvable
4
-
5
- #
6
- # The goal for a population can be specified via assignment - `population.goal = Evolvable::Goal::Equalize.new` - or by passing an evaluation object when [initializing a population](#evolvablepopulationnew).
7
4
  #
8
- # You can intialize the `Evolvable::Evaluation` object with any goal object like this:
5
+ # @see Evolvable::Evaluation
9
6
  #
10
- # You can implement custom goal object like so:
7
+ # @readme
8
+ # **Custom Goals**
11
9
  #
12
- # @example
13
- # goal_object = SomeGoal.new(value: 100)
14
- # Evolvable::Evaluation.new(goal_object)
10
+ # You can create custom goals by subclassing `Evolvable::Goal` and implementing:
11
+ # - `evaluate(evolvable)`: Return a value that for sorting evolvables
12
+ # - `met?(evolvable)`: Returns true when the goal value is reached
15
13
  #
16
- # or more succinctly like this:
14
+ # @example Example goal implementation that prioritizes evolvables with fitness values within a specific range
15
+ # class YourRangeGoal < Evolvable::Goal
16
+ # def value
17
+ # @value ||= 0..100
18
+ # end
17
19
  #
18
- # @example
19
- # Evolvable::Evaluation.new(:maximize) # Uses default goal value of Float::INFINITY
20
- # Evolvable::Evaluation.new(maximize: 50) # Sets goal value to 50
21
- # Evolvable::Evaluation.new(:minimize) # Uses default goal value of -Float::INFINITY
22
- # Evolvable::Evaluation.new(minimize: 100) # Sets goal value to 100
23
- # Evolvable::Evaluation.new(:equalize) # Uses default goal value of 0
24
- # Evolvable::Evaluation.new(equalize: 1000) # Sets goal value to 1000
20
+ # def evaluate(evolvable)
21
+ # return 1 if value.include?(evolvable.fitness)
25
22
  #
26
- # @example
27
- # class CustomGoal < Evolvable::Goal
28
- # def evaluate(instance)
29
- # # Required by Evolvable::Evaluation in order to sort instances in preparation for selection.
23
+ # min, max = value.minmax
24
+ # -[(min - evolvable.fitness).abs, (max - evolvable.fitness).abs].min
30
25
  # end
31
26
  #
32
- # def met?(instance)
33
- # # Used by Evolvable::Population#evolve to stop evolving when the goal value has been reached.
27
+ # def met?(evolvable)
28
+ # value.include?(evolvable.fitness)
34
29
  # end
35
30
  # end
36
31
  #
@@ -42,11 +37,11 @@ module Evolvable
42
37
  attr_accessor :value
43
38
 
44
39
  def evaluate(_evolvable)
45
- raise Errors::UndefinedMethod, "#{self.class.name}##{__method__}"
40
+ raise Error, "Undefined method: #{self.class.name}##{__method__}"
46
41
  end
47
42
 
48
43
  def met?(_evolvable)
49
- raise Errors::UndefinedMethod, "#{self.class.name}##{__method__}"
44
+ raise Error, "Undefined method: #{self.class.name}##{__method__}"
50
45
  end
51
46
  end
52
47
  end
@@ -13,18 +13,11 @@ module Evolvable
13
13
  end
14
14
 
15
15
  def evaluate(evolvable)
16
- evolvable.value
16
+ evolvable.fitness
17
17
  end
18
18
 
19
19
  def met?(evolvable)
20
- evolvable.value >= value
20
+ evolvable.fitness >= value
21
21
  end
22
22
  end
23
-
24
- #
25
- # @deprecated
26
- # Will be removed in 2.0.
27
- # Use {MaximizeGoal} instead
28
- #
29
- class Goal::Maximize < MaximizeGoal; end
30
23
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Evolvable
4
+ #
4
5
  # Prioritizes instances with lesser values.
5
6
  #
6
7
  # The default goal value is `-Float::INFINITY`, but it can be reassigned
@@ -12,18 +13,11 @@ module Evolvable
12
13
  end
13
14
 
14
15
  def evaluate(evolvable)
15
- -evolvable.value
16
+ -evolvable.fitness
16
17
  end
17
18
 
18
19
  def met?(evolvable)
19
- evolvable.value <= value
20
+ evolvable.fitness <= value
20
21
  end
21
22
  end
22
-
23
- #
24
- # @deprecated
25
- # Will be removed in 2.0.
26
- # Use {MinimizeGoal} instead
27
- #
28
- class Goal::Minimize < MinimizeGoal; end
29
23
  end
@@ -3,70 +3,101 @@
3
3
  module Evolvable
4
4
  #
5
5
  # @readme
6
- # Mutation serves the role of increasing genetic variation. When an evolvable
7
- # undergoes a mutation, one or more of its genes are replaced by newly
8
- # initialized ones. In effect, a gene mutation invokes a new random outcome
9
- # from the genetic search space.
6
+ # Mutation introduces genetic variation by randomly replacing genes with new
7
+ # ones. This helps the population explore new areas of the solution space
8
+ # and prevents premature convergence on suboptimal solutions.
10
9
  #
11
- # Mutation frequency is configurable using the `probability` and `rate`
12
- # parameters.
10
+ # Mutation is controlled by two key parameters:
11
+ # - **probability**: Likelihood that an individual will undergo mutation (range: 0.0–1.0)
12
+ # - **rate**: Fraction of genes to mutate within those individuals (range: 0.0–1.0)
13
13
  #
14
- # @example
15
- # # Show how to initialize/assign population with a specific mutation object
14
+ # A typical strategy is to start with higher mutation to encourage exploration:
15
+ #
16
+ # ```ruby
17
+ # population = MyEvolvable.new_population(
18
+ # mutation: { probability: 0.4, rate: 0.2 }
19
+ # )
20
+ # ```
21
+ #
22
+ # Then later reduce the mutation rate to focus on refinement and convergence:
23
+ #
24
+ # ```ruby
25
+ # population.mutation_probability = 0.1
26
+ # population.mutation_rate = 0.05
27
+ # ```
16
28
  #
17
29
  class Mutation
18
30
  extend Forwardable
19
31
 
32
+ #
33
+ # The default probability of mutation (3%).
34
+ # This is used when no probability is specified.
35
+ #
20
36
  DEFAULT_PROBABILITY = 0.03
21
37
 
22
38
  #
23
39
  # Initializes a new mutation object.
24
40
  #
25
- # Keyword arguments:
26
- #
27
- # #### probability
28
- # The probability that a particular instance undergoes a mutation.
29
- # By default, the probability is 0.03 which translates to 3%.
30
- # If initialized with a `rate`, the probability will be 1 which
31
- # means all genes _can_ undergo mutation, but actual gene mutations
32
- # will be subject to the given mutation rate.
33
- # #### rate
34
- # the rate at which individual genes mutate. The default rate is 0 which,
35
- # when combined with a non-zero `probability` (the default), means that
36
- # one gene for each instance that undergoes mutation will change.
37
- # If a rate is given, but no `probability` is given, then the `probability`
38
- # will bet set to 1 which always defers to the mutation rate.
39
- #
40
- # To summarize, the `probability` represents the chance of mutation on
41
- # the instance level and the `rate` represents the chance on the gene level.
42
- # The `probability` and `rate` can be any number from 0 to 1. When the
43
- # `probability` is 0, no mutation will ever happen. When the `probability`
44
- # is not 0 but the rate is 0, then any instance that undergoes mutation
45
- # will only receive one mutant gene. If the rate is not 0, then if an
46
- # instance has been chosen to undergo mutation, each of its genes will
47
- # mutate with a probability as defined by the `rate`.
48
- #
49
- # @example Example Initializations:
50
- # Evolvable::Mutation.new # Approximately #{DEFAULT_PROBABILITY} of instances will receive one mutant gene
51
- # Evolvable::Mutation.new(probability: 0.5) # Approximately 50% of instances will receive one mutant gene
52
- # Evolvable::Mutation.new(rate: 0.03) # Approximately 3% of all genes in the population will mutate.
53
- # Evolvable::Mutation.new(probability: 0.3, rate: 0.03) # Approximately 30% of instances will have approximately 3% of their genes mutated.
54
- #
55
- # Custom mutation objects must implement the `#call` method which accepts the population as the first object.
41
+ # @example Basic initialization patterns
42
+ # # Default: 3% of instances get one mutated gene
43
+ # Evolvable::Mutation.new
44
+ #
45
+ # # 50% of instances get one mutated gene
46
+ # Evolvable::Mutation.new(probability: 0.5)
47
+ #
48
+ # # All instances are considered, with 3% of genes mutating in each
49
+ # Evolvable::Mutation.new(rate: 0.03)
50
+ #
51
+ # # 30% of instances have 3% of their genes mutated
52
+ # Evolvable::Mutation.new(probability: 0.3, rate: 0.03)
53
+ #
54
+ # When rate is specified but probability isn't, probability defaults to 1.0.
55
+ # When rate is 0 or not specified, only one gene is mutated per affected instance.
56
+ #
57
+ # @param probability [Float, nil] Chance of an instance being mutated (0.0-1.0)
58
+ # @param rate [Float, nil] Chance of each gene mutating when an instance is selected (0.0-1.0)
56
59
  #
57
60
  def initialize(probability: nil, rate: nil)
58
61
  @probability = probability || (rate ? 1 : DEFAULT_PROBABILITY)
59
62
  @rate = rate
60
63
  end
61
64
 
62
- attr_accessor :probability,
63
- :rate
65
+ #
66
+ # The probability that an evolvable instance will undergo mutation.
67
+ # Value between 0.0 (never) and 1.0 (always).
68
+ #
69
+ # @return [Float] The mutation probability
70
+ #
71
+ attr_accessor :probability
64
72
 
73
+ #
74
+ # The rate at which genes mutate within an instance.
75
+ # Value between 0.0 (no genes mutate) and 1.0 (all genes likely to mutate).
76
+ # When nil, exactly one random gene is mutated per instance.
77
+ #
78
+ # @return [Float, nil] The mutation rate
79
+ #
80
+ attr_accessor :rate
81
+
82
+ #
83
+ # Applies mutations to the population's evolvables based on the
84
+ # configured probability and rate.
85
+ #
86
+ # @param population [Evolvable::Population] The population to mutate
87
+ # @return [Evolvable::Population] The mutated population
88
+ #
65
89
  def call(population)
66
90
  mutate_evolvables(population.evolvables) unless probability.zero?
67
91
  population
68
92
  end
69
93
 
94
+ #
95
+ # Mutates a collection of evolvable instances based on the mutation
96
+ # probability and rate.
97
+ #
98
+ # @param evolvables [Array<Evolvable>] The collection of evolvable instances to potentially mutate
99
+ # @return [Array<Evolvable>] The potentially mutated evolvables
100
+ #
70
101
  def mutate_evolvables(evolvables)
71
102
  evolvables.each do |evolvable|
72
103
  next unless rand <= probability
@@ -77,6 +108,14 @@ module Evolvable
77
108
 
78
109
  private
79
110
 
111
+ #
112
+ # Mutates genes in the given collection based on the mutation rate.
113
+ # If a rate is set, each gene has a chance to mutate according to the rate.
114
+ # If no rate is set, a single random gene is mutated.
115
+ #
116
+ # @param genes [Array<Evolvable::Gene>] The collection of genes to potentially mutate
117
+ # @return [void]
118
+ #
80
119
  def mutate_genes(genes)
81
120
  genes_count = genes.count
82
121
  return if genes_count.zero?
@@ -86,6 +125,13 @@ module Evolvable
86
125
  genes_count.times { |index| mutate_gene_by_index(genes, index) if rand <= rate }
87
126
  end
88
127
 
128
+ #
129
+ # Replaces a gene at the specified index with a new instance of the same gene class.
130
+ #
131
+ # @param genes [Array<Evolvable::Gene>] The collection of genes
132
+ # @param gene_index [Integer] The index of the gene to mutate
133
+ # @return [void]
134
+ #
89
135
  def mutate_gene_by_index(genes, gene_index)
90
136
  genes[gene_index] = genes[gene_index].class.new
91
137
  end
@@ -2,10 +2,30 @@
2
2
 
3
3
  module Evolvable
4
4
  #
5
- # Supports single and multi-point crossover. The default is single-point
6
- # crossover via a `points_count` of 1 which can be changed on an existing population
7
- # (`population.crossover.points_count = 5`) or during initialization
8
- # (`Evolvable::PointCrossover.new(5)`)
5
+ # @readme
6
+ # A classic genetic algorithm strategy that performs single or multi-point crossover
7
+ # by selecting random positions in the genome and swapping gene segments between parents.
8
+ #
9
+ # - **Single-point crossover (default):** Swaps all genes after a randomly chosen position.
10
+ # - **Multi-point crossover:** Alternates segments between multiple randomly chosen points.
11
+ #
12
+ # Best for:
13
+ # - Preserving beneficial gene blocks
14
+ # - Problems where related traits are located near each other
15
+ #
16
+ # Set your population to use this strategy during initialization with:
17
+ #
18
+ # ```ruby
19
+ # population = MyEvolvable.new_population(
20
+ # combination: Evolvable::PointCrossover.new(points_count: 2)
21
+ # )
22
+ # ```
23
+ #
24
+ # Or update an existing population:
25
+ #
26
+ # ```ruby
27
+ # population.combination = Evolvable::PointCrossover.new(points_count: 3)
28
+ # ```
9
29
  #
10
30
  class PointCrossover
11
31
  def initialize(points_count: 1)