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.
@@ -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