From 405e698a6f016f9a36c5ae4e75d71e629dec78c6 Mon Sep 17 00:00:00 2001 From: Manuel Bustillo Date: Wed, 24 Jul 2024 20:14:58 +0200 Subject: [PATCH] Initial version of VNS algorithm --- Gemfile | 4 ++- Gemfile.lock | 7 +++++ app/services/tables/distribution.rb | 44 +++++++++++++++++++++++++++++ app/services/tables/swap.rb | 28 ++++++++++++++++++ db/seeds.rb | 16 ++--------- lib/tasks/vns.rake | 16 +++++++++++ 6 files changed, 100 insertions(+), 15 deletions(-) create mode 100644 app/services/tables/distribution.rb create mode 100644 app/services/tables/swap.rb create mode 100644 lib/tasks/vns.rake diff --git a/Gemfile b/Gemfile index a3cbafd..7ff1257 100644 --- a/Gemfile +++ b/Gemfile @@ -64,4 +64,6 @@ group :development do end gem "money" -gem 'acts-as-taggable-on' \ No newline at end of file +gem 'acts-as-taggable-on' + +gem "vns", path: "../vns" \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock index 6f951a1..8d5808f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,9 @@ +PATH + remote: ../vns + specs: + vns (0.4.0.pre.rc.1) + activesupport + GEM remote: https://rubygems.org/ specs: @@ -276,6 +282,7 @@ DEPENDENCIES stimulus-rails turbo-rails tzinfo-data + vns! web-console RUBY VERSION diff --git a/app/services/tables/distribution.rb b/app/services/tables/distribution.rb new file mode 100644 index 0000000..b8a9997 --- /dev/null +++ b/app/services/tables/distribution.rb @@ -0,0 +1,44 @@ +module Tables + class Distribution + attr_reader :tables + + def initialize(min_per_table:, max_per_table:) + @min_per_table = min_per_table + @max_per_table = max_per_table + end + + def random_distribution(people) + @tables = [] + + @tables << people.slice!(0..rand(@min_per_table..@max_per_table)) while people.any? + end + + def discomfort + @tables.map do |table| + local_discomfort(table) + end.sum + end + + def inspect + "#{@tables.count} tables, discomfort: #{discomfort}" + end + + def pretty_print + @tables.map.with_index do |table, i| + "Table #{i + 1} (#{table.count} ppl): (#{local_discomfort(table)}) #{table.map(&:full_name).join(', ')}" + end.join("\n") + end + + private + + def local_discomfort(table) + 10 * (number_of_groups(table) - 1) + end + + def number_of_groups(table) + table.map do |person| + person.affinity_groups + end.flatten.uniq.count + end + end +end diff --git a/app/services/tables/swap.rb b/app/services/tables/swap.rb new file mode 100644 index 0000000..ddc3681 --- /dev/null +++ b/app/services/tables/swap.rb @@ -0,0 +1,28 @@ +module Tables + class Swap + private attr_reader :initial_solution + def initialize(initial_solution) + @initial_solution = initial_solution + end + + def each + @initial_solution.tables.combination(2) do |table_a, table_b| + table_a.product(table_b).each do |(person_a, person_b)| + table_a.delete(person_a) + table_b.delete(person_b) + + table_a << person_b + table_b << person_a + + yield(@initial_solution) + ensure + table_a.delete(person_b) + table_b.delete(person_a) + + table_a << person_a + table_b << person_b + end + end + end + end +end diff --git a/db/seeds.rb b/db/seeds.rb index e79ad9b..c5f684c 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,12 +1,4 @@ -# This file should ensure the existence of records required to run the application in every environment (production, -# development, test). The code here should be idempotent so that it can be executed at any point in every environment. -# The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). -# -# Example: -# -# ["Action", "Comedy", "Drama", "Horror"].each do |genre_name| -# MovieGenre.find_or_create_by!(name: genre_name) -# end +NUMBER_OF_GUESTS = 50 Expense.delete_all Guest.delete_all @@ -28,10 +20,6 @@ Expense.create!(name: 'Transportation', amount: 3000, pricing_type: 'fixed') Expense.create!(name: 'Invitations', amount: 200, pricing_type: 'fixed') Expense.create!(name: 'Cake', amount: 500, pricing_type: 'fixed') -<<<<<<< HEAD - -======= ->>>>>>> 8fd0b7c (Modify seeds file to make sure every guest is part of a group) samples = { close_family: 10, family_1_group_a: 5, @@ -50,7 +38,7 @@ samples = { count.times { acc << affinity_group } end -300.times do +NUMBER_OF_GUESTS.times do guest = Guest.create!(first_name: Faker::Name.first_name, last_name: Faker::Name.last_name, email: Faker::Internet.email, diff --git a/lib/tasks/vns.rake b/lib/tasks/vns.rake new file mode 100644 index 0000000..dd5a942 --- /dev/null +++ b/lib/tasks/vns.rake @@ -0,0 +1,16 @@ +namespace :vns do + task distribute_tables: :environment do + engine = VNS::Engine.new + + engine.add_perturbation(Tables::Swap) + + initial_solution = Tables::Distribution.new(min_per_table: 8, max_per_table: 10) + initial_solution.random_distribution(Guest.all.shuffle) + + engine.initial_solution = initial_solution + + engine.target_function(&:discomfort) + + best_solution = engine.run + end +end