From 75a0191d40efe34cffdfe3b9524f47d8b8e544fd Mon Sep 17 00:00:00 2001 From: Manuel Bustillo Date: Fri, 1 Aug 2025 12:25:43 +0200 Subject: [PATCH 1/8] Add a new status column to tables arrangements table --- app/models/tables_arrangement.rb | 1 + ...0250801102437_add_status_column_to_tables_arrangements.rb | 5 +++++ db/schema.rb | 3 ++- 3 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20250801102437_add_status_column_to_tables_arrangements.rb diff --git a/app/models/tables_arrangement.rb b/app/models/tables_arrangement.rb index b5fccb0..a0d4bb2 100644 --- a/app/models/tables_arrangement.rb +++ b/app/models/tables_arrangement.rb @@ -10,6 +10,7 @@ # digest :uuid not null # discomfort :integer # name :string not null +# status :string default("complete"), not null # created_at :datetime not null # updated_at :datetime not null # wedding_id :uuid not null diff --git a/db/migrate/20250801102437_add_status_column_to_tables_arrangements.rb b/db/migrate/20250801102437_add_status_column_to_tables_arrangements.rb new file mode 100644 index 0000000..abe56da --- /dev/null +++ b/db/migrate/20250801102437_add_status_column_to_tables_arrangements.rb @@ -0,0 +1,5 @@ +class AddStatusColumnToTablesArrangements < ActiveRecord::Migration[8.0] + def change + add_column :tables_arrangements, :status, :string, default: :complete, null: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 2576583..b0279ad 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_06_08_181054) do +ActiveRecord::Schema[8.0].define(version: 2025_08_01_102437) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" @@ -216,6 +216,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_06_08_181054) do t.string "name", null: false t.uuid "wedding_id", null: false t.uuid "digest", default: -> { "gen_random_uuid()" }, null: false + t.string "status", default: "complete", null: false t.index ["wedding_id"], name: "index_tables_arrangements_on_wedding_id" end From dd14a96e98631c2bbb3483c039ae3289cdbd5695 Mon Sep 17 00:00:00 2001 From: Manuel Bustillo Date: Fri, 1 Aug 2025 12:29:13 +0200 Subject: [PATCH 2/8] Expose and document the new status attribute in the tables arrangements controller --- app/controllers/tables_arrangements_controller.rb | 4 ++-- spec/requests/tables_arrangements_spec.rb | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/controllers/tables_arrangements_controller.rb b/app/controllers/tables_arrangements_controller.rb index 45cf633..9d2243a 100644 --- a/app/controllers/tables_arrangements_controller.rb +++ b/app/controllers/tables_arrangements_controller.rb @@ -9,10 +9,10 @@ class TablesArrangementsController < ApplicationController render json: TablesArrangement .order(valid: :desc) .order(discomfort: :asc) - .select(:id, :name, :discomfort) + .select(:id, :name, :discomfort, :status) .select("digest = '#{current_digest}'::uuid as valid") .limit(20) - .as_json(only: %i[id name discomfort valid]) + .as_json(only: %i[id name discomfort valid status]) end def show diff --git a/spec/requests/tables_arrangements_spec.rb b/spec/requests/tables_arrangements_spec.rb index 3cc11c1..b8fedfe 100644 --- a/spec/requests/tables_arrangements_spec.rb +++ b/spec/requests/tables_arrangements_spec.rb @@ -19,7 +19,8 @@ RSpec.describe 'tables_arrangements' do id: { type: :string, format: :uuid }, name: { type: :string }, discomfort: { type: :integer }, - valid: { type: :boolean } + valid: { type: :boolean }, + status: { type: :string, enum: %w[complete in_progress] } } } xit From ac659bef860be1756e1520eea09f0d6464a0bdc8 Mon Sep 17 00:00:00 2001 From: Manuel Bustillo Date: Mon, 8 Sep 2025 15:51:43 +0200 Subject: [PATCH 3/8] Update Tables::Distribution#save! to consider that the distribution may already be persisted --- app/models/seat.rb | 2 +- app/services/tables/distribution.rb | 11 +++++--- spec/services/tables/distribution_spec.rb | 33 +++++++++++++++++++++++ 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/app/models/seat.rb b/app/models/seat.rb index f4f7b97..4104d81 100644 --- a/app/models/seat.rb +++ b/app/models/seat.rb @@ -29,5 +29,5 @@ class Seat < ApplicationRecord acts_as_tenant :wedding belongs_to :guest - belongs_to :table_arrangement + belongs_to :tables_arrangement end diff --git a/app/services/tables/distribution.rb b/app/services/tables/distribution.rb index 4751cf8..bad8bf1 100644 --- a/app/services/tables/distribution.rb +++ b/app/services/tables/distribution.rb @@ -12,13 +12,14 @@ module Tables end end - attr_accessor :tables, :min_per_table, :max_per_table, :hierarchy + attr_accessor :tables, :min_per_table, :max_per_table, :hierarchy, :tables_arrangement_id - def initialize(min_per_table:, max_per_table:, hierarchy: AffinityGroupsHierarchy.new) + def initialize(min_per_table:, max_per_table:, hierarchy: AffinityGroupsHierarchy.new, tables_arrangement_id: nil) @min_per_table = min_per_table @max_per_table = max_per_table @hierarchy = hierarchy @tables = [] + @tables_arrangement_id = tables_arrangement_id end def random_distribution(people, random: Random.new) @@ -50,7 +51,11 @@ module Tables def save! ActiveRecord::Base.transaction do - arrangement = TablesArrangement.create! + arrangement = TablesArrangement.find_or_create_by!(id: tables_arrangement_id) + + self.tables_arrangement_id = arrangement.id + + arrangement.seats.delete_all records_to_store = [] diff --git a/spec/services/tables/distribution_spec.rb b/spec/services/tables/distribution_spec.rb index e598a52..912e4b3 100644 --- a/spec/services/tables/distribution_spec.rb +++ b/spec/services/tables/distribution_spec.rb @@ -6,6 +6,39 @@ require 'rails_helper' module Tables RSpec.describe Distribution do + describe '#save!' do + + around do |example| + ActsAsTenant.with_tenant(create(:wedding)) do + example.run + end + end + + let(:people) { create_list(:guest, 2, status: :invited) } + let(:distribution) do + described_class.new(min_per_table: 5, max_per_table: 10).tap{|d| d.random_distribution(people)} + end + + context 'when tables_arrangement_id is nil' do + + + it { expect { distribution.save! }.to change { TablesArrangement.count }.by(1) } + it { expect { distribution.save! }.to change { Seat.count }.by(2) } + end + + context 'when tables_arrangement_id is set' do + before do + existing_arrangement = TablesArrangement.create! + + existing_arrangement.seats.create!(guest: people.first, table_number: 1) + distribution.tables_arrangement_id = existing_arrangement.id + end + + it { expect { distribution.save! }.not_to change { TablesArrangement.count } } + it { expect { distribution.save! }.to change { Seat.count }.by(1) } + end + end + describe '#random_distribution' do subject(:distribution) { described_class.new(min_per_table: 5, max_per_table: 10) } From 0d1b64256da7481c638789a261eb97a1584508e9 Mon Sep 17 00:00:00 2001 From: Manuel Bustillo Date: Mon, 8 Sep 2025 16:32:13 +0200 Subject: [PATCH 4/8] Provide notification callbacks for progress and new solutions --- app/services/vns/engine.rb | 17 ++++++++++++++++- lib/tasks/vns.rake | 8 ++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/app/services/vns/engine.rb b/app/services/vns/engine.rb index dc0feff..a5f680c 100644 --- a/app/services/vns/engine.rb +++ b/app/services/vns/engine.rb @@ -5,6 +5,7 @@ module VNS class Engine PERTURBATION_SIZES = [1, 1, 1, 2, 2, 3].freeze + ITERATIONS = 50 class << self def sequence(elements) elements = elements.to_a @@ -26,6 +27,14 @@ module VNS @perturbations << klass end + def notify_progress(&block) + @progress_notifier = block + end + + def on_better_solution(&block) + @better_solution_notifier = block + end + attr_writer :initial_solution def run @@ -40,18 +49,24 @@ module VNS run_all_optimizations + @progress_notifier&.call(Rational(1, ITERATIONS + 1)) + best_solution = @current_solution - 50.times do + ITERATIONS.times do |iteration| @current_solution = Tables::WheelSwap.new(best_solution).call(PERTURBATION_SIZES.sample) @best_score = @target_function.call(@current_solution) Rails.logger.debug { "After perturbation: #{@best_score}" } run_all_optimizations + @progress_notifier&.call(Rational(iteration + 1, ITERATIONS + 1)) + next unless best_solution.discomfort > @current_solution.discomfort best_solution = @current_solution + @better_solution_notifier&.call(best_solution) + Rails.logger.debug do "Found better solution after perturbation optimization: #{@current_solution.discomfort}" end diff --git a/lib/tasks/vns.rake b/lib/tasks/vns.rake index 9554a76..b7c38ac 100644 --- a/lib/tasks/vns.rake +++ b/lib/tasks/vns.rake @@ -21,6 +21,14 @@ namespace :vns do engine.target_function(&:discomfort) + engine.notify_progress do |current_progress| + Rails.logger.info "Progress: #{(current_progress * 100.0).round(2)}%" + end + + engine.on_better_solution do |better_solution| + Rails.logger.info "New best solution found with discomfort: #{better_solution.discomfort}" + end + solution = Rails.benchmark('VNS Benchmarking') { engine.run } Rails.logger.info "Best solution found with discomfort: #{solution.discomfort}" From 12174b6f20b53933279ebcf0d941ddc9aa627001 Mon Sep 17 00:00:00 2001 From: Manuel Bustillo Date: Mon, 8 Sep 2025 22:44:54 +0200 Subject: [PATCH 5/8] Persist VNS calculation progress whenever an improvement has been made --- .../tables_arrangements_controller.rb | 11 +++++--- app/jobs/table_simulator_job.rb | 25 +++++++++++++++++-- app/models/tables_arrangement.rb | 1 + app/services/tables/distribution.rb | 12 ++++++--- app/services/vns/engine.rb | 2 +- bin/jobs | 1 + ...119_add_progress_to_tables_arrangements.rb | 5 ++++ db/schema.rb | 3 ++- db/seeds.rb | 4 ++- nginx.conf | 5 ++++ 10 files changed, 56 insertions(+), 13 deletions(-) create mode 100644 db/migrate/20250908145119_add_progress_to_tables_arrangements.rb diff --git a/app/controllers/tables_arrangements_controller.rb b/app/controllers/tables_arrangements_controller.rb index 9d2243a..184b577 100644 --- a/app/controllers/tables_arrangements_controller.rb +++ b/app/controllers/tables_arrangements_controller.rb @@ -9,10 +9,10 @@ class TablesArrangementsController < ApplicationController render json: TablesArrangement .order(valid: :desc) .order(discomfort: :asc) - .select(:id, :name, :discomfort, :status) - .select("digest = '#{current_digest}'::uuid as valid") + .select(:id, :name, :discomfort, :status, :progress) + .select("digest = '#{current_digest}'::uuid OR discomfort IS NULL as valid") .limit(20) - .as_json(only: %i[id name discomfort valid status]) + .as_json(only: %i[id name discomfort valid status progress]) end def show @@ -25,7 +25,10 @@ class TablesArrangementsController < ApplicationController end def create - TableSimulatorJob.perform_later(current_tenant.id) + ActiveRecord::Base.transaction do + tables_arrangement = TablesArrangement.create!(status: :not_started) + TableSimulatorJob.perform_later(current_tenant.id, tables_arrangement.id) + end render json: {}, status: :created end diff --git a/app/jobs/table_simulator_job.rb b/app/jobs/table_simulator_job.rb index 0471795..44d4759 100644 --- a/app/jobs/table_simulator_job.rb +++ b/app/jobs/table_simulator_job.rb @@ -8,16 +8,35 @@ class TableSimulatorJob < ApplicationJob MIN_PER_TABLE = 8 MAX_PER_TABLE = 10 - def perform(wedding_id) + def perform(wedding_id, tables_arrangement_id) + Rails.logger.info "Starting table simulation #{tables_arrangement_id} for wedding #{wedding_id}" ActsAsTenant.with_tenant(Wedding.find(wedding_id)) do engine = VNS::Engine.new engine.add_optimization(Tables::Swap) engine.add_optimization(Tables::Shift) - initial_solution = Tables::Distribution.new(min_per_table: MIN_PER_TABLE, max_per_table: MAX_PER_TABLE) + tables_arrangement = TablesArrangement.find(tables_arrangement_id) + + initial_solution = Tables::Distribution.new( + min_per_table: MIN_PER_TABLE, + max_per_table: MAX_PER_TABLE, + tables_arrangement_id: + ) + initial_solution.random_distribution(Guest.potential.shuffle) + initial_solution.save! + + engine.notify_progress do |current_progress| + tables_arrangement.update_columns(status: :in_progress, progress: current_progress) + end + + engine.on_better_solution do |better_solution| + better_solution.save! + tables_arrangement.update_columns(discomfort: better_solution.discomfort) # TODO: remove? + end + engine.initial_solution = initial_solution engine.target_function(&:discomfort) @@ -25,6 +44,8 @@ class TableSimulatorJob < ApplicationJob best_solution = engine.run best_solution.save! + + tables_arrangement.update_columns(status: :completed) end end end diff --git a/app/models/tables_arrangement.rb b/app/models/tables_arrangement.rb index a0d4bb2..99f8c7c 100644 --- a/app/models/tables_arrangement.rb +++ b/app/models/tables_arrangement.rb @@ -10,6 +10,7 @@ # digest :uuid not null # discomfort :integer # name :string not null +# progress :float default(0.0), not null # status :string default("complete"), not null # created_at :datetime not null # updated_at :datetime not null diff --git a/app/services/tables/distribution.rb b/app/services/tables/distribution.rb index bad8bf1..922d184 100644 --- a/app/services/tables/distribution.rb +++ b/app/services/tables/distribution.rb @@ -14,7 +14,7 @@ module Tables attr_accessor :tables, :min_per_table, :max_per_table, :hierarchy, :tables_arrangement_id - def initialize(min_per_table:, max_per_table:, hierarchy: AffinityGroupsHierarchy.new, tables_arrangement_id: nil) + def initialize(min_per_table:, max_per_table:, tables_arrangement_id:, hierarchy: AffinityGroupsHierarchy.new) @min_per_table = min_per_table @max_per_table = max_per_table @hierarchy = hierarchy @@ -43,15 +43,19 @@ module Tables end def deep_dup - self.class.new(min_per_table: @min_per_table, max_per_table: @max_per_table, - hierarchy: @hierarchy).tap do |new_distribution| + self.class.new( + min_per_table: @min_per_table, + max_per_table: @max_per_table, + hierarchy: @hierarchy, + tables_arrangement_id: @tables_arrangement_id + ).tap do |new_distribution| new_distribution.tables = @tables.map(&:dup) end end def save! ActiveRecord::Base.transaction do - arrangement = TablesArrangement.find_or_create_by!(id: tables_arrangement_id) + arrangement = TablesArrangement.find(tables_arrangement_id) self.tables_arrangement_id = arrangement.id diff --git a/app/services/vns/engine.rb b/app/services/vns/engine.rb index a5f680c..5408bc9 100644 --- a/app/services/vns/engine.rb +++ b/app/services/vns/engine.rb @@ -53,7 +53,7 @@ module VNS best_solution = @current_solution - ITERATIONS.times do |iteration| + (1..ITERATIONS).each do |iteration| @current_solution = Tables::WheelSwap.new(best_solution).call(PERTURBATION_SIZES.sample) @best_score = @target_function.call(@current_solution) Rails.logger.debug { "After perturbation: #{@best_score}" } diff --git a/bin/jobs b/bin/jobs index dcf59f3..fcc5d90 100755 --- a/bin/jobs +++ b/bin/jobs @@ -3,4 +3,5 @@ require_relative "../config/environment" require "solid_queue/cli" +SolidQueue.logger = ActiveSupport::Logger.new($stdout) SolidQueue::Cli.start(ARGV) diff --git a/db/migrate/20250908145119_add_progress_to_tables_arrangements.rb b/db/migrate/20250908145119_add_progress_to_tables_arrangements.rb new file mode 100644 index 0000000..2783aa2 --- /dev/null +++ b/db/migrate/20250908145119_add_progress_to_tables_arrangements.rb @@ -0,0 +1,5 @@ +class AddProgressToTablesArrangements < ActiveRecord::Migration[8.0] + def change + add_column :tables_arrangements, :progress, :float, default: 0, null: false + end +end diff --git a/db/schema.rb b/db/schema.rb index b0279ad..fb57903 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_08_01_102437) do +ActiveRecord::Schema[8.0].define(version: 2025_09_08_145119) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" @@ -217,6 +217,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_08_01_102437) do t.uuid "wedding_id", null: false t.uuid "digest", default: -> { "gen_random_uuid()" }, null: false t.string "status", default: "complete", null: false + t.float "progress", default: 0.0, null: false t.index ["wedding_id"], name: "index_tables_arrangements_on_wedding_id" end diff --git a/db/seeds.rb b/db/seeds.rb index f69b6e9..c1214ea 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -86,7 +86,9 @@ ActsAsTenant.with_tenant(wedding) do # TODO: Clean up invitations with no guests - ActiveJob.perform_all_later(3.times.map { TableSimulatorJob.new(wedding.id) }) + 3.times { TablesArrangement.create! } + .map { |arrangement| TableSimulatorJob.new(wedding.id, arrangement.id) } + .then { |jobs| ActiveJob.perform_all_later } "red".dup.paint.palette.triad(as: :hex).zip(Group.roots).each { |(color, group)| group.update!(color: color.paint.desaturate(40)) } diff --git a/nginx.conf b/nginx.conf index 58c2bb9..07f73fd 100644 --- a/nginx.conf +++ b/nginx.conf @@ -12,6 +12,11 @@ server { proxy_set_header Host $http_host; } + location /jobs/ { + proxy_pass http://backend:3000/jobs/; + proxy_set_header Host $http_host; + } + location /captcha/v2/media/ { proxy_pass http://libre-captcha:8888/v2/media/; proxy_set_header Host $http_host; From 78ab27a697ba3368868a413f9c797f6e0eae06ce Mon Sep 17 00:00:00 2001 From: Manuel Bustillo Date: Mon, 15 Sep 2025 22:52:41 +0200 Subject: [PATCH 6/8] Fix specs --- spec/services/tables/distribution_spec.rb | 32 ++++++++++++----------- spec/services/tables/shift_spec.rb | 4 +-- spec/services/tables/swap_spec.rb | 6 ++--- spec/services/tables/wheel_swap_spec.rb | 2 +- 4 files changed, 23 insertions(+), 21 deletions(-) diff --git a/spec/services/tables/distribution_spec.rb b/spec/services/tables/distribution_spec.rb index 912e4b3..7e1e7c6 100644 --- a/spec/services/tables/distribution_spec.rb +++ b/spec/services/tables/distribution_spec.rb @@ -6,24 +6,24 @@ require 'rails_helper' module Tables RSpec.describe Distribution do - describe '#save!' do - - around do |example| - ActsAsTenant.with_tenant(create(:wedding)) do - example.run - end - end + let(:tables_arrangement) { TablesArrangement.create! } + around do |example| + ActsAsTenant.with_tenant(create(:wedding)) do + example.run + end + end + + describe '#save!' do let(:people) { create_list(:guest, 2, status: :invited) } let(:distribution) do - described_class.new(min_per_table: 5, max_per_table: 10).tap{|d| d.random_distribution(people)} + described_class.new(min_per_table: 5, max_per_table: 10, tables_arrangement_id: tables_arrangement.id) + .tap { |d| d.random_distribution(people) } end context 'when tables_arrangement_id is nil' do - - - it { expect { distribution.save! }.to change { TablesArrangement.count }.by(1) } - it { expect { distribution.save! }.to change { Seat.count }.by(2) } + it { expect { distribution.save! }.to change(TablesArrangement, :count).by(1) } + it { expect { distribution.save! }.to change(Seat, :count).by(2) } end context 'when tables_arrangement_id is set' do @@ -34,13 +34,15 @@ module Tables distribution.tables_arrangement_id = existing_arrangement.id end - it { expect { distribution.save! }.not_to change { TablesArrangement.count } } - it { expect { distribution.save! }.to change { Seat.count }.by(1) } + it { expect { distribution.save! }.not_to(change(TablesArrangement, :count)) } + it { expect { distribution.save! }.to change(Seat, :count).by(1) } end end describe '#random_distribution' do - subject(:distribution) { described_class.new(min_per_table: 5, max_per_table: 10) } + subject(:distribution) do + described_class.new(min_per_table: 5, max_per_table: 10, tables_arrangement_id: tables_arrangement.id) + end context 'when there are fewer people than the minimum per table' do it 'creates one table' do diff --git a/spec/services/tables/shift_spec.rb b/spec/services/tables/shift_spec.rb index 39e07f8..ab7be98 100644 --- a/spec/services/tables/shift_spec.rb +++ b/spec/services/tables/shift_spec.rb @@ -17,7 +17,7 @@ module Tables context 'when there are two tables with two people each' do let(:initial_solution) do - Distribution.new(min_per_table: 2, max_per_table: 2).tap do |distribution| + Distribution.new(min_per_table: 2, max_per_table: 2, tables_arrangement_id: nil).tap do |distribution| distribution.tables << Set[:a, :b].to_table distribution.tables << Set[:c, :d].to_table end @@ -35,7 +35,7 @@ module Tables context 'when there are two tables with three people each' do let(:initial_solution) do - Distribution.new(min_per_table: 3, max_per_table: 3).tap do |distribution| + Distribution.new(min_per_table: 3, max_per_table: 3, tables_arrangement_id: nil).tap do |distribution| distribution.tables << Set[:a, :b, :c].to_table distribution.tables << Set[:d, :e, :f].to_table end diff --git a/spec/services/tables/swap_spec.rb b/spec/services/tables/swap_spec.rb index d8db4a7..00e978f 100644 --- a/spec/services/tables/swap_spec.rb +++ b/spec/services/tables/swap_spec.rb @@ -17,7 +17,7 @@ module Tables context 'when there are two tables with two people each' do let(:initial_solution) do - Distribution.new(min_per_table: 2, max_per_table: 2).tap do |distribution| + Distribution.new(min_per_table: 2, max_per_table: 2, tables_arrangement_id: nil).tap do |distribution| distribution.tables << Set[:a, :b].to_table distribution.tables << Set[:c, :d].to_table end @@ -35,7 +35,7 @@ module Tables context 'when there are two tables with three people each' do let(:initial_solution) do - Distribution.new(min_per_table: 3, max_per_table: 3).tap do |distribution| + Distribution.new(min_per_table: 3, max_per_table: 3, tables_arrangement_id: nil).tap do |distribution| distribution.tables << Set[:a, :b, :c].to_table distribution.tables << Set[:d, :e, :f].to_table end @@ -58,7 +58,7 @@ module Tables context 'when there are three tables with two people each' do let(:initial_solution) do - Distribution.new(min_per_table: 2, max_per_table: 2).tap do |distribution| + Distribution.new(min_per_table: 2, max_per_table: 2, tables_arrangement_id: nil).tap do |distribution| distribution.tables << Set[:a, :b].to_table distribution.tables << Set[:c, :d].to_table distribution.tables << Set[:e, :f].to_table diff --git a/spec/services/tables/wheel_swap_spec.rb b/spec/services/tables/wheel_swap_spec.rb index 02dc161..7ea67db 100644 --- a/spec/services/tables/wheel_swap_spec.rb +++ b/spec/services/tables/wheel_swap_spec.rb @@ -8,7 +8,7 @@ module Tables RSpec.describe WheelSwap do context 'when the solution has three tables' do let(:initial_solution) do - Distribution.new(min_per_table: 3, max_per_table: 3).tap do |distribution| + Distribution.new(min_per_table: 3, max_per_table: 3, tables_arrangement_id: nil).tap do |distribution| distribution.tables << Set[:a, :b, :c].to_table distribution.tables << Set[:d, :e, :f].to_table distribution.tables << Set[:g, :h, :i].to_table From 7d8ecfd0e388a3b70b6f5a78635236c8d997dc43 Mon Sep 17 00:00:00 2001 From: Manuel Bustillo Date: Mon, 15 Sep 2025 23:04:02 +0200 Subject: [PATCH 7/8] Refactor class to reduce complexity of #run method --- app/services/vns/engine.rb | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/app/services/vns/engine.rb b/app/services/vns/engine.rb index 5408bc9..6a68eaf 100644 --- a/app/services/vns/engine.rb +++ b/app/services/vns/engine.rb @@ -13,6 +13,10 @@ module VNS end end + def initialize + @perturbations = Set.new + end + def target_function(&function) @target_function = function end @@ -23,7 +27,6 @@ module VNS end def add_perturbation(klass) - @perturbations ||= Set.new @perturbations << klass end @@ -38,11 +41,7 @@ module VNS attr_writer :initial_solution def run - raise 'No target function defined' unless @target_function - raise 'No optimizations defined' unless @optimizations - raise 'No initial solution defined' unless @initial_solution - - @perturbations ||= Set.new + check_preconditions! @current_solution = @initial_solution @best_score = @target_function.call(@current_solution) @@ -77,6 +76,12 @@ module VNS private + def check_preconditions! + raise 'No target function defined' unless @target_function + raise 'No optimizations defined' unless @optimizations + raise 'No initial solution defined' unless @initial_solution + end + def run_all_optimizations self.class.sequence(@optimizations).each do |optimization| optimize(optimization) From 0502bc4552b41b3bfb9faf7ae23e2a46ed427e77 Mon Sep 17 00:00:00 2001 From: Manuel Bustillo Date: Mon, 15 Sep 2025 23:17:00 +0200 Subject: [PATCH 8/8] Disable a rubocop alert --- app/jobs/table_simulator_job.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/jobs/table_simulator_job.rb b/app/jobs/table_simulator_job.rb index 44d4759..5b6599e 100644 --- a/app/jobs/table_simulator_job.rb +++ b/app/jobs/table_simulator_job.rb @@ -8,7 +8,7 @@ class TableSimulatorJob < ApplicationJob MIN_PER_TABLE = 8 MAX_PER_TABLE = 10 - def perform(wedding_id, tables_arrangement_id) + def perform(wedding_id, tables_arrangement_id) # rubocop:disable Metrics/MethodLength Rails.logger.info "Starting table simulation #{tables_arrangement_id} for wedding #{wedding_id}" ActsAsTenant.with_tenant(Wedding.find(wedding_id)) do engine = VNS::Engine.new