Skip to content

Commit

Permalink
Generic genetic optimizer + tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Eric Matte committed Nov 17, 2019
1 parent 5a6271f commit 34dcccc
Show file tree
Hide file tree
Showing 9 changed files with 253 additions and 64 deletions.
3 changes: 3 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ Metrics/LineLength:
Metrics/MethodLength:
Max: 35

Metrics/ClassLength:
Enabled: false

Gemspec/OrderedDependencies:
Enabled: false

Expand Down
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"editor.formatOnSave": false,
"editor.formatOnSave": true,
"editor.formatOnSaveTimeout": 5000,
"editor.tabSize": 2,
"files.trimTrailingWhitespace": true,
"files.insertFinalNewline": true,
Expand Down
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ source "https://rubygems.org"
# Specify your gem's dependencies in genetic-algorithm-resolver.gemspec
gemspec

gem "rubocop"
18 changes: 18 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,30 @@ PATH
GEM
remote: https://rubygems.org/
specs:
ast (2.4.0)
debase (0.2.4.1)
debase-ruby_core_source (>= 0.10.2)
debase-ruby_core_source (0.10.6)
jaro_winkler (1.5.2)
minitest (5.11.3)
parallel (1.13.0)
parser (2.6.2.0)
ast (~> 2.4.0)
psych (3.1.0)
rainbow (3.0.0)
rake (12.3.3)
rubocop (0.66.0)
jaro_winkler (~> 1.5.1)
parallel (~> 1.10)
parser (>= 2.5, != 2.5.1.1)
psych (>= 3.1.0)
rainbow (>= 2.2.2, < 4.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 1.6)
ruby-debug-ide (0.7.0)
rake (>= 0.8.1)
ruby-progressbar (1.10.0)
unicode-display_width (1.5.0)

PLATFORMS
ruby
Expand All @@ -23,6 +40,7 @@ DEPENDENCIES
genetic-algorithm-resolver!
minitest (~> 5.0)
rake (~> 12.3.2)
rubocop
ruby-debug-ide (~> 0.7)

BUNDLED WITH
Expand Down
34 changes: 17 additions & 17 deletions lib/genetic/algorithm/chromosome.rb
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
# A member of a given population
class Genetic::Algorithm::Chromosome
attr_accessor :genes
attr_accessor :fitness
module Genetic
module Algorithm
# A member of a given population
class Chromosome
attr_reader :genes
attr_reader :fitness

def initialize(genes:, fitness:)
@genes = genes
@fitness = fitness
end
def initialize(genes, fitness_calculator)
@genes = genes
@fitness = fitness_calculator.call(genes)
end

# Randomly change one of its gene.
# Help maintain diversity within the population and prevent premature convergence.
# @param [(Object | Float)[][]] ranges - The available ranges to mutate from for all genes. ranges.lenght must equals genes.length
# @param [Boolean] sampleFromRanges
# If true: take one of the available values for a given range
# if false: take a random floating value between ranges[i][0] and ranges[i][1]
def mutate(ranges, sample_from_ranges = false)
i = rand(0..(genes.length - 1))
@genes[i] = sample_from_ranges ? ranges[i].sample : rand(ranges[i][0]..ranges[i][1])
# Randomly change one of its gene.
# Help maintain diversity within the population and prevent premature convergence.
def mutate(genes_generator)
i = rand(0..(genes.length - 1))
@genes[i] = genes_generator.call(i)
end
end
end
end
31 changes: 16 additions & 15 deletions lib/genetic/algorithm/fitness_recorder.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# Record and display best individuals accross generations
class Genetic::Algorithm::FitnessRecorder
attr_accessor :best_individual
attr_reader :best_individual

def initialize
@best_individual = nil
@best_individual_generation = nil
@max_fitness = []
@max_overall_finess = []
@average_fitness = []
Expand All @@ -12,12 +13,13 @@ def initialize
def record_generation(generation, population)
best_generation_individual = population.max_by(&:fitness)

# Save best individual across all generations
if best_generation_individual.fitness > (@best_individual&.fitness || -1e10)
# Save the best individual across all generations
if best_generation_individual.fitness > @best_individual&.fitness
@best_individual = best_generation_individual
@best_individual_generation = generation
end

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

def display_best_individual(shifts)
def display_best_individual
puts "\n"
puts "Result: Best overall individual fitness: #{@best_individual.fitness}"
puts " Shift\t->\tSelected employee:"
print_shift = ->(shift) { "[#{shift.start_at.localtime} - #{shift.end_at.localtime}]: #{shift.position.name}" }
print_employee = ->(employee) { "#{employee.profile.full_name} (#{employee.email})" }
puts shifts.each_with_index.map { |s, i| "#{print_shift[s[:shift]]}\t->\t#{print_employee[@best_individual.genes[i].user]}" }

# puts "#{@max_fitness.each_with_index.map { |_, i| i }},"
# puts "#{@max_fitness},"
# puts "#{@max_overall_finess},"
# puts "#{@average_fitness},"
puts "Result: The best overall individual was found on generation #{@best_individual_generation} with:"
puts " Fitness: #{@best_individual.fitness}"
puts " Genes: #{@best_individual.fitness}"
puts "\n---\n"
puts "Generations evlotion:"
puts "#{@max_fitness.each_with_index.map { |_, i| i }},"
puts "#{@max_fitness},"
puts "#{@max_overall_finess},"
puts "#{@average_fitness},"
end

def save_best_individual_genes
Expand Down
76 changes: 76 additions & 0 deletions lib/genetic/algorithm/optimiser.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
module Genetic
module Algorithm
class Optimiser
attr_accessor :crossover_probability # Int
attr_accessor :mutation_probability # Int
attr_accessor :population_size # Int
attr_accessor :number_of_generations # Int
attr_accessor :genes_generator # (index: Int?) -> Array<Gene> | Gene if index.nil?
attr_accessor :fitness_calculator # (genes: Array<Gene>) -> Int

def execute
population = initialize_population

fitness_recorder = FitnessRecorder.new
@number_of_generations.times.each do |generation|
if generation != @number_of_generations
population = do_natural_selection(population)
end
fitness_recorder.record_generation(generation, population)
end

fitness_recorder.display_best_individual
end

private

def initialize_population
population = []
@population_size.times.each do
population << Chromosome.new(@genes_generator.call, @fitness_calculator)
end
population
end

def do_natural_selection(population)
total_fitness = population.map(&:fitness).inject(:+)
new_population = []
(@population_size / 2).times.each do
parent1 = weighted_random_sampling(population, total_fitness)
parent2 = weighted_random_sampling(population - [parent1], total_fitness)

child1, child2 = mate(parent1, parent2)

new_population << child1
new_population << child2
end

new_population
end

def weighted_random_sampling(population, total_fitness)
population.max_by { |chromosome| rand**(total_fitness / chromosome.fitness) }
end

def mate(parent1, parent2)
# crossover
if rand < @crossover_probability
i = rand(1..parent1.genes.length - 1)
child1 = Chromosome.new(parent1.genes[0..i - 1] + parent2.genes[i..-1], @fitness_calculator)
child2 = Chromosome.new(parent2.genes[0..i - 1] + parent1.genes[i..-1], @fitness_calculator)
else
child1 = Chromosome.new(parent1.genes.dup, @fitness_calculator)
child2 = Chromosome.new(parent2.genes.dup, @fitness_calculator)
end

# mutation
if rand < @mutation_probability
child1.mutate(@genes_generator)
child2.mutate(@genes_generator)
end

[child1, child2]
end
end
end
end
45 changes: 14 additions & 31 deletions test/genetic/algorithm/chromosome_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,31 @@
require "genetic/algorithm/chromosome"

class Genetic::Algorithm::ChromosomeTest < Minitest::Test
def test_it_can_mutate_floats
20.times do
test_genes = Array.new(rand(1..20)) { rand(0..100) }
test_ranges = Array.new(test_genes.length) do |i|
el = test_genes[i].to_f
[rand(el - 50.0..el), rand(el..el + 50.0)]
end

chromosome = Genetic::Algorithm::Chromosome.new(fitness: nil, genes: test_genes.dup)
chromosome.mutate(test_ranges)

number_of_mutated_genes = 0
chromosome.genes.each_with_index do |el, i|
if el != test_genes[i]
number_of_mutated_genes += 1
end

assert(test_ranges[i][0] < el && el < test_ranges[i][1])
end
assert_equal(1, number_of_mutated_genes)
end
end

def test_it_can_mutate_with_data_range
test_ranges = [%w[male female], [18, 19, 20, 21, 22], ["🔥", "❄️", "🌎", "💨"]]
test_genes = Array.new(test_ranges.length) { |i| test_ranges[i][0] }
genes_generator = proc do |index = nil|
index.nil? ? Array.new(test_ranges.length) { |i| test_ranges[i][0] } : test_ranges[index].sample
end
test_genes = genes_generator.call

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

number_of_mutated_genes = 0
chromosome.genes.each_with_index do |el, i|
if el != test_genes[i]
number_of_mutated_genes += 1
end

assert((test_ranges[i].include? el))
number_of_mutated_genes += 1 if el != test_genes[i]
assert(test_ranges[i].include?(el))
end
assert_equal(1, number_of_mutated_genes)
end

private

def fitness_calculator(_genes)
1
end
end
Loading

0 comments on commit 34dcccc

Please sign in to comment.