stable-matching 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.gitlab-ci.yml +28 -0
- data/.rspec +4 -0
- data/.rubocop.yml +31 -0
- data/Gemfile +3 -0
- data/LICENSE +20 -0
- data/README.md +154 -0
- data/Rakefile +6 -0
- data/circle.yml +3 -0
- data/lib/stable-matching/logging_helper.rb +16 -0
- data/lib/stable-matching/marriage.rb +135 -0
- data/lib/stable-matching/marriage/phase_i_runner.rb +122 -0
- data/lib/stable-matching/marriage/preference_table.rb +64 -0
- data/lib/stable-matching/marriage/validator.rb +51 -0
- data/lib/stable-matching/member.rb +82 -0
- data/lib/stable-matching/phase_runner.rb +44 -0
- data/lib/stable-matching/preference_list.rb +18 -0
- data/lib/stable-matching/preference_table.rb +80 -0
- data/lib/stable-matching/roommate.rb +124 -0
- data/lib/stable-matching/roommate/phase_i_runner.rb +111 -0
- data/lib/stable-matching/roommate/phase_ii_runner.rb +105 -0
- data/lib/stable-matching/roommate/phase_iii_runner.rb +159 -0
- data/lib/stable-matching/roommate/preference_table.rb +20 -0
- data/lib/stable-matching/roommate/validator.rb +26 -0
- data/lib/stable-matching/stable_matching.rb +11 -0
- data/lib/stable-matching/validator.rb +93 -0
- data/lib/version.rb +3 -0
- data/stable-matching.gemspec +29 -0
- metadata +134 -0
@@ -0,0 +1,64 @@
|
|
1
|
+
# Provides a ruby implementation of several common matching algorithms
|
2
|
+
#
|
3
|
+
# Author:: Abhishek Chandrasekhar (mailto:me@abhchand.me)
|
4
|
+
# License:: MIT
|
5
|
+
|
6
|
+
require_relative "../preference_table"
|
7
|
+
|
8
|
+
class StableMatching
|
9
|
+
class Marriage
|
10
|
+
class PreferenceTable < StableMatching::PreferenceTable
|
11
|
+
attr_accessor :partner_table
|
12
|
+
|
13
|
+
def self.initialize_pair(raw_preference_table_a, raw_preference_table_b)
|
14
|
+
table_a = new(raw_preference_table_a)
|
15
|
+
table_b = new(raw_preference_table_b)
|
16
|
+
|
17
|
+
table_a.partner_table = table_b
|
18
|
+
table_b.partner_table = table_a
|
19
|
+
|
20
|
+
[table_a, table_b]
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize(raw_preference_table)
|
24
|
+
members = initialize_members_from(raw_preference_table)
|
25
|
+
|
26
|
+
@raw_preference_table = raw_preference_table
|
27
|
+
|
28
|
+
# Avoid calling the parent initializer, but we still need to set
|
29
|
+
# the delegated object. Thankfully SimpleDelegator offers a method
|
30
|
+
# to set this directly
|
31
|
+
__setobj__(members)
|
32
|
+
end
|
33
|
+
|
34
|
+
def partner_table=(partner_table)
|
35
|
+
@partner_table = partner_table
|
36
|
+
|
37
|
+
@raw_preference_table.each do |name, raw_preference_list|
|
38
|
+
generate_preference_list(
|
39
|
+
find_member_by_name(name),
|
40
|
+
raw_preference_list,
|
41
|
+
partner_table
|
42
|
+
)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def unmatched
|
47
|
+
have_accepted = partner_table.members.select(&:accepted_proposal?)
|
48
|
+
have_been_accepted = have_accepted.map(&:current_proposer)
|
49
|
+
|
50
|
+
members - have_been_accepted
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def generate_preference_list(member, raw_preference_list, partner_table)
|
56
|
+
member_list = raw_preference_list.map do |name|
|
57
|
+
partner_table.name_to_member_mapping[name]
|
58
|
+
end
|
59
|
+
|
60
|
+
member.preference_list = PreferenceList.new(member_list)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# Provides a ruby implementation of several common matching algorithms
|
2
|
+
#
|
3
|
+
# Author:: Abhishek Chandrasekhar (mailto:me@abhchand.me)
|
4
|
+
# License:: MIT
|
5
|
+
|
6
|
+
require_relative "../validator"
|
7
|
+
|
8
|
+
class StableMatching
|
9
|
+
class Marriage
|
10
|
+
class Validator < StableMatching::Validator
|
11
|
+
def self.validate_pair!(alpha_preferences, beta_preferences)
|
12
|
+
new(alpha_preferences, beta_preferences).validate!
|
13
|
+
new(beta_preferences, alpha_preferences).validate!
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(preference_table, partner_table)
|
17
|
+
@preference_table = preference_table
|
18
|
+
@partner_table = partner_table
|
19
|
+
end
|
20
|
+
|
21
|
+
def validate!
|
22
|
+
case
|
23
|
+
when !hash_of_arrays? then handle_not_hash_of_arrays
|
24
|
+
when empty? then handle_empty
|
25
|
+
when !strings_or_integers? then handle_not_strings_or_integers
|
26
|
+
when !symmetrical? then handle_not_symmetrical
|
27
|
+
end
|
28
|
+
|
29
|
+
raise ::StableMatching::InvalidPreferences, @error if @error
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def symmetrical?
|
35
|
+
@preference_table.each do |name, preference_list|
|
36
|
+
expected_members = @partner_table.keys - [name]
|
37
|
+
actual_members = preference_list
|
38
|
+
|
39
|
+
next if expected_members.sort == actual_members.sort
|
40
|
+
|
41
|
+
@name = name
|
42
|
+
@extra = actual_members - expected_members
|
43
|
+
@missing = expected_members - actual_members
|
44
|
+
return false
|
45
|
+
end
|
46
|
+
|
47
|
+
true
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# Provides a ruby implementation of several common matching algorithms
|
2
|
+
#
|
3
|
+
# Author:: Abhishek Chandrasekhar (mailto:me@abhchand.me)
|
4
|
+
# License:: MIT
|
5
|
+
|
6
|
+
class StableMatching
|
7
|
+
class Member
|
8
|
+
attr_reader :name, :received_proposals_from, :accepted_proposal_from
|
9
|
+
attr_writer :preference_list
|
10
|
+
|
11
|
+
def initialize(name)
|
12
|
+
@name = name
|
13
|
+
@accepted_proposal_from = nil
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_s
|
17
|
+
name
|
18
|
+
end
|
19
|
+
|
20
|
+
def preference_list
|
21
|
+
if @preference_list.nil?
|
22
|
+
raise "preference list not set for member: #{name}"
|
23
|
+
end
|
24
|
+
|
25
|
+
@preference_list
|
26
|
+
end
|
27
|
+
|
28
|
+
def current_proposer
|
29
|
+
@accepted_proposal_from
|
30
|
+
end
|
31
|
+
|
32
|
+
def current_acceptor
|
33
|
+
preference_list.detect do |member|
|
34
|
+
member.accepted_proposal_from == self
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def accepted_proposal?
|
39
|
+
!current_proposer.nil?
|
40
|
+
end
|
41
|
+
|
42
|
+
def would_prefer?(new_proposer)
|
43
|
+
return true unless accepted_proposal?
|
44
|
+
preference_of(new_proposer) > preference_of(current_proposer)
|
45
|
+
end
|
46
|
+
|
47
|
+
def accept_proposal_from!(member)
|
48
|
+
@accepted_proposal_from = member
|
49
|
+
end
|
50
|
+
|
51
|
+
def reject!(member)
|
52
|
+
@accepted_proposal_from = nil if current_proposer == member
|
53
|
+
|
54
|
+
# Delete each member from the other member's preference list
|
55
|
+
preference_list.delete(member)
|
56
|
+
member.preference_list.delete(self)
|
57
|
+
end
|
58
|
+
|
59
|
+
def first_preference
|
60
|
+
preference_list.first
|
61
|
+
end
|
62
|
+
|
63
|
+
def second_preference
|
64
|
+
preference_list[1]
|
65
|
+
end
|
66
|
+
|
67
|
+
def last_preference
|
68
|
+
preference_list.last
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def preference_of(member)
|
74
|
+
index = preference_list.index(member)
|
75
|
+
return if index.nil?
|
76
|
+
|
77
|
+
# Return the preference as the inverse of the index so a smaller index
|
78
|
+
# has a greater preference
|
79
|
+
preference_list.size - index
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# Provides a ruby implementation of several common matching algorithms
|
2
|
+
#
|
3
|
+
# Author:: Abhishek Chandrasekhar (mailto:me@abhchand.me)
|
4
|
+
# License:: MIT
|
5
|
+
|
6
|
+
class StableMatching
|
7
|
+
class PhaseRunner
|
8
|
+
private
|
9
|
+
|
10
|
+
def simulate_proposal(proposer, proposed)
|
11
|
+
@logger.debug("'#{proposer.name}' proposes to '#{proposed.name}'")
|
12
|
+
|
13
|
+
case
|
14
|
+
when !proposed.accepted_proposal?
|
15
|
+
accept(proposer, proposed)
|
16
|
+
when proposed.would_prefer?(proposer)
|
17
|
+
accept_better_proposal(proposer, proposed)
|
18
|
+
else
|
19
|
+
reject(proposer, proposed)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def accept(proposer, proposed)
|
24
|
+
@logger.debug("'#{proposed.name}' accepts '#{proposer.name}'")
|
25
|
+
|
26
|
+
proposed.accept_proposal_from!(proposer)
|
27
|
+
end
|
28
|
+
|
29
|
+
def accept_better_proposal(proposer, proposed)
|
30
|
+
@logger.debug(
|
31
|
+
"'#{proposed.name}' accepts '#{proposer.name}', "\
|
32
|
+
"rejects '#{proposed.current_proposer.name}'"
|
33
|
+
)
|
34
|
+
|
35
|
+
proposed.reject!(proposed.current_proposer)
|
36
|
+
proposed.accept_proposal_from!(proposer)
|
37
|
+
end
|
38
|
+
|
39
|
+
def reject(proposer, proposed)
|
40
|
+
@logger.debug("'#{proposed.name}' rejects '#{proposer.name}'")
|
41
|
+
proposed.reject!(proposer)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# Provides a ruby implementation of several common matching algorithms
|
2
|
+
#
|
3
|
+
# Author:: Abhishek Chandrasekhar (mailto:me@abhchand.me)
|
4
|
+
# License:: MIT
|
5
|
+
|
6
|
+
require "delegate"
|
7
|
+
|
8
|
+
class StableMatching
|
9
|
+
class PreferenceList < SimpleDelegator
|
10
|
+
def initialize(preference_list)
|
11
|
+
super(preference_list)
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_s
|
15
|
+
map(&:name).to_s
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# Provides a ruby implementation of several common matching algorithms
|
2
|
+
#
|
3
|
+
# Author:: Abhishek Chandrasekhar (mailto:me@abhchand.me)
|
4
|
+
# License:: MIT
|
5
|
+
|
6
|
+
require "delegate"
|
7
|
+
require "pp"
|
8
|
+
|
9
|
+
require_relative "member"
|
10
|
+
require_relative "preference_list"
|
11
|
+
|
12
|
+
class StableMatching
|
13
|
+
class PreferenceTable < SimpleDelegator
|
14
|
+
attr_reader :name_to_member_mapping
|
15
|
+
|
16
|
+
def initialize(raw_preference_table)
|
17
|
+
members = initialize_members_from(raw_preference_table)
|
18
|
+
|
19
|
+
raw_preference_table.each do |name, raw_preference_list|
|
20
|
+
generate_preference_list(
|
21
|
+
find_member_by_name(name),
|
22
|
+
raw_preference_list
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
super(members)
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_s
|
30
|
+
members.map { |m| "#{m.name} => #{m.preference_list}" }.join(", ")
|
31
|
+
end
|
32
|
+
|
33
|
+
def print
|
34
|
+
table = {}
|
35
|
+
members.each { |m| table[m.name] = m.preference_list.map(&:name) }
|
36
|
+
|
37
|
+
pp(table)
|
38
|
+
end
|
39
|
+
|
40
|
+
def unmatched
|
41
|
+
have_accepted = members.select(&:accepted_proposal?)
|
42
|
+
have_been_accepted = have_accepted.map(&:current_proposer)
|
43
|
+
|
44
|
+
members - have_been_accepted
|
45
|
+
end
|
46
|
+
|
47
|
+
def complete?
|
48
|
+
# Does every member have only one preference remaining?
|
49
|
+
counts = members.map { |member| member.preference_list.count }
|
50
|
+
counts.uniq == [1]
|
51
|
+
end
|
52
|
+
|
53
|
+
def members
|
54
|
+
__getobj__
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def find_member_by_name(name)
|
60
|
+
@name_to_member_mapping[name]
|
61
|
+
end
|
62
|
+
|
63
|
+
def initialize_members_from(raw_preference_table)
|
64
|
+
@name_to_member_mapping = {}
|
65
|
+
|
66
|
+
raw_preference_table.keys.each do |name|
|
67
|
+
@name_to_member_mapping[name] = Member.new(name)
|
68
|
+
end
|
69
|
+
|
70
|
+
@name_to_member_mapping.values
|
71
|
+
end
|
72
|
+
|
73
|
+
def generate_preference_list(member, raw_preference_list)
|
74
|
+
member_list =
|
75
|
+
raw_preference_list.map { |name| find_member_by_name(name) }
|
76
|
+
|
77
|
+
member.preference_list = PreferenceList.new(member_list)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# Provides a ruby implementation of several common matching algorithms
|
2
|
+
#
|
3
|
+
# Author:: Abhishek Chandrasekhar (mailto:me@abhchand.me)
|
4
|
+
# License:: MIT
|
5
|
+
|
6
|
+
require_relative "stable_matching"
|
7
|
+
require_relative "logging_helper"
|
8
|
+
|
9
|
+
require_relative "roommate/validator"
|
10
|
+
require_relative "roommate/preference_table"
|
11
|
+
require_relative "roommate/phase_i_runner"
|
12
|
+
require_relative "roommate/phase_ii_runner"
|
13
|
+
require_relative "roommate/phase_iii_runner"
|
14
|
+
|
15
|
+
class StableMatching
|
16
|
+
# Provides a solution to the Stable Roommate problem by implementing
|
17
|
+
# the Irving algorithm
|
18
|
+
#
|
19
|
+
# Takes as input the preferences of each member and produces a mathematically
|
20
|
+
# optimal matching/pairing between members.
|
21
|
+
#
|
22
|
+
# Example Input:
|
23
|
+
#
|
24
|
+
# preferences = {
|
25
|
+
# "A" => ["B", "D", "C"],
|
26
|
+
# "B" => ["D", "A", "C"],
|
27
|
+
# "C" => ["D", "A", "B"],
|
28
|
+
# "D" => ["C", "A", "B"]
|
29
|
+
# }
|
30
|
+
#
|
31
|
+
# Example Output:
|
32
|
+
#
|
33
|
+
# {"A"=>"B", "B"=>"A", "C"=>"D", "D"=>"C"}
|
34
|
+
class Roommate
|
35
|
+
include StableMatching::LoggingHelper
|
36
|
+
|
37
|
+
# Runs the algorithm with the specified inputs.
|
38
|
+
#
|
39
|
+
# This is a class level shortcut to initialize a new
|
40
|
+
# +StableMatching::Roommate+ instance and calls +solve!+ on it.
|
41
|
+
#
|
42
|
+
# <b>Inputs:</b>
|
43
|
+
#
|
44
|
+
# <tt>preference_table</tt>::
|
45
|
+
# A +Hash+ of +Array+ values specifying the preferences of the group
|
46
|
+
#
|
47
|
+
# <b>Output:</b>
|
48
|
+
# A +Hash+ mapping members to other members.
|
49
|
+
def self.solve!(preference_table)
|
50
|
+
new(preference_table).solve!
|
51
|
+
end
|
52
|
+
|
53
|
+
# Initializes a `StableMatching::Roommate` object.
|
54
|
+
#
|
55
|
+
#
|
56
|
+
# <b>Inputs:</b>
|
57
|
+
#
|
58
|
+
# <tt>preference_table</tt>::
|
59
|
+
# A +Hash+ of +Array+ values specifying the preferences of the group.
|
60
|
+
# +Array+ can contain +String+ or +Integer+ entries.
|
61
|
+
# <tt>opts[:logger]</tt>::
|
62
|
+
# +Logger+ instance to use for logging
|
63
|
+
#
|
64
|
+
# <b>Output:</b>
|
65
|
+
#
|
66
|
+
# +StableMatching::Roommate+ instance
|
67
|
+
def initialize(preference_table, opts = {})
|
68
|
+
@orig_preference_table = preference_table
|
69
|
+
set_logger(opts)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Runs the algorithm on the preference_table.
|
73
|
+
# Also validates the preference_table and raises an error if invalid.
|
74
|
+
#
|
75
|
+
# The roommate algorithm is not guranteed to find a solution in all cases
|
76
|
+
# and will raise an error if a solution is mathematically unstable (does
|
77
|
+
# not exist).
|
78
|
+
#
|
79
|
+
# <b>Output:</b>
|
80
|
+
#
|
81
|
+
# A +Hash+ mapping members to other members.
|
82
|
+
#
|
83
|
+
# <b>Raises:</b>
|
84
|
+
#
|
85
|
+
# <tt>StableMatching::InvalidPreferences</tt>::
|
86
|
+
# When preference_table is of invalid format
|
87
|
+
# <tt>StableMatching::NoStableSolutionError</tt>::
|
88
|
+
# When no mathematically stable solution can be found
|
89
|
+
def solve!
|
90
|
+
validate!
|
91
|
+
|
92
|
+
@logger.debug("Running Phase I")
|
93
|
+
PhaseIRunner.new(preference_table, logger: @logger).run
|
94
|
+
|
95
|
+
@logger.debug("Running Phase II")
|
96
|
+
PhaseIIRunner.new(preference_table, logger: @logger).run
|
97
|
+
|
98
|
+
@logger.debug("Running Phase III")
|
99
|
+
PhaseIIIRunner.new(preference_table, logger: @logger).run
|
100
|
+
|
101
|
+
build_solution
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
def validate!
|
107
|
+
Validator.validate!(@orig_preference_table)
|
108
|
+
end
|
109
|
+
|
110
|
+
def preference_table
|
111
|
+
@preference_table ||= PreferenceTable.new(@orig_preference_table)
|
112
|
+
end
|
113
|
+
|
114
|
+
def build_solution
|
115
|
+
solution = {}
|
116
|
+
|
117
|
+
preference_table.members.each do |member|
|
118
|
+
solution[member.name] = member.first_preference.name
|
119
|
+
end
|
120
|
+
|
121
|
+
solution
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|