From 5a6271fe1f62bdbcad91520990bc16d36b8cefca Mon Sep 17 00:00:00 2001 From: Eric Matte <14026234@usherbrooke.ca> Date: Sun, 17 Nov 2019 11:29:55 -0500 Subject: [PATCH] Chromosome tests --- .rubocop.yml | 3 ++ .vscode/settings.json | 7 +++- genetic-algorithm-resolver.gemspec | 2 +- lib/genetic/algorithm.rb | 1 + lib/genetic/algorithm/chromosome.rb | 21 ++++++++++ lib/genetic/algorithm/fitness_recorder.rb | 49 +++++++++++++++++++++++ lib/genetic/algorithm/version.rb | 5 --- test/genetic/algorithm/chromosome_test.rb | 49 +++++++++++++++++++++++ test/genetic/algorithm/resolver_test.rb | 6 +-- test/test_helper.rb | 2 +- 10 files changed, 132 insertions(+), 13 deletions(-) create mode 100644 lib/genetic/algorithm/chromosome.rb create mode 100644 lib/genetic/algorithm/fitness_recorder.rb delete mode 100644 lib/genetic/algorithm/version.rb create mode 100644 test/genetic/algorithm/chromosome_test.rb diff --git a/.rubocop.yml b/.rubocop.yml index 4e51084..8d3f4a6 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -7,6 +7,9 @@ Style/StringLiterals: Metrics/LineLength: Max: 160 +Metrics/MethodLength: + Max: 35 + Gemspec/OrderedDependencies: Enabled: false diff --git a/.vscode/settings.json b/.vscode/settings.json index ae922ee..e0fcbe8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,7 +4,12 @@ "files.trimTrailingWhitespace": true, "files.insertFinalNewline": true, "ruby.lintDebounceTime": 500, + "ruby.useBundler": true, + "ruby.useLanguageServer": true, "ruby.lint": { - "rubocop": true + "rubocop": { + "useBundler": true + } }, + "ruby.format": "rubocop" } diff --git a/genetic-algorithm-resolver.gemspec b/genetic-algorithm-resolver.gemspec index e5b256a..5f281ec 100644 --- a/genetic-algorithm-resolver.gemspec +++ b/genetic-algorithm-resolver.gemspec @@ -1,7 +1,7 @@ lib = File.expand_path("../lib", __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) -require "genetic/algorithm/version" +require "genetic/algorithm" Gem::Specification.new do |spec| spec.name = "genetic-algorithm-resolver" diff --git a/lib/genetic/algorithm.rb b/lib/genetic/algorithm.rb index a6433f2..dfc2382 100644 --- a/lib/genetic/algorithm.rb +++ b/lib/genetic/algorithm.rb @@ -1,4 +1,5 @@ module Genetic module Algorithm + VERSION = "0.1.0".freeze end end diff --git a/lib/genetic/algorithm/chromosome.rb b/lib/genetic/algorithm/chromosome.rb new file mode 100644 index 0000000..aa10c62 --- /dev/null +++ b/lib/genetic/algorithm/chromosome.rb @@ -0,0 +1,21 @@ +# A member of a given population +class Genetic::Algorithm::Chromosome + attr_accessor :genes + attr_accessor :fitness + + def initialize(genes:, fitness:) + @genes = genes + @fitness = fitness + 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]) + end +end diff --git a/lib/genetic/algorithm/fitness_recorder.rb b/lib/genetic/algorithm/fitness_recorder.rb new file mode 100644 index 0000000..c554a0c --- /dev/null +++ b/lib/genetic/algorithm/fitness_recorder.rb @@ -0,0 +1,49 @@ +# Record and display best individuals accross generations +class Genetic::Algorithm::FitnessRecorder + attr_accessor :best_individual + + def initialize + @best_individual = nil + @max_fitness = [] + @max_overall_finess = [] + @average_fitness = [] + end + + 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) + @best_individual = best_generation_individual + end + + # Record progress information + @max_fitness << best_generation_individual.fitness + @max_overall_finess << @best_individual.fitness + average = population.map(&:fitness).inject(:+) / population.length + @average_fitness << average + + puts "Generation #{generation} completed." + puts " Best overall fitness: #{@best_individual.fitness}" + puts " Best population fitness: #{best_generation_individual.fitness}" + puts " Average population fitness: #{average}" + end + + def display_best_individual(shifts) + 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}," + end + + def save_best_individual_genes + raise "not implemented".freeze + end +end diff --git a/lib/genetic/algorithm/version.rb b/lib/genetic/algorithm/version.rb deleted file mode 100644 index dfc2382..0000000 --- a/lib/genetic/algorithm/version.rb +++ /dev/null @@ -1,5 +0,0 @@ -module Genetic - module Algorithm - VERSION = "0.1.0".freeze - end -end diff --git a/test/genetic/algorithm/chromosome_test.rb b/test/genetic/algorithm/chromosome_test.rb new file mode 100644 index 0000000..2fc9a88 --- /dev/null +++ b/test/genetic/algorithm/chromosome_test.rb @@ -0,0 +1,49 @@ +require "test_helper" +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] } + + chromosome = Genetic::Algorithm::Chromosome.new(fitness: nil, genes: test_genes.dup) + 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) + 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)) + end + assert_equal(1, number_of_mutated_genes) + end +end diff --git a/test/genetic/algorithm/resolver_test.rb b/test/genetic/algorithm/resolver_test.rb index b867b82..34237eb 100644 --- a/test/genetic/algorithm/resolver_test.rb +++ b/test/genetic/algorithm/resolver_test.rb @@ -2,10 +2,6 @@ class Genetic::Algorithm::ResolverTest < Minitest::Test def test_that_it_has_a_version_number - refute_nil ::Genetic::Algorithm::Resolver::VERSION - end - - def test_it_does_something_useful - assert false + refute_nil Genetic::Algorithm::VERSION end end diff --git a/test/test_helper.rb b/test/test_helper.rb index 2a557ce..9d976a0 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,4 +1,4 @@ $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__) -require "genetic/algorithm/resolver" +require "genetic/algorithm" require "minitest/autorun"