stable-matching 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|