Compare commits
	
		
			10 Commits
		
	
	
		
			152e16ff5d
			...
			838e52a0a0
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					838e52a0a0 | ||
| e28751521d | |||
| 0502bc4552 | |||
| 7d8ecfd0e3 | |||
| 78ab27a697 | |||
| 12174b6f20 | |||
| 0d1b64256d | |||
| ac659bef86 | |||
| dd14a96e98 | |||
| 75a0191d40 | 
							
								
								
									
										16
									
								
								Gemfile.lock
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								Gemfile.lock
									
									
									
									
									
								
							@ -241,7 +241,7 @@ GEM
 | 
				
			|||||||
    pp (0.6.2)
 | 
					    pp (0.6.2)
 | 
				
			||||||
      prettyprint
 | 
					      prettyprint
 | 
				
			||||||
    prettyprint (0.2.0)
 | 
					    prettyprint (0.2.0)
 | 
				
			||||||
    prism (1.4.0)
 | 
					    prism (1.5.1)
 | 
				
			||||||
    pry (0.15.2)
 | 
					    pry (0.15.2)
 | 
				
			||||||
      coderay (~> 1.1)
 | 
					      coderay (~> 1.1)
 | 
				
			||||||
      method_source (~> 1.0)
 | 
					      method_source (~> 1.0)
 | 
				
			||||||
@ -308,7 +308,7 @@ GEM
 | 
				
			|||||||
      redis-client (>= 0.22.0)
 | 
					      redis-client (>= 0.22.0)
 | 
				
			||||||
    redis-client (0.23.2)
 | 
					    redis-client (0.23.2)
 | 
				
			||||||
      connection_pool
 | 
					      connection_pool
 | 
				
			||||||
    regexp_parser (2.11.0)
 | 
					    regexp_parser (2.11.3)
 | 
				
			||||||
    reline (0.6.2)
 | 
					    reline (0.6.2)
 | 
				
			||||||
      io-console (~> 0.5)
 | 
					      io-console (~> 0.5)
 | 
				
			||||||
    responders (3.1.1)
 | 
					    responders (3.1.1)
 | 
				
			||||||
@ -351,7 +351,7 @@ GEM
 | 
				
			|||||||
    rswag-ui (2.16.0)
 | 
					    rswag-ui (2.16.0)
 | 
				
			||||||
      actionpack (>= 5.2, < 8.1)
 | 
					      actionpack (>= 5.2, < 8.1)
 | 
				
			||||||
      railties (>= 5.2, < 8.1)
 | 
					      railties (>= 5.2, < 8.1)
 | 
				
			||||||
    rubocop (1.79.2)
 | 
					    rubocop (1.80.2)
 | 
				
			||||||
      json (~> 2.3)
 | 
					      json (~> 2.3)
 | 
				
			||||||
      language_server-protocol (~> 3.17.0.2)
 | 
					      language_server-protocol (~> 3.17.0.2)
 | 
				
			||||||
      lint_roller (~> 1.1.0)
 | 
					      lint_roller (~> 1.1.0)
 | 
				
			||||||
@ -368,7 +368,7 @@ GEM
 | 
				
			|||||||
    rubocop-factory_bot (2.27.1)
 | 
					    rubocop-factory_bot (2.27.1)
 | 
				
			||||||
      lint_roller (~> 1.1)
 | 
					      lint_roller (~> 1.1)
 | 
				
			||||||
      rubocop (~> 1.72, >= 1.72.1)
 | 
					      rubocop (~> 1.72, >= 1.72.1)
 | 
				
			||||||
    rubocop-rails (2.32.0)
 | 
					    rubocop-rails (2.33.3)
 | 
				
			||||||
      activesupport (>= 4.2.0)
 | 
					      activesupport (>= 4.2.0)
 | 
				
			||||||
      lint_roller (~> 1.1)
 | 
					      lint_roller (~> 1.1)
 | 
				
			||||||
      rack (>= 1.1)
 | 
					      rack (>= 1.1)
 | 
				
			||||||
@ -588,7 +588,7 @@ CHECKSUMS
 | 
				
			|||||||
  pluck_to_hash (1.0.2) sha256=1599906239716f98262a41493dd7d4cb72e8d83ad3d76d666deacfc5de50a47e
 | 
					  pluck_to_hash (1.0.2) sha256=1599906239716f98262a41493dd7d4cb72e8d83ad3d76d666deacfc5de50a47e
 | 
				
			||||||
  pp (0.6.2) sha256=947ec3120c6f92195f8ee8aa25a7b2c5297bb106d83b41baa02983686577b6ff
 | 
					  pp (0.6.2) sha256=947ec3120c6f92195f8ee8aa25a7b2c5297bb106d83b41baa02983686577b6ff
 | 
				
			||||||
  prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193
 | 
					  prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193
 | 
				
			||||||
  prism (1.4.0) sha256=dc0e3e00e93160213dc2a65519d9002a4a1e7b962db57d444cf1a71565bb703e
 | 
					  prism (1.5.1) sha256=b40c1b76ccb9fcccc3d1553967cda6e79fa7274d8bfea0d98b15d27a6d187134
 | 
				
			||||||
  pry (0.15.2) sha256=12d54b8640d3fa29c9211dd4ffb08f3fd8bf7a4fd9b5a73ce5b59c8709385b6b
 | 
					  pry (0.15.2) sha256=12d54b8640d3fa29c9211dd4ffb08f3fd8bf7a4fd9b5a73ce5b59c8709385b6b
 | 
				
			||||||
  psych (5.2.6) sha256=814328aa5dcb6d604d32126a20bc1cbcf05521a5b49dbb1a8b30a07e580f316e
 | 
					  psych (5.2.6) sha256=814328aa5dcb6d604d32126a20bc1cbcf05521a5b49dbb1a8b30a07e580f316e
 | 
				
			||||||
  public_suffix (6.0.1) sha256=61d44e1cab5cbbbe5b31068481cf16976dd0dc1b6b07bd95617ef8c5e3e00c6f
 | 
					  public_suffix (6.0.1) sha256=61d44e1cab5cbbbe5b31068481cf16976dd0dc1b6b07bd95617ef8c5e3e00c6f
 | 
				
			||||||
@ -610,7 +610,7 @@ CHECKSUMS
 | 
				
			|||||||
  react-rails (3.2.1) sha256=2235db0b240517596b1cb3e26177ab5bc64d3a56579b0415ee242b1691f81f64
 | 
					  react-rails (3.2.1) sha256=2235db0b240517596b1cb3e26177ab5bc64d3a56579b0415ee242b1691f81f64
 | 
				
			||||||
  redis (5.4.1) sha256=b5e675b57ad22b15c9bcc765d5ac26f60b675408af916d31527af9bd5a81faae
 | 
					  redis (5.4.1) sha256=b5e675b57ad22b15c9bcc765d5ac26f60b675408af916d31527af9bd5a81faae
 | 
				
			||||||
  redis-client (0.23.2) sha256=e33bab6682c8155cfef95e6dd296936bb9c2981a89fb578ace27a076fa2836fa
 | 
					  redis-client (0.23.2) sha256=e33bab6682c8155cfef95e6dd296936bb9c2981a89fb578ace27a076fa2836fa
 | 
				
			||||||
  regexp_parser (2.11.0) sha256=d9dd78b475d18893ce3da55ea1a913499b75f26180a3463e9233d7e419c0cd40
 | 
					  regexp_parser (2.11.3) sha256=ca13f381a173b7a93450e53459075c9b76a10433caadcb2f1180f2c741fc55a4
 | 
				
			||||||
  reline (0.6.2) sha256=1dad26a6008872d59c8e05244b119347c9f2ddaf4a53dce97856cd5f30a02846
 | 
					  reline (0.6.2) sha256=1dad26a6008872d59c8e05244b119347c9f2ddaf4a53dce97856cd5f30a02846
 | 
				
			||||||
  responders (3.1.1) sha256=92f2a87e09028347368639cfb468f5fefa745cb0dc2377ef060db1cdd79a341a
 | 
					  responders (3.1.1) sha256=92f2a87e09028347368639cfb468f5fefa745cb0dc2377ef060db1cdd79a341a
 | 
				
			||||||
  rexml (3.3.9) sha256=d71875b85299f341edf47d44df0212e7658cbdf35aeb69cefdb63f57af3137c9
 | 
					  rexml (3.3.9) sha256=d71875b85299f341edf47d44df0212e7658cbdf35aeb69cefdb63f57af3137c9
 | 
				
			||||||
@ -625,10 +625,10 @@ CHECKSUMS
 | 
				
			|||||||
  rswag-api (2.16.0) sha256=b653f7bd92e98be18b01ab4525d88950d7b0960e293a99f856b9efcee3ae6074
 | 
					  rswag-api (2.16.0) sha256=b653f7bd92e98be18b01ab4525d88950d7b0960e293a99f856b9efcee3ae6074
 | 
				
			||||||
  rswag-specs (2.16.0) sha256=8ba26085c408b0bd2ed21dc8015c80f417c7d34c63720ab7133c2549b5bd2a91
 | 
					  rswag-specs (2.16.0) sha256=8ba26085c408b0bd2ed21dc8015c80f417c7d34c63720ab7133c2549b5bd2a91
 | 
				
			||||||
  rswag-ui (2.16.0) sha256=a1f49e927dceda92e6e6e7c1000f1e217ee66c565f69e28131dc98b33cd3a04f
 | 
					  rswag-ui (2.16.0) sha256=a1f49e927dceda92e6e6e7c1000f1e217ee66c565f69e28131dc98b33cd3a04f
 | 
				
			||||||
  rubocop (1.79.2) sha256=d3f42a7d197952c2a163719c5462fea827710a435b18bfb7070c6eedd2e90391
 | 
					  rubocop (1.80.2) sha256=6485f30fefcf5c199db3b91e5e253b1ef43f7e564784e2315255809a3dd9abf4
 | 
				
			||||||
  rubocop-ast (1.46.0) sha256=0da7f6ad5b98614f89b74f11873c191059c823eae07d6ffd40a42a3338f2232b
 | 
					  rubocop-ast (1.46.0) sha256=0da7f6ad5b98614f89b74f11873c191059c823eae07d6ffd40a42a3338f2232b
 | 
				
			||||||
  rubocop-factory_bot (2.27.1) sha256=9d744b5916778c1848e5fe6777cc69855bd96548853554ec239ba9961b8573fe
 | 
					  rubocop-factory_bot (2.27.1) sha256=9d744b5916778c1848e5fe6777cc69855bd96548853554ec239ba9961b8573fe
 | 
				
			||||||
  rubocop-rails (2.32.0) sha256=9fcc623c8722fe71e835e99c4a18b740b5b0d3fb69915d7f0777f00794b30490
 | 
					  rubocop-rails (2.33.3) sha256=848c011b58c1292f3066246c9eb18abf6ffcfbce28bc57c4ab888bbec79af74b
 | 
				
			||||||
  rubocop-rspec (3.6.0) sha256=c0e4205871776727e54dee9cc91af5fd74578001551ba40e1fe1a1ab4b404479
 | 
					  rubocop-rspec (3.6.0) sha256=c0e4205871776727e54dee9cc91af5fd74578001551ba40e1fe1a1ab4b404479
 | 
				
			||||||
  rubocop-rspec_rails (2.31.0) sha256=775375e18a26a1184a812ef3054b79d218e85601b9ae897f38f8be24dddf1f45
 | 
					  rubocop-rspec_rails (2.31.0) sha256=775375e18a26a1184a812ef3054b79d218e85601b9ae897f38f8be24dddf1f45
 | 
				
			||||||
  ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
 | 
					  ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
 | 
				
			||||||
 | 
				
			|||||||
@ -9,10 +9,10 @@ class TablesArrangementsController < ApplicationController
 | 
				
			|||||||
    render json: TablesArrangement
 | 
					    render json: TablesArrangement
 | 
				
			||||||
      .order(valid: :desc)
 | 
					      .order(valid: :desc)
 | 
				
			||||||
      .order(discomfort: :asc)
 | 
					      .order(discomfort: :asc)
 | 
				
			||||||
      .select(:id, :name, :discomfort)
 | 
					      .select(:id, :name, :discomfort, :status, :progress)
 | 
				
			||||||
      .select("digest = '#{current_digest}'::uuid as valid")
 | 
					      .select("digest = '#{current_digest}'::uuid OR discomfort IS NULL as valid")
 | 
				
			||||||
      .limit(20)
 | 
					      .limit(20)
 | 
				
			||||||
      .as_json(only: %i[id name discomfort valid])
 | 
					      .as_json(only: %i[id name discomfort valid status progress])
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def show
 | 
					  def show
 | 
				
			||||||
@ -25,7 +25,10 @@ class TablesArrangementsController < ApplicationController
 | 
				
			|||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def create
 | 
					  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
 | 
					    render json: {}, status: :created
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
				
			|||||||
@ -8,16 +8,35 @@ class TableSimulatorJob < ApplicationJob
 | 
				
			|||||||
  MIN_PER_TABLE = 8
 | 
					  MIN_PER_TABLE = 8
 | 
				
			||||||
  MAX_PER_TABLE = 10
 | 
					  MAX_PER_TABLE = 10
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def perform(wedding_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
 | 
					    ActsAsTenant.with_tenant(Wedding.find(wedding_id)) do
 | 
				
			||||||
      engine = VNS::Engine.new
 | 
					      engine = VNS::Engine.new
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      engine.add_optimization(Tables::Swap)
 | 
					      engine.add_optimization(Tables::Swap)
 | 
				
			||||||
      engine.add_optimization(Tables::Shift)
 | 
					      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.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.initial_solution = initial_solution
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      engine.target_function(&:discomfort)
 | 
					      engine.target_function(&:discomfort)
 | 
				
			||||||
@ -25,6 +44,8 @@ class TableSimulatorJob < ApplicationJob
 | 
				
			|||||||
      best_solution = engine.run
 | 
					      best_solution = engine.run
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      best_solution.save!
 | 
					      best_solution.save!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      tables_arrangement.update_columns(status: :completed)
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
				
			|||||||
@ -29,5 +29,5 @@
 | 
				
			|||||||
class Seat < ApplicationRecord
 | 
					class Seat < ApplicationRecord
 | 
				
			||||||
  acts_as_tenant :wedding
 | 
					  acts_as_tenant :wedding
 | 
				
			||||||
  belongs_to :guest
 | 
					  belongs_to :guest
 | 
				
			||||||
  belongs_to :table_arrangement
 | 
					  belongs_to :tables_arrangement
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
				
			|||||||
@ -10,6 +10,8 @@
 | 
				
			|||||||
#  digest     :uuid             not null
 | 
					#  digest     :uuid             not null
 | 
				
			||||||
#  discomfort :integer
 | 
					#  discomfort :integer
 | 
				
			||||||
#  name       :string           not null
 | 
					#  name       :string           not null
 | 
				
			||||||
 | 
					#  progress   :float            default(0.0), not null
 | 
				
			||||||
 | 
					#  status     :string           default("complete"), not null
 | 
				
			||||||
#  created_at :datetime         not null
 | 
					#  created_at :datetime         not null
 | 
				
			||||||
#  updated_at :datetime         not null
 | 
					#  updated_at :datetime         not null
 | 
				
			||||||
#  wedding_id :uuid             not null
 | 
					#  wedding_id :uuid             not null
 | 
				
			||||||
 | 
				
			|||||||
@ -12,13 +12,14 @@ module Tables
 | 
				
			|||||||
      end
 | 
					      end
 | 
				
			||||||
    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:, tables_arrangement_id:, hierarchy: AffinityGroupsHierarchy.new)
 | 
				
			||||||
      @min_per_table = min_per_table
 | 
					      @min_per_table = min_per_table
 | 
				
			||||||
      @max_per_table = max_per_table
 | 
					      @max_per_table = max_per_table
 | 
				
			||||||
      @hierarchy = hierarchy
 | 
					      @hierarchy = hierarchy
 | 
				
			||||||
      @tables = []
 | 
					      @tables = []
 | 
				
			||||||
 | 
					      @tables_arrangement_id = tables_arrangement_id
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def random_distribution(people, random: Random.new)
 | 
					    def random_distribution(people, random: Random.new)
 | 
				
			||||||
@ -42,15 +43,23 @@ module Tables
 | 
				
			|||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def deep_dup
 | 
					    def deep_dup
 | 
				
			||||||
      self.class.new(min_per_table: @min_per_table, max_per_table: @max_per_table,
 | 
					      self.class.new(
 | 
				
			||||||
                     hierarchy: @hierarchy).tap do |new_distribution|
 | 
					        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)
 | 
					        new_distribution.tables = @tables.map(&:dup)
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def save!
 | 
					    def save!
 | 
				
			||||||
      ActiveRecord::Base.transaction do
 | 
					      ActiveRecord::Base.transaction do
 | 
				
			||||||
        arrangement = TablesArrangement.create!
 | 
					        arrangement = TablesArrangement.find(tables_arrangement_id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.tables_arrangement_id = arrangement.id
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        arrangement.seats.delete_all
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        records_to_store = []
 | 
					        records_to_store = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -5,6 +5,7 @@
 | 
				
			|||||||
module VNS
 | 
					module VNS
 | 
				
			||||||
  class Engine
 | 
					  class Engine
 | 
				
			||||||
    PERTURBATION_SIZES = [1, 1, 1, 2, 2, 3].freeze
 | 
					    PERTURBATION_SIZES = [1, 1, 1, 2, 2, 3].freeze
 | 
				
			||||||
 | 
					    ITERATIONS = 50
 | 
				
			||||||
    class << self
 | 
					    class << self
 | 
				
			||||||
      def sequence(elements)
 | 
					      def sequence(elements)
 | 
				
			||||||
        elements = elements.to_a
 | 
					        elements = elements.to_a
 | 
				
			||||||
@ -12,6 +13,10 @@ module VNS
 | 
				
			|||||||
      end
 | 
					      end
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def initialize
 | 
				
			||||||
 | 
					      @perturbations = Set.new
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def target_function(&function)
 | 
					    def target_function(&function)
 | 
				
			||||||
      @target_function = function
 | 
					      @target_function = function
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
@ -22,36 +27,45 @@ module VNS
 | 
				
			|||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def add_perturbation(klass)
 | 
					    def add_perturbation(klass)
 | 
				
			||||||
      @perturbations ||= Set.new
 | 
					 | 
				
			||||||
      @perturbations << klass
 | 
					      @perturbations << klass
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def notify_progress(&block)
 | 
				
			||||||
 | 
					      @progress_notifier = block
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def on_better_solution(&block)
 | 
				
			||||||
 | 
					      @better_solution_notifier = block
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    attr_writer :initial_solution
 | 
					    attr_writer :initial_solution
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def run
 | 
					    def run
 | 
				
			||||||
      raise 'No target function defined' unless @target_function
 | 
					      check_preconditions!
 | 
				
			||||||
      raise 'No optimizations defined' unless @optimizations
 | 
					 | 
				
			||||||
      raise 'No initial solution defined' unless @initial_solution
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      @perturbations ||= Set.new
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      @current_solution = @initial_solution
 | 
					      @current_solution = @initial_solution
 | 
				
			||||||
      @best_score = @target_function.call(@current_solution)
 | 
					      @best_score = @target_function.call(@current_solution)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      run_all_optimizations
 | 
					      run_all_optimizations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      @progress_notifier&.call(Rational(1, ITERATIONS + 1))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      best_solution = @current_solution
 | 
					      best_solution = @current_solution
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      50.times do
 | 
					      (1..ITERATIONS).each do |iteration|
 | 
				
			||||||
        @current_solution = Tables::WheelSwap.new(best_solution).call(PERTURBATION_SIZES.sample)
 | 
					        @current_solution = Tables::WheelSwap.new(best_solution).call(PERTURBATION_SIZES.sample)
 | 
				
			||||||
        @best_score = @target_function.call(@current_solution)
 | 
					        @best_score = @target_function.call(@current_solution)
 | 
				
			||||||
        Rails.logger.debug { "After perturbation: #{@best_score}" }
 | 
					        Rails.logger.debug { "After perturbation: #{@best_score}" }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        run_all_optimizations
 | 
					        run_all_optimizations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        @progress_notifier&.call(Rational(iteration + 1, ITERATIONS + 1))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        next unless best_solution.discomfort > @current_solution.discomfort
 | 
					        next unless best_solution.discomfort > @current_solution.discomfort
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        best_solution = @current_solution
 | 
					        best_solution = @current_solution
 | 
				
			||||||
 | 
					        @better_solution_notifier&.call(best_solution)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Rails.logger.debug do
 | 
					        Rails.logger.debug do
 | 
				
			||||||
          "Found better solution after perturbation optimization: #{@current_solution.discomfort}"
 | 
					          "Found better solution after perturbation optimization: #{@current_solution.discomfort}"
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
@ -62,6 +76,12 @@ module VNS
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    private
 | 
					    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
 | 
					    def run_all_optimizations
 | 
				
			||||||
      self.class.sequence(@optimizations).each do |optimization|
 | 
					      self.class.sequence(@optimizations).each do |optimization|
 | 
				
			||||||
        optimize(optimization)
 | 
					        optimize(optimization)
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										1
									
								
								bin/jobs
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								bin/jobs
									
									
									
									
									
								
							@ -3,4 +3,5 @@
 | 
				
			|||||||
require_relative "../config/environment"
 | 
					require_relative "../config/environment"
 | 
				
			||||||
require "solid_queue/cli"
 | 
					require "solid_queue/cli"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					SolidQueue.logger = ActiveSupport::Logger.new($stdout)
 | 
				
			||||||
SolidQueue::Cli.start(ARGV)
 | 
					SolidQueue::Cli.start(ARGV)
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					class AddStatusColumnToTablesArrangements < ActiveRecord::Migration[8.0]
 | 
				
			||||||
 | 
					  def change
 | 
				
			||||||
 | 
					    add_column :tables_arrangements, :status, :string, default: :complete, null: false
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					class AddProgressToTablesArrangements < ActiveRecord::Migration[8.0]
 | 
				
			||||||
 | 
					  def change
 | 
				
			||||||
 | 
					    add_column :tables_arrangements, :progress, :float, default: 0, null: false
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
							
								
								
									
										4
									
								
								db/schema.rb
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4
									
								
								db/schema.rb
									
									
									
										generated
									
									
									
								
							@ -10,7 +10,7 @@
 | 
				
			|||||||
#
 | 
					#
 | 
				
			||||||
# It's strongly recommended that you check this file into your version control system.
 | 
					# 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_09_08_145119) do
 | 
				
			||||||
  # These are extensions that must be enabled in order to support this database
 | 
					  # These are extensions that must be enabled in order to support this database
 | 
				
			||||||
  enable_extension "pg_catalog.plpgsql"
 | 
					  enable_extension "pg_catalog.plpgsql"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -216,6 +216,8 @@ ActiveRecord::Schema[8.0].define(version: 2025_06_08_181054) do
 | 
				
			|||||||
    t.string "name", null: false
 | 
					    t.string "name", null: false
 | 
				
			||||||
    t.uuid "wedding_id", null: false
 | 
					    t.uuid "wedding_id", null: false
 | 
				
			||||||
    t.uuid "digest", default: -> { "gen_random_uuid()" }, 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"
 | 
					    t.index ["wedding_id"], name: "index_tables_arrangements_on_wedding_id"
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -86,7 +86,9 @@ ActsAsTenant.with_tenant(wedding) do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  # TODO: Clean up invitations with no guests
 | 
					  # 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)) }
 | 
					  "red".dup.paint.palette.triad(as: :hex).zip(Group.roots).each { |(color, group)| group.update!(color: color.paint.desaturate(40)) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -21,6 +21,14 @@ namespace :vns do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      engine.target_function(&:discomfort)
 | 
					      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 }
 | 
					      solution = Rails.benchmark('VNS Benchmarking') { engine.run }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      Rails.logger.info "Best solution found with discomfort: #{solution.discomfort}"
 | 
					      Rails.logger.info "Best solution found with discomfort: #{solution.discomfort}"
 | 
				
			||||||
 | 
				
			|||||||
@ -12,6 +12,11 @@ server {
 | 
				
			|||||||
        proxy_set_header Host $http_host;
 | 
					        proxy_set_header Host $http_host;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    location /jobs/ {
 | 
				
			||||||
 | 
					        proxy_pass http://backend:3000/jobs/;
 | 
				
			||||||
 | 
					        proxy_set_header Host $http_host;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    location /captcha/v2/media/ {
 | 
					    location /captcha/v2/media/ {
 | 
				
			||||||
        proxy_pass http://libre-captcha:8888/v2/media/;
 | 
					        proxy_pass http://libre-captcha:8888/v2/media/;
 | 
				
			||||||
        proxy_set_header Host $http_host;
 | 
					        proxy_set_header Host $http_host;
 | 
				
			||||||
 | 
				
			|||||||
@ -19,7 +19,8 @@ RSpec.describe 'tables_arrangements' do
 | 
				
			|||||||
                   id: { type: :string, format: :uuid },
 | 
					                   id: { type: :string, format: :uuid },
 | 
				
			||||||
                   name: { type: :string },
 | 
					                   name: { type: :string },
 | 
				
			||||||
                   discomfort: { type: :integer },
 | 
					                   discomfort: { type: :integer },
 | 
				
			||||||
                   valid: { type: :boolean }
 | 
					                   valid: { type: :boolean },
 | 
				
			||||||
 | 
					                   status: { type: :string, enum: %w[complete in_progress] }
 | 
				
			||||||
                 }
 | 
					                 }
 | 
				
			||||||
               }
 | 
					               }
 | 
				
			||||||
        xit
 | 
					        xit
 | 
				
			||||||
 | 
				
			|||||||
@ -6,8 +6,43 @@ require 'rails_helper'
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
module Tables
 | 
					module Tables
 | 
				
			||||||
  RSpec.describe Distribution do
 | 
					  RSpec.describe Distribution do
 | 
				
			||||||
 | 
					    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, 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) }
 | 
				
			||||||
 | 
					      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
 | 
					    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
 | 
					      context 'when there are fewer people than the minimum per table' do
 | 
				
			||||||
        it 'creates one table' do
 | 
					        it 'creates one table' do
 | 
				
			||||||
 | 
				
			|||||||
@ -17,7 +17,7 @@ module Tables
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      context 'when there are two tables with two people each' do
 | 
					      context 'when there are two tables with two people each' do
 | 
				
			||||||
        let(:initial_solution) 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[:a, :b].to_table
 | 
				
			||||||
            distribution.tables << Set[:c, :d].to_table
 | 
					            distribution.tables << Set[:c, :d].to_table
 | 
				
			||||||
          end
 | 
					          end
 | 
				
			||||||
@ -35,7 +35,7 @@ module Tables
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      context 'when there are two tables with three people each' do
 | 
					      context 'when there are two tables with three people each' do
 | 
				
			||||||
        let(:initial_solution) 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[:a, :b, :c].to_table
 | 
				
			||||||
            distribution.tables << Set[:d, :e, :f].to_table
 | 
					            distribution.tables << Set[:d, :e, :f].to_table
 | 
				
			||||||
          end
 | 
					          end
 | 
				
			||||||
 | 
				
			|||||||
@ -17,7 +17,7 @@ module Tables
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      context 'when there are two tables with two people each' do
 | 
					      context 'when there are two tables with two people each' do
 | 
				
			||||||
        let(:initial_solution) 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[:a, :b].to_table
 | 
				
			||||||
            distribution.tables << Set[:c, :d].to_table
 | 
					            distribution.tables << Set[:c, :d].to_table
 | 
				
			||||||
          end
 | 
					          end
 | 
				
			||||||
@ -35,7 +35,7 @@ module Tables
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      context 'when there are two tables with three people each' do
 | 
					      context 'when there are two tables with three people each' do
 | 
				
			||||||
        let(:initial_solution) 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[:a, :b, :c].to_table
 | 
				
			||||||
            distribution.tables << Set[:d, :e, :f].to_table
 | 
					            distribution.tables << Set[:d, :e, :f].to_table
 | 
				
			||||||
          end
 | 
					          end
 | 
				
			||||||
@ -58,7 +58,7 @@ module Tables
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      context 'when there are three tables with two people each' do
 | 
					      context 'when there are three tables with two people each' do
 | 
				
			||||||
        let(:initial_solution) 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[:a, :b].to_table
 | 
				
			||||||
            distribution.tables << Set[:c, :d].to_table
 | 
					            distribution.tables << Set[:c, :d].to_table
 | 
				
			||||||
            distribution.tables << Set[:e, :f].to_table
 | 
					            distribution.tables << Set[:e, :f].to_table
 | 
				
			||||||
 | 
				
			|||||||
@ -8,7 +8,7 @@ module Tables
 | 
				
			|||||||
  RSpec.describe WheelSwap do
 | 
					  RSpec.describe WheelSwap do
 | 
				
			||||||
    context 'when the solution has three tables' do
 | 
					    context 'when the solution has three tables' do
 | 
				
			||||||
      let(:initial_solution) 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[:a, :b, :c].to_table
 | 
				
			||||||
          distribution.tables << Set[:d, :e, :f].to_table
 | 
					          distribution.tables << Set[:d, :e, :f].to_table
 | 
				
			||||||
          distribution.tables << Set[:g, :h, :i].to_table
 | 
					          distribution.tables << Set[:g, :h, :i].to_table
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user