Skip to content

Commit 34dcccc

Browse files
author
Eric Matte
committed
Generic genetic optimizer + tests
1 parent 5a6271f commit 34dcccc

File tree

9 files changed

+253
-64
lines changed

9 files changed

+253
-64
lines changed

.rubocop.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ Metrics/LineLength:
1010
Metrics/MethodLength:
1111
Max: 35
1212

13+
Metrics/ClassLength:
14+
Enabled: false
15+
1316
Gemspec/OrderedDependencies:
1417
Enabled: false
1518

.vscode/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
2-
"editor.formatOnSave": false,
2+
"editor.formatOnSave": true,
3+
"editor.formatOnSaveTimeout": 5000,
34
"editor.tabSize": 2,
45
"files.trimTrailingWhitespace": true,
56
"files.insertFinalNewline": true,

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ source "https://rubygems.org"
33
# Specify your gem's dependencies in genetic-algorithm-resolver.gemspec
44
gemspec
55

6+
gem "rubocop"

Gemfile.lock

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,30 @@ PATH
66
GEM
77
remote: https://rubygems.org/
88
specs:
9+
ast (2.4.0)
910
debase (0.2.4.1)
1011
debase-ruby_core_source (>= 0.10.2)
1112
debase-ruby_core_source (0.10.6)
13+
jaro_winkler (1.5.2)
1214
minitest (5.11.3)
15+
parallel (1.13.0)
16+
parser (2.6.2.0)
17+
ast (~> 2.4.0)
18+
psych (3.1.0)
19+
rainbow (3.0.0)
1320
rake (12.3.3)
21+
rubocop (0.66.0)
22+
jaro_winkler (~> 1.5.1)
23+
parallel (~> 1.10)
24+
parser (>= 2.5, != 2.5.1.1)
25+
psych (>= 3.1.0)
26+
rainbow (>= 2.2.2, < 4.0)
27+
ruby-progressbar (~> 1.7)
28+
unicode-display_width (>= 1.4.0, < 1.6)
1429
ruby-debug-ide (0.7.0)
1530
rake (>= 0.8.1)
31+
ruby-progressbar (1.10.0)
32+
unicode-display_width (1.5.0)
1633

1734
PLATFORMS
1835
ruby
@@ -23,6 +40,7 @@ DEPENDENCIES
2340
genetic-algorithm-resolver!
2441
minitest (~> 5.0)
2542
rake (~> 12.3.2)
43+
rubocop
2644
ruby-debug-ide (~> 0.7)
2745

2846
BUNDLED WITH

lib/genetic/algorithm/chromosome.rb

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
1-
# A member of a given population
2-
class Genetic::Algorithm::Chromosome
3-
attr_accessor :genes
4-
attr_accessor :fitness
1+
module Genetic
2+
module Algorithm
3+
# A member of a given population
4+
class Chromosome
5+
attr_reader :genes
6+
attr_reader :fitness
57

6-
def initialize(genes:, fitness:)
7-
@genes = genes
8-
@fitness = fitness
9-
end
8+
def initialize(genes, fitness_calculator)
9+
@genes = genes
10+
@fitness = fitness_calculator.call(genes)
11+
end
1012

11-
# Randomly change one of its gene.
12-
# Help maintain diversity within the population and prevent premature convergence.
13-
# @param [(Object | Float)[][]] ranges - The available ranges to mutate from for all genes. ranges.lenght must equals genes.length
14-
# @param [Boolean] sampleFromRanges
15-
# If true: take one of the available values for a given range
16-
# if false: take a random floating value between ranges[i][0] and ranges[i][1]
17-
def mutate(ranges, sample_from_ranges = false)
18-
i = rand(0..(genes.length - 1))
19-
@genes[i] = sample_from_ranges ? ranges[i].sample : rand(ranges[i][0]..ranges[i][1])
13+
# Randomly change one of its gene.
14+
# Help maintain diversity within the population and prevent premature convergence.
15+
def mutate(genes_generator)
16+
i = rand(0..(genes.length - 1))
17+
@genes[i] = genes_generator.call(i)
18+
end
19+
end
2020
end
2121
end

lib/genetic/algorithm/fitness_recorder.rb

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
# Record and display best individuals accross generations
22
class Genetic::Algorithm::FitnessRecorder
3-
attr_accessor :best_individual
3+
attr_reader :best_individual
44

55
def initialize
66
@best_individual = nil
7+
@best_individual_generation = nil
78
@max_fitness = []
89
@max_overall_finess = []
910
@average_fitness = []
@@ -12,12 +13,13 @@ def initialize
1213
def record_generation(generation, population)
1314
best_generation_individual = population.max_by(&:fitness)
1415

15-
# Save best individual across all generations
16-
if best_generation_individual.fitness > (@best_individual&.fitness || -1e10)
16+
# Save the best individual across all generations
17+
if best_generation_individual.fitness > @best_individual&.fitness
1718
@best_individual = best_generation_individual
19+
@best_individual_generation = generation
1820
end
1921

20-
# Record progress information
22+
# Record progress information across all generations
2123
@max_fitness << best_generation_individual.fitness
2224
@max_overall_finess << @best_individual.fitness
2325
average = population.map(&:fitness).inject(:+) / population.length
@@ -29,18 +31,17 @@ def record_generation(generation, population)
2931
puts " Average population fitness: #{average}"
3032
end
3133

32-
def display_best_individual(shifts)
34+
def display_best_individual
3335
puts "\n"
34-
puts "Result: Best overall individual fitness: #{@best_individual.fitness}"
35-
puts " Shift\t->\tSelected employee:"
36-
print_shift = ->(shift) { "[#{shift.start_at.localtime} - #{shift.end_at.localtime}]: #{shift.position.name}" }
37-
print_employee = ->(employee) { "#{employee.profile.full_name} (#{employee.email})" }
38-
puts shifts.each_with_index.map { |s, i| "#{print_shift[s[:shift]]}\t->\t#{print_employee[@best_individual.genes[i].user]}" }
39-
40-
# puts "#{@max_fitness.each_with_index.map { |_, i| i }},"
41-
# puts "#{@max_fitness},"
42-
# puts "#{@max_overall_finess},"
43-
# puts "#{@average_fitness},"
36+
puts "Result: The best overall individual was found on generation #{@best_individual_generation} with:"
37+
puts " Fitness: #{@best_individual.fitness}"
38+
puts " Genes: #{@best_individual.fitness}"
39+
puts "\n---\n"
40+
puts "Generations evlotion:"
41+
puts "#{@max_fitness.each_with_index.map { |_, i| i }},"
42+
puts "#{@max_fitness},"
43+
puts "#{@max_overall_finess},"
44+
puts "#{@average_fitness},"
4445
end
4546

4647
def save_best_individual_genes

lib/genetic/algorithm/optimiser.rb

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
module Genetic
2+
module Algorithm
3+
class Optimiser
4+
attr_accessor :crossover_probability # Int
5+
attr_accessor :mutation_probability # Int
6+
attr_accessor :population_size # Int
7+
attr_accessor :number_of_generations # Int
8+
attr_accessor :genes_generator # (index: Int?) -> Array<Gene> | Gene if index.nil?
9+
attr_accessor :fitness_calculator # (genes: Array<Gene>) -> Int
10+
11+
def execute
12+
population = initialize_population
13+
14+
fitness_recorder = FitnessRecorder.new
15+
@number_of_generations.times.each do |generation|
16+
if generation != @number_of_generations
17+
population = do_natural_selection(population)
18+
end
19+
fitness_recorder.record_generation(generation, population)
20+
end
21+
22+
fitness_recorder.display_best_individual
23+
end
24+
25+
private
26+
27+
def initialize_population
28+
population = []
29+
@population_size.times.each do
30+
population << Chromosome.new(@genes_generator.call, @fitness_calculator)
31+
end
32+
population
33+
end
34+
35+
def do_natural_selection(population)
36+
total_fitness = population.map(&:fitness).inject(:+)
37+
new_population = []
38+
(@population_size / 2).times.each do
39+
parent1 = weighted_random_sampling(population, total_fitness)
40+
parent2 = weighted_random_sampling(population - [parent1], total_fitness)
41+
42+
child1, child2 = mate(parent1, parent2)
43+
44+
new_population << child1
45+
new_population << child2
46+
end
47+
48+
new_population
49+
end
50+
51+
def weighted_random_sampling(population, total_fitness)
52+
population.max_by { |chromosome| rand**(total_fitness / chromosome.fitness) }
53+
end
54+
55+
def mate(parent1, parent2)
56+
# crossover
57+
if rand < @crossover_probability
58+
i = rand(1..parent1.genes.length - 1)
59+
child1 = Chromosome.new(parent1.genes[0..i - 1] + parent2.genes[i..-1], @fitness_calculator)
60+
child2 = Chromosome.new(parent2.genes[0..i - 1] + parent1.genes[i..-1], @fitness_calculator)
61+
else
62+
child1 = Chromosome.new(parent1.genes.dup, @fitness_calculator)
63+
child2 = Chromosome.new(parent2.genes.dup, @fitness_calculator)
64+
end
65+
66+
# mutation
67+
if rand < @mutation_probability
68+
child1.mutate(@genes_generator)
69+
child2.mutate(@genes_generator)
70+
end
71+
72+
[child1, child2]
73+
end
74+
end
75+
end
76+
end

test/genetic/algorithm/chromosome_test.rb

Lines changed: 14 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,48 +2,31 @@
22
require "genetic/algorithm/chromosome"
33

44
class Genetic::Algorithm::ChromosomeTest < Minitest::Test
5-
def test_it_can_mutate_floats
6-
20.times do
7-
test_genes = Array.new(rand(1..20)) { rand(0..100) }
8-
test_ranges = Array.new(test_genes.length) do |i|
9-
el = test_genes[i].to_f
10-
[rand(el - 50.0..el), rand(el..el + 50.0)]
11-
end
12-
13-
chromosome = Genetic::Algorithm::Chromosome.new(fitness: nil, genes: test_genes.dup)
14-
chromosome.mutate(test_ranges)
15-
16-
number_of_mutated_genes = 0
17-
chromosome.genes.each_with_index do |el, i|
18-
if el != test_genes[i]
19-
number_of_mutated_genes += 1
20-
end
21-
22-
assert(test_ranges[i][0] < el && el < test_ranges[i][1])
23-
end
24-
assert_equal(1, number_of_mutated_genes)
25-
end
26-
end
27-
285
def test_it_can_mutate_with_data_range
296
test_ranges = [%w[male female], [18, 19, 20, 21, 22], ["🔥", "❄️", "🌎", "💨"]]
30-
test_genes = Array.new(test_ranges.length) { |i| test_ranges[i][0] }
7+
genes_generator = proc do |index = nil|
8+
index.nil? ? Array.new(test_ranges.length) { |i| test_ranges[i][0] } : test_ranges[index].sample
9+
end
10+
test_genes = genes_generator.call
3111

32-
chromosome = Genetic::Algorithm::Chromosome.new(fitness: nil, genes: test_genes.dup)
12+
chromosome = Genetic::Algorithm::Chromosome.new(test_genes.dup, method(:fitness_calculator))
3313
20.times do |i| # Discrete mutations can sometimes return the same value
3414
assert(false, "Unable to mutate") if i == 20
35-
chromosome.mutate(test_ranges, true)
15+
chromosome.mutate(genes_generator)
3616
break if chromosome.genes != test_genes
3717
end
3818

3919
number_of_mutated_genes = 0
4020
chromosome.genes.each_with_index do |el, i|
41-
if el != test_genes[i]
42-
number_of_mutated_genes += 1
43-
end
44-
45-
assert((test_ranges[i].include? el))
21+
number_of_mutated_genes += 1 if el != test_genes[i]
22+
assert(test_ranges[i].include?(el))
4623
end
4724
assert_equal(1, number_of_mutated_genes)
4825
end
26+
27+
private
28+
29+
def fitness_calculator(_genes)
30+
1
31+
end
4932
end

0 commit comments

Comments
 (0)