stable-matching 0.1.0

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