Compare commits
No commits in common. "main" and "specs-cohesion_penalty" have entirely different histories.
main
...
specs-cohe
42
Gemfile.lock
42
Gemfile.lock
@ -113,7 +113,7 @@ GEM
|
||||
warden (~> 1.2.3)
|
||||
diff-lcs (1.6.2)
|
||||
drb (2.2.3)
|
||||
erb (5.0.2)
|
||||
erb (5.0.1)
|
||||
erubi (1.13.1)
|
||||
et-orbi (1.2.11)
|
||||
tzinfo
|
||||
@ -140,7 +140,7 @@ GEM
|
||||
actionpack (>= 6.0.0)
|
||||
activesupport (>= 6.0.0)
|
||||
railties (>= 6.0.0)
|
||||
io-console (0.8.1)
|
||||
io-console (0.8.0)
|
||||
irb (1.15.2)
|
||||
pp (>= 0.6.0)
|
||||
rdoc (>= 4.0.0)
|
||||
@ -211,18 +211,18 @@ GEM
|
||||
net-smtp (0.5.1)
|
||||
net-protocol
|
||||
nio4r (2.7.4)
|
||||
nokogiri (1.18.9)
|
||||
nokogiri (1.18.8)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.9-aarch64-linux-gnu)
|
||||
nokogiri (1.18.8-aarch64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.9-arm-linux-gnu)
|
||||
nokogiri (1.18.8-arm-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.9-arm64-darwin)
|
||||
nokogiri (1.18.8-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.9-x86_64-darwin)
|
||||
nokogiri (1.18.8-x86_64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.9-x86_64-linux-gnu)
|
||||
nokogiri (1.18.8-x86_64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
orm_adapter (0.5.0)
|
||||
ostruct (0.6.2)
|
||||
@ -305,7 +305,7 @@ GEM
|
||||
redis-client (0.23.2)
|
||||
connection_pool
|
||||
regexp_parser (2.10.0)
|
||||
reline (0.6.2)
|
||||
reline (0.6.1)
|
||||
io-console (~> 0.5)
|
||||
responders (3.1.1)
|
||||
actionpack (>= 5.2)
|
||||
@ -384,13 +384,13 @@ GEM
|
||||
securerandom (0.4.1)
|
||||
shoulda-matchers (6.5.0)
|
||||
activesupport (>= 5.2.0)
|
||||
solid_queue (1.2.1)
|
||||
solid_queue (1.2.0)
|
||||
activejob (>= 7.1)
|
||||
activerecord (>= 7.1)
|
||||
concurrent-ruby (>= 1.3.1)
|
||||
fugit (~> 1.11.0)
|
||||
railties (>= 7.1)
|
||||
thor (>= 1.3.1)
|
||||
thor (~> 1.3.1)
|
||||
sprockets (4.2.1)
|
||||
concurrent-ruby (~> 1.0)
|
||||
rack (>= 2.2.4, < 4)
|
||||
@ -521,7 +521,7 @@ CHECKSUMS
|
||||
devise (4.9.4) sha256=920042fe5e704c548aa4eb65ebdd65980b83ffae67feb32c697206bfd975a7f8
|
||||
diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962
|
||||
drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373
|
||||
erb (5.0.2) sha256=d30f258143d4300fb4ecf430042ac12970c9bb4b33c974a545b8f58c1ec26c0f
|
||||
erb (5.0.1) sha256=760439803b36cc93eca8a266aab614614e588024a89bc30a62e78d98ff452c23
|
||||
erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9
|
||||
et-orbi (1.2.11) sha256=d26e868cc21db88280a9ec1a50aa3da5d267eb9b2037ba7b831d6c2731f5df64
|
||||
execjs (2.9.1) sha256=e8fd066f6df60c8e8fbebc32c6fb356b5212c77374e8416a9019ca4bb154dcfb
|
||||
@ -533,7 +533,7 @@ CHECKSUMS
|
||||
httparty (0.23.1) sha256=3ac1dd62f2010f6ece551716f5ceec2b2012011d89f1751917ab7f724e966b55
|
||||
i18n (1.14.7) sha256=ceba573f8138ff2c0915427f1fc5bdf4aa3ab8ae88c8ce255eb3ecf0a11a5d0f
|
||||
importmap-rails (2.1.0) sha256=9f10c67d60651a547579f448100d033df311c5d5db578301374aeb774faae741
|
||||
io-console (0.8.1) sha256=1e15440a6b2f67b6ea496df7c474ed62c860ad11237f29b3bd187f054b925fcb
|
||||
io-console (0.8.0) sha256=cd6a9facbc69871d69b2cb8b926fc6ea7ef06f06e505e81a64f14a470fddefa2
|
||||
irb (1.15.2) sha256=222f32952e278da34b58ffe45e8634bf4afc2dc7aa9da23fed67e581aa50fdba
|
||||
jbuilder (2.13.0) sha256=7200a38a1c0081aa81b7a9757e7a299db75bc58cf1fd45ca7919a91627d227d6
|
||||
json (2.12.2) sha256=ba94a48ad265605c8fa9a50a5892f3ba6a02661aa010f638211f3cb36f44abf4
|
||||
@ -566,12 +566,12 @@ CHECKSUMS
|
||||
net-protocol (0.2.2) sha256=aa73e0cba6a125369de9837b8d8ef82a61849360eba0521900e2c3713aa162a8
|
||||
net-smtp (0.5.1) sha256=ed96a0af63c524fceb4b29b0d352195c30d82dd916a42f03c62a3a70e5b70736
|
||||
nio4r (2.7.4) sha256=d95dee68e0bb251b8ff90ac3423a511e3b784124e5db7ff5f4813a220ae73ca9
|
||||
nokogiri (1.18.9) sha256=ac5a7d93fd0e3cef388800b037407890882413feccca79eb0272a2715a82fa33
|
||||
nokogiri (1.18.9-aarch64-linux-gnu) sha256=5bcfdf7aa8d1056a7ad5e52e1adffc64ef53d12d0724fbc6f458a3af1a4b9e32
|
||||
nokogiri (1.18.9-arm-linux-gnu) sha256=fe611ae65880e445a9c0f650d52327db239f3488626df4173c05beafd161d46e
|
||||
nokogiri (1.18.9-arm64-darwin) sha256=eea3f1f06463ff6309d3ff5b88033c4948d0da1ab3cc0a3a24f63c4d4a763979
|
||||
nokogiri (1.18.9-x86_64-darwin) sha256=e0d2deb03d3d7af8016e8c9df5ff4a7d692159cefb135cbb6a4109f265652348
|
||||
nokogiri (1.18.9-x86_64-linux-gnu) sha256=b52f5defedc53d14f71eeaaf990da66b077e1918a2e13088b6a96d0230f44360
|
||||
nokogiri (1.18.8) sha256=8c7464875d9ca7f71080c24c0db7bcaa3940e8be3c6fc4bcebccf8b9a0016365
|
||||
nokogiri (1.18.8-aarch64-linux-gnu) sha256=36badd2eb281fca6214a5188e24a34399b15d89730639a068d12931e2adc210e
|
||||
nokogiri (1.18.8-arm-linux-gnu) sha256=17de01ca3adf9f8e187883ed73c672344d3dbb3c260f88ffa1008e8dc255a28e
|
||||
nokogiri (1.18.8-arm64-darwin) sha256=483b5b9fb33653f6f05cbe00d09ea315f268f0e707cfc809aa39b62993008212
|
||||
nokogiri (1.18.8-x86_64-darwin) sha256=024cdfe7d9ae3466bba6c06f348fb2a8395d9426b66a3c82f1961b907945cc0c
|
||||
nokogiri (1.18.8-x86_64-linux-gnu) sha256=4a747875db873d18a2985ee2c320a6070c4a414ad629da625fbc58d1a20e5ecc
|
||||
orm_adapter (0.5.0) sha256=aa5d0be5d540cbb46d3a93e88061f4ece6a25f6e97d6a47122beb84fe595e9b9
|
||||
ostruct (0.6.2) sha256=6d7302a299e400a2c248d6ce0dad18fc3a5714e8096facc25ffd0c54ee57cfc0
|
||||
parallel (1.27.0) sha256=4ac151e1806b755fb4e2dc2332cbf0e54f2e24ba821ff2d3dcf86bf6dc4ae130
|
||||
@ -603,7 +603,7 @@ CHECKSUMS
|
||||
redis (5.4.1) sha256=b5e675b57ad22b15c9bcc765d5ac26f60b675408af916d31527af9bd5a81faae
|
||||
redis-client (0.23.2) sha256=e33bab6682c8155cfef95e6dd296936bb9c2981a89fb578ace27a076fa2836fa
|
||||
regexp_parser (2.10.0) sha256=cb6f0ddde88772cd64bff1dbbf68df66d376043fe2e66a9ef77fcb1b0c548c61
|
||||
reline (0.6.2) sha256=1dad26a6008872d59c8e05244b119347c9f2ddaf4a53dce97856cd5f30a02846
|
||||
reline (0.6.1) sha256=1afcc9d7cb1029cdbe780d72f2f09251ce46d3780050f3ec39c3ccc6b60675fb
|
||||
responders (3.1.1) sha256=92f2a87e09028347368639cfb468f5fefa745cb0dc2377ef060db1cdd79a341a
|
||||
rexml (3.3.9) sha256=d71875b85299f341edf47d44df0212e7658cbdf35aeb69cefdb63f57af3137c9
|
||||
rqrcode (3.1.0) sha256=e2d5996375f6e9a013823c289ed575dbea678b8e0388574302c1fac563f098af
|
||||
@ -628,7 +628,7 @@ CHECKSUMS
|
||||
rubyzip (2.3.2) sha256=3f57e3935dc2255c414484fbf8d673b4909d8a6a57007ed754dde39342d2373f
|
||||
securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1
|
||||
shoulda-matchers (6.5.0) sha256=ef6b572b2bed1ac4aba6ab2c5ff345a24b6d055a93a3d1c3bfc86d9d499e3f44
|
||||
solid_queue (1.2.1) sha256=7976b3690a08080ef63d1b11281f0b77398f7697dbeda0e2c5532682639d4b15
|
||||
solid_queue (1.2.0) sha256=482ac5305cbe91ebf845627caec493fda8545bf22b18205df01afb80999e28de
|
||||
sprockets (4.2.1) sha256=951b13dd2f2fcae840a7184722689a803e0ff9d2702d902bd844b196da773f97
|
||||
sprockets-rails (3.5.2) sha256=a9e88e6ce9f8c912d349aa5401509165ec42326baf9e942a85de4b76dbc4119e
|
||||
stimulus-rails (1.3.4) sha256=765676ffa1f33af64ce026d26b48e8ffb2e0b94e0f50e9119e11d6107d67cb06
|
||||
|
@ -12,8 +12,8 @@ class TableSimulatorJob < ApplicationJob
|
||||
ActsAsTenant.with_tenant(Wedding.find(wedding_id)) do
|
||||
engine = VNS::Engine.new
|
||||
|
||||
engine.add_optimization(Tables::Swap)
|
||||
engine.add_optimization(Tables::Shift)
|
||||
engine.add_perturbation(Tables::Swap)
|
||||
engine.add_perturbation(Tables::Shift)
|
||||
|
||||
initial_solution = Tables::Distribution.new(min_per_table: MIN_PER_TABLE, max_per_table: MAX_PER_TABLE)
|
||||
initial_solution.random_distribution(Guest.potential.shuffle)
|
||||
|
@ -16,7 +16,6 @@ class AffinityGroupsHierarchy < Array
|
||||
end
|
||||
|
||||
discomforts
|
||||
invitation_counts
|
||||
freeze
|
||||
end
|
||||
|
||||
@ -55,16 +54,8 @@ class AffinityGroupsHierarchy < Array
|
||||
Rational(dist, dist + 1)
|
||||
end
|
||||
|
||||
def guest_count(invitation_id)
|
||||
@invitation_counts[invitation_id] || 0
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def invitation_counts
|
||||
@invitation_counts = Guest.where.not(invitation_id: nil).group(:invitation_id).count
|
||||
end
|
||||
|
||||
def discomforts
|
||||
@discomforts ||= GroupAffinity.pluck(:group_a_id, :group_b_id,
|
||||
:discomfort).each_with_object({}) do |(id_a, id_b, discomfort), acc|
|
||||
|
@ -15,7 +15,7 @@ module Tables
|
||||
end
|
||||
|
||||
def breakdown
|
||||
@breakdown ||= { table_size_penalty:, cohesion_penalty:, invitations_penalty: }
|
||||
@breakdown ||= { table_size_penalty:, cohesion_penalty: }
|
||||
end
|
||||
|
||||
private
|
||||
@ -39,12 +39,6 @@ module Tables
|
||||
10 * (cohesion_discomfort * 1.0 / table.size)
|
||||
end
|
||||
|
||||
def invitations_penalty
|
||||
2 * table.map(&:invitation_id)
|
||||
.tally
|
||||
.sum { |invitation_id, guests_in_table| hierarchy.guest_count(invitation_id) - guests_in_table }
|
||||
end
|
||||
|
||||
#
|
||||
# Calculates the discomfort of the table based on the cohesion of the guests. The total discomfort
|
||||
# is calculated as the sum of the discomfort of each pair of guests. The discomfort of a pair of
|
||||
|
@ -1,33 +0,0 @@
|
||||
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
|
||||
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Tables
|
||||
class WheelSwap
|
||||
private attr_reader :initial_solution
|
||||
def initialize(initial_solution)
|
||||
@initial_solution = initial_solution
|
||||
end
|
||||
|
||||
def call(size = 1)
|
||||
Rails.logger.debug { "WheelSwap with size: #{size}" }
|
||||
new_solution = @initial_solution.deep_dup
|
||||
|
||||
selected_guests = []
|
||||
|
||||
size.times do
|
||||
selected_guests += new_solution.tables.map(&:pop)
|
||||
end
|
||||
|
||||
selected_guests.shuffle!
|
||||
|
||||
tables = new_solution.tables.cycle
|
||||
|
||||
tables.next << selected_guests.pop while selected_guests.any?
|
||||
|
||||
new_solution.tables.each(&:reset)
|
||||
|
||||
new_solution
|
||||
end
|
||||
end
|
||||
end
|
@ -4,7 +4,6 @@
|
||||
|
||||
module VNS
|
||||
class Engine
|
||||
PERTURBATION_SIZES = [1, 1, 1, 2, 2, 3].freeze
|
||||
class << self
|
||||
def sequence(elements)
|
||||
elements = elements.to_a
|
||||
@ -16,11 +15,6 @@ module VNS
|
||||
@target_function = function
|
||||
end
|
||||
|
||||
def add_optimization(klass)
|
||||
@optimizations ||= Set.new
|
||||
@optimizations << klass
|
||||
end
|
||||
|
||||
def add_perturbation(klass)
|
||||
@perturbations ||= Set.new
|
||||
@perturbations << klass
|
||||
@ -30,58 +24,32 @@ module VNS
|
||||
|
||||
def run
|
||||
raise 'No target function defined' unless @target_function
|
||||
raise 'No optimizations defined' unless @optimizations
|
||||
raise 'No perturbations defined' unless @perturbations
|
||||
raise 'No initial solution defined' unless @initial_solution
|
||||
|
||||
@perturbations ||= Set.new
|
||||
@best_solution = @initial_solution
|
||||
@best_score = @target_function.call(@best_solution)
|
||||
|
||||
@current_solution = @initial_solution
|
||||
@best_score = @target_function.call(@current_solution)
|
||||
|
||||
run_all_optimizations
|
||||
|
||||
best_solution = @current_solution
|
||||
|
||||
50.times do
|
||||
@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
|
||||
|
||||
next unless best_solution.discomfort > @current_solution.discomfort
|
||||
|
||||
best_solution = @current_solution
|
||||
Rails.logger.debug do
|
||||
"Found better solution after perturbation optimization: #{@current_solution.discomfort}"
|
||||
end
|
||||
self.class.sequence(@perturbations).each do |perturbation|
|
||||
optimize(perturbation)
|
||||
end
|
||||
|
||||
best_solution
|
||||
@best_solution
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def run_all_optimizations
|
||||
self.class.sequence(@optimizations).each do |optimization|
|
||||
optimize(optimization)
|
||||
Rails.logger.debug { "Finished optimization phase: #{optimization}" }
|
||||
end
|
||||
Rails.logger.debug { 'Finished all optimization phases' }
|
||||
end
|
||||
|
||||
def optimize(optimization_klass)
|
||||
def optimize(perturbation_klass)
|
||||
loop do
|
||||
optimized = false
|
||||
|
||||
optimization_klass.new(@current_solution).each do |alternative_solution|
|
||||
perturbation_klass.new(@best_solution).each do |alternative_solution|
|
||||
score = @target_function.call(alternative_solution)
|
||||
next if score >= @best_score
|
||||
|
||||
@current_solution = alternative_solution.deep_dup
|
||||
@best_solution = alternative_solution.deep_dup
|
||||
@best_score = score
|
||||
optimized = true
|
||||
Rails.logger.debug { "[#{optimization_klass}] Found better solution with score: #{score}" }
|
||||
|
||||
break
|
||||
end
|
||||
|
@ -10,7 +10,7 @@ Rswag::Ui.configure do |c|
|
||||
# (under openapi_root) as JSON or YAML endpoints, then the list below should
|
||||
# correspond to the relative paths for those endpoints.
|
||||
|
||||
c.openapi_endpoint '/api/api-docs/v1/swagger.yaml', 'API V1 Docs'
|
||||
c.swagger_endpoint '/api/api-docs/v1/swagger.yaml', 'API V1 Docs'
|
||||
|
||||
# Add Basic Auth in case your API is private
|
||||
# c.basic_auth_enabled = true
|
||||
|
@ -10,10 +10,4 @@ class Set
|
||||
def to_table
|
||||
Tables::Table.new(self)
|
||||
end
|
||||
|
||||
def pop
|
||||
element = self.to_a.sample
|
||||
self.delete(element)
|
||||
element
|
||||
end
|
||||
end
|
@ -8,8 +8,8 @@ namespace :vns do
|
||||
|
||||
engine = VNS::Engine.new
|
||||
|
||||
engine.add_optimization(Tables::Swap)
|
||||
engine.add_optimization(Tables::Shift)
|
||||
engine.add_perturbation(Tables::Swap)
|
||||
engine.add_perturbation(Tables::Shift)
|
||||
|
||||
hierarchy = AffinityGroupsHierarchy.new
|
||||
initial_solution = Tables::Distribution.new(min_per_table: 8, max_per_table: 10, hierarchy:)
|
||||
|
@ -14,14 +14,14 @@ module Tables
|
||||
|
||||
describe '#calculate' do
|
||||
before do
|
||||
allow(calculator).to receive_messages(table_size_penalty: 2, cohesion_penalty: 5, invitations_penalty: 4)
|
||||
allow(calculator).to receive_messages(table_size_penalty: 2, cohesion_discomfort: 3)
|
||||
end
|
||||
|
||||
let(:table) { Table.new(create_list(:guest, 6)) }
|
||||
|
||||
it 'returns the sum of the table size penalty and the average cohesion penalty', :aggregate_failures do
|
||||
expect(calculator.calculate).to eq(11)
|
||||
expect(calculator.breakdown).to eq(table_size_penalty: 2, cohesion_penalty: 5, invitations_penalty: 4)
|
||||
expect(calculator.calculate).to eq(7)
|
||||
expect(calculator.breakdown).to eq(table_size_penalty: 2, cohesion_penalty: 5)
|
||||
end
|
||||
end
|
||||
|
||||
@ -131,44 +131,5 @@ module Tables
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#invitations_penalty' do
|
||||
let(:invitation_a) { create(:invitation) }
|
||||
let(:invitation_b) { create(:invitation) }
|
||||
let(:invitation_c) { create(:invitation) }
|
||||
|
||||
let(:table) do
|
||||
create_list(:guest, 2, invitation: invitation_a) +
|
||||
create_list(:guest, 3, invitation: invitation_b) +
|
||||
create_list(:guest, 4, invitation: invitation_c)
|
||||
end
|
||||
|
||||
context 'when the table contains all members of an invitation' do
|
||||
it 'returns 0 as penalty' do
|
||||
expect(calculator.send(:invitations_penalty)).to eq(0)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is an additional guest of one of the invitations that is not included' do
|
||||
before do
|
||||
create(:guest, invitation: invitation_a)
|
||||
end
|
||||
|
||||
it 'returns the penalty for the missing guest' do
|
||||
expect(calculator.send(:invitations_penalty)).to eq(2)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are multiple guests missing from different invitations' do
|
||||
before do
|
||||
create(:guest, invitation: invitation_b)
|
||||
create(:guest, invitation: invitation_c)
|
||||
end
|
||||
|
||||
it 'returns 2x # of guests left out as the total penalty for all missing guests' do
|
||||
expect(calculator.send(:invitations_penalty)).to eq(4)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -1,28 +0,0 @@
|
||||
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
|
||||
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
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.tables << Set[:a, :b, :c].to_table
|
||||
distribution.tables << Set[:d, :e, :f].to_table
|
||||
distribution.tables << Set[:g, :h, :i].to_table
|
||||
end
|
||||
end
|
||||
|
||||
it 'swaps a random guest from each table with a guest from another table', :aggregate_failures do
|
||||
result = described_class.new(initial_solution).call
|
||||
|
||||
expect(result.tables.size).to eq(3)
|
||||
expect(result.tables.map(&:size)).to all(eq(3))
|
||||
|
||||
expect(result.tables.map(&:to_a).flatten).to match_array((:a..:i).to_a)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
x
Reference in New Issue
Block a user