stable-matching 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 9f33d15fb7603874a410eac7e811c303aef3dec6
4
+ data.tar.gz: 32087ed764d51a24b8c791b8d68b933590388088
5
+ SHA512:
6
+ metadata.gz: 5ec63ba206e500f249d7b69c4cde8d7938e3a0447332c8533b67d181760e9b03189d33bc70033c5887aa124fe3476af2c064e9e361d717591497a309fcc7edf0
7
+ data.tar.gz: f7b2ce792d35b420fad9bc2d8fc39b48a1db369bb0bb30763250944245dd5f75ad12b2f133ff6257a8c0478270333b3746633ea8c3464413d9c3a95ef9c7078a
data/.gitignore ADDED
@@ -0,0 +1,15 @@
1
+ .DS_Store
2
+
3
+ /.bundle/
4
+ /.yardoc
5
+ /Gemfile.lock
6
+ /_yardoc/
7
+ /coverage/
8
+ /doc/
9
+ /pkg/
10
+ /spec/reports/
11
+ /tmp/
12
+ /spec/examples.txt
13
+
14
+ # Custom
15
+ TODO
data/.gitlab-ci.yml ADDED
@@ -0,0 +1,28 @@
1
+ variables:
2
+ RUNNING_ON_CI_SERVER: 1
3
+
4
+ cache:
5
+ key: "$CI_BUILD_REF_NAME"
6
+ paths:
7
+ - /cache
8
+
9
+ before_script:
10
+ # System
11
+ - apt-get update -qq
12
+
13
+ # Ruby
14
+ - ruby -v
15
+ - which ruby
16
+
17
+ # Ruby Gems
18
+ - 'echo ''gem: --no-ri --no-rdoc'' > ~/.gemrc'
19
+ - gem install bundler
20
+ - bundle install --path=/cache --without production --jobs $(nproc) "${FLAGS[@]}"
21
+
22
+ rspec:
23
+ script:
24
+ - bundle exec rspec --color --require spec_helper --format progress --no-profile
25
+
26
+ rubocop:
27
+ script:
28
+ - bundle exec rubocop --display-cop-names
data/.rspec ADDED
@@ -0,0 +1,4 @@
1
+ --color
2
+ --require spec_helper
3
+ --format progress
4
+ --no-profile
data/.rubocop.yml ADDED
@@ -0,0 +1,31 @@
1
+ AllCops:
2
+ Exclude:
3
+ - "stable-matching.gemspec"
4
+ - "bin/*"
5
+ Metrics/AbcSize:
6
+ Enabled: false
7
+ Layout/DotPosition:
8
+ EnforcedStyle: leading
9
+ Layout/MultilineOperationIndentation:
10
+ Enabled: true
11
+ EnforcedStyle: indented
12
+ Style/BlockComments:
13
+ Enabled: false
14
+ Style/Documentation:
15
+ Enabled: false
16
+ Style/EmptyCaseCondition:
17
+ Enabled: false
18
+ Style/StringLiterals:
19
+ EnforcedStyle: double_quotes
20
+ Metrics/BlockLength:
21
+ ExcludedMethods:
22
+ - context
23
+ - describe
24
+ - it
25
+ - feature
26
+ - shared_examples
27
+ - shared_examples_for
28
+ - namespace
29
+ - task
30
+ - proc
31
+ - draw
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2018 Abhishek Chandrasekhar
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,154 @@
1
+
2
+
3
+ # Stable Matching
4
+
5
+ ![Stable Matching](https://gitlab.com/abhchand/stable-matching/raw/master/meta/logo.png)
6
+
7
+ [![Build Status](https://gitlab.com/abhchand/stable-matching/badges/master/build.svg)](https://gitlab.com/abhchand/stable-matching/pipelines)
8
+
9
+ A ruby implementation of various stable matching algorithms.
10
+
11
+ # Background
12
+
13
+ This gem provides ruby implementations of algorithms that solve the following matching problems:
14
+
15
+ - [Stable Roommates Problem](https://en.wikipedia.org/wiki/Stable_roommates_problem)
16
+ - [Stable Marriage Problem](https://en.wikipedia.org/wiki/Stable_marriage_problem)
17
+
18
+ # Install
19
+
20
+ In your Gemfile:
21
+
22
+ ```
23
+ source "https://rubygems.org"
24
+
25
+ ...
26
+
27
+ gem "stable-matching"
28
+ ```
29
+
30
+ Then run:
31
+
32
+ ```
33
+ bundle install
34
+ ```
35
+
36
+ # Quick Start
37
+
38
+ ## Stable Roommates
39
+
40
+ See or run `bin/stable-roommates-example` for an example usage.
41
+
42
+ Specify an input of ordered preferences as a hash of arrays. Keys may be `String` or `Integer` and preference table must include an even number of members.
43
+
44
+ ``` ruby
45
+ preference_table = {
46
+ 1 => [3, 4, 2, 6, 5],
47
+ 2 => [6, 5, 4, 1, 3],
48
+ 3 => [2, 4, 5, 1, 6],
49
+ 4 => [5, 2, 3, 6, 1],
50
+ 5 => [3, 1, 2, 4, 6],
51
+ 6 => [5, 1, 3, 4, 2]
52
+ }
53
+
54
+ StableRoommate.solve!(preference_table)
55
+ #=> {1=>6, 2=>4, 3=>5, 4=>2, 5=>3, 6=>1}
56
+ ```
57
+
58
+ The implementation of this algorithm is *not* guranteed to return a mathematically stable solution and may raise an error if no solution is found (see Errors below).
59
+
60
+ ## Stable Marriage
61
+
62
+ See or run `bin/stable-marriage-example` for an example usage
63
+
64
+ Specify an input of ordered preferences as a hash of arrays for two groups. Keys may be `String` or `Integer`
65
+
66
+ ```
67
+ alpha_preferences = {
68
+ "A" => ["O", "M", "N", "L", "P"],
69
+ "B" => ["P", "N", "M", "L", "O"],
70
+ "C" => ["M", "P", "L", "O", "N"],
71
+ "D" => ["P", "M", "O", "N", "L"],
72
+ "E" => ["O", "L", "M", "N", "P"],
73
+ }
74
+
75
+ beta_preferences = {
76
+ "L" => ["D", "B", "E", "C", "A"],
77
+ "M" => ["B", "A", "D", "C", "E"],
78
+ "N" => ["A", "C", "E", "D", "B"],
79
+ "O" => ["D", "A", "C", "B", "E"],
80
+ "P" => ["B", "E", "A", "C", "D"],
81
+ }
82
+
83
+ puts StableMarriage.solve!(alpha_preferences, beta_preferences,)
84
+ #=> {"A"=>"O", "B"=>"P", "C"=>"N", "D"=>"M", "E"=>"L", "L"=>"E", "M"=>"D", "N"=>"C", "O"=>"A", "P"=>"B"}
85
+ ```
86
+
87
+ The implementation of this algorithm is always guranteed to return a mathematically stable solution.
88
+
89
+ # Errors
90
+
91
+ Your process should be prepared to handle the following errors when calling the stable matching library
92
+
93
+ ```
94
+ StableMatching::Error
95
+ |- StableMatching::NoStableSolutionError
96
+ |- StableMatching::InvalidPreferences
97
+ ```
98
+
99
+ # Logging
100
+
101
+ You may optionally pass a logger that will output the progress of the algorithm.
102
+
103
+ To utilize this option you'll have to instantiate an algorithm object yourself with a `:logger` option and then call `#solve!`.
104
+
105
+ ``` ruby
106
+ logger = Logger.new(STDOUT)
107
+ logger.level = Logger::DEBUG
108
+
109
+ StableRoommate.new(preference_table, logger: logger).solve!
110
+ ```
111
+
112
+ # Benchmark / Performance
113
+
114
+ Below are some benchmarks for runtimes captured on a machine running OS X 10.11.5 (El Capitan) / 2.5 GHz Intel Core i7.
115
+
116
+ You can run `bin/benchmark` on any machine to regenerate these benchmarks
117
+
118
+ Note: Many combinatorics algorithms run in quadratic time (`O(n^2)`) and therfore performance degrades significantly when processing a large number of members.
119
+
120
+ ### Stable Roommates
121
+
122
+ ```
123
+ N | Avg Runtime (sec)
124
+ -----|------------------
125
+ 10 | 0.103
126
+ 100 | 1.075
127
+ 250 | 17.372
128
+ ```
129
+
130
+ ### Stable Marriage
131
+
132
+ ```
133
+ N | Avg Runtime (sec)
134
+ ------|------------------
135
+ 10 | 0.004
136
+ 100 | 0.053
137
+ 1000 | 0.334
138
+ ```
139
+
140
+ # Issues
141
+
142
+ Feel free to submit issues and enhancement requests.
143
+
144
+ # Contributing
145
+
146
+ All contributions to this project are welcome.
147
+
148
+ Code changes follow the "fork-and-pull" Git workflow.
149
+
150
+ 1. **Fork** the repository
151
+ 2. **Clone** the project to your own machine
152
+ 3. **Commit** changes to your own branch
153
+ 4. **Push** your work back up to your fork
154
+ 5. Submit a **Pull request** so that your changes can be reviewed!
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
data/circle.yml ADDED
@@ -0,0 +1,3 @@
1
+ dependencies:
2
+ pre:
3
+ - gem install bundler --pre
@@ -0,0 +1,16 @@
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 "logger"
7
+
8
+ class StableMatching
9
+ module LoggingHelper
10
+ # rubocop:disable Style/AccessorMethodName
11
+ def set_logger(opts = {})
12
+ @logger = opts.key?(:logger) ? opts[:logger] : Logger.new(nil)
13
+ end
14
+ # rubocop:enable Style/AccessorMethodName
15
+ end
16
+ end
@@ -0,0 +1,135 @@
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 "marriage/validator"
10
+ require_relative "marriage/preference_table"
11
+ require_relative "marriage/phase_i_runner"
12
+
13
+ class StableMatching
14
+ # Provides a solution to the Stable Marriage problem by implementing
15
+ # the Gale-Shapley algorithm
16
+ #
17
+ # Takes as input the preferences of two groups - alpha and beta - and
18
+ # produces a mathematically optimal matching/pairing between the two groups.
19
+ #
20
+ # Example Input:
21
+ #
22
+ # alpha_preferences = {
23
+ # "A" => ["M", "N", "L"],
24
+ # "B" => ["N", "M", "L"],
25
+ # "C" => ["M", "L", "N"],
26
+ # }
27
+ #
28
+ # beta_preferences = {
29
+ # "L" => ["B", "C", "A"],
30
+ # "M" => ["B", "A", "C"],
31
+ # "N" => ["A", "C", "B"],
32
+ # }
33
+ #
34
+ # Example Output:
35
+ #
36
+ # {"A"=>"M", "B"=>"N", "C"=>"L", "L"=>"C", "M"=>"A", "N"=>"B"}
37
+ class Marriage
38
+ include StableMatching::LoggingHelper
39
+
40
+ # Runs the algorithm with the specified inputs.
41
+ #
42
+ # This is a class level shortcut to initialize a new
43
+ # +StableMatching::Marriage+ instance and calls +solve!+ on it.
44
+ #
45
+ # <b>Inputs:</b>
46
+ #
47
+ # <tt>alpha_preferences</tt>::
48
+ # A +Hash+ of +Array+ values specifying the preferences of the alpha
49
+ # group
50
+ # <tt>beta_preferences</tt>::
51
+ # A +Hash+ of +Array+ values specifying the preferences of the beta
52
+ # group
53
+ #
54
+ # <b>Output:</b>
55
+ # A +Hash+ mapping alpha members to beta and beta members to alpha members.
56
+ def self.solve!(alpha_preferences, beta_preferences)
57
+ new(alpha_preferences, beta_preferences).solve!
58
+ end
59
+
60
+ # Initializes a `StableMatching::Marriage` object.
61
+ #
62
+ #
63
+ # <b>Inputs:</b>
64
+ #
65
+ # <tt>alpha_preferences</tt>::
66
+ # A +Hash+ of +Array+ values specifying the preferences of the alpha
67
+ # group. +Array+ can contain +String+ or +Integer+ entries.
68
+ # <tt>beta_preferences</tt>::
69
+ # A +Hash+ of +Array+ values specifying the preferences of the beta
70
+ # group. +Array+ can contain +String+ or +Integer+ entries.
71
+ # <tt>opts[:logger]</tt>::
72
+ # +Logger+ instance to use for logging
73
+ #
74
+ # <b>Output:</b>
75
+ #
76
+ # +StableMatching::Marriage+ instance
77
+ def initialize(alpha_preferences, beta_preferences, opts = {})
78
+ @orig_alpha_preferences = alpha_preferences
79
+ @orig_beta_preferences = beta_preferences
80
+
81
+ @alpha_preferences, @beta_preferences =
82
+ PreferenceTable.initialize_pair(alpha_preferences, beta_preferences)
83
+
84
+ set_logger(opts)
85
+ end
86
+
87
+ # Runs the algorithm on the alpha and beta preference sets.
88
+ # Also validates the preference sets and raises an error if invalid.
89
+ #
90
+ # <b>Output:</b>
91
+ #
92
+ # A +Hash+ mapping alpha members to beta and beta members to alpha members.
93
+ #
94
+ # <b>Raises:</b>
95
+ #
96
+ # <tt>StableMatching::InvalidPreferences</tt>::
97
+ # When alpha or beta preference groups are of invalid format
98
+ def solve!
99
+ validate!
100
+
101
+ @logger.info("Running Phase I")
102
+ PhaseIRunner.new(alpha_preferences, beta_preferences, logger: @logger).run
103
+
104
+ build_solution
105
+ end
106
+
107
+ private
108
+
109
+ def validate!
110
+ Validator.validate_pair!(@orig_alpha_preferences, @orig_beta_preferences)
111
+ end
112
+
113
+ def alpha_preferences
114
+ @alpha_preferences ||= PreferenceTable.new(@orig_alpha_preferences)
115
+ end
116
+
117
+ def beta_preferences
118
+ @beta_preferences ||= PreferenceTable.new(@orig_beta_preferences)
119
+ end
120
+
121
+ def build_solution
122
+ solution = {}
123
+
124
+ @alpha_preferences.members.each do |partner|
125
+ solution[partner.name] = partner.current_acceptor.name
126
+ end
127
+
128
+ @beta_preferences.members.each do |partner|
129
+ solution[partner.name] = partner.current_proposer.name
130
+ end
131
+
132
+ solution
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,122 @@
1
+ # Provides a ruby implementation of several common matching algorithms
2
+ #
3
+ # Author:: Abhishek Chandrasekhar (mailto:me@abhchand.me)
4
+ # License:: MIT
5
+
6
+ # Implements the Gale-Shapley (1962) algorithm.
7
+ #
8
+ # Calculates a stable match between two groups - alpha and beta - given their
9
+ # individual preference lists for members of the other group
10
+ #
11
+ # See:
12
+ # https://en.wikipedia.org/wiki/Stable_marriage_problem#Solution
13
+ # https://www.youtube.com/watch?v=GsBf3fJFpSw
14
+ #
15
+ # Each member who has not had their proposal accepted "proposes" to their top
16
+ # remaining preference
17
+ #
18
+ # Each recipient of a proposal can take one of 3 actions -
19
+ #
20
+ # 1. The recipient has not received a previous proposal and immediately accepts
21
+ #
22
+ # 2. The recipient prefers this new proposal over an existing one.
23
+ # The recipient "rejects" it's initial proposl and accepts this new one
24
+ #
25
+ # 3. The recipient prefers the existing proposal over the new one.
26
+ # The recipient "rejects" the new proposal
27
+ #
28
+ # Note: Rejections are mutual. If `i` removes `j` from their preference list,
29
+ # then `j` must also remove `i` from its list
30
+ #
31
+ # This cycle continues until every alpha/beta has a match.
32
+ #
33
+ # Mathematically, every participant is guranteed a match so this algorithm
34
+ # always converges on a solution.
35
+ #
36
+ #
37
+ # === EXAMPLE
38
+ #
39
+ # Take the following preference lists
40
+ #
41
+ # alpha preferences:
42
+ # A => [O, M, N, L, P]
43
+ # B => [P, N, M, L, O]
44
+ # C => [M, P, L, O, N]
45
+ # D => [P, M, O, N, L]
46
+ # E => [O, L, M, N, P]
47
+ #
48
+ # beta preferences:
49
+ # L => [D, B, E, C, A]
50
+ # M => [B, A, D, C, E]
51
+ # N => [A, C, E, D, B]
52
+ # O => [D, A, C, B, E]
53
+ # P => [B, E, A, C, D]
54
+ #
55
+ # We always start with the first unmatched user. Initially this is "A" (We only
56
+ # cycle through alphas since they propose to the betas).
57
+ # The sequence of events are -
58
+ #
59
+ # 'A' proposes to 'O'
60
+ # 'O' accepts 'A'
61
+ # 'B' proposes to 'P'
62
+ # 'P' accepts 'B'
63
+ # 'C' proposes to 'M'
64
+ # 'M' accepts 'C'
65
+ # 'D' proposes to 'P'
66
+ # 'P' rejects 'D'
67
+ # 'E' proposes to 'O'
68
+ # 'O' rejects 'E'
69
+ # 'D' proposes to 'M'
70
+ # 'M' accepts 'D', rejects 'C'
71
+ # 'E' proposes to 'L'
72
+ # 'L' accepts 'E'
73
+ # 'C' proposes to 'P'
74
+ # 'P' rejects 'C'
75
+ # 'C' proposes to 'L'
76
+ # 'L' rejects 'C'
77
+ # 'C' proposes to 'O'
78
+ # 'O' rejects 'C'
79
+ # 'C' proposes to 'N'
80
+ # 'N' accepts 'C'
81
+ #
82
+ # At this point there are no alpha users left unmatched (and by definition, no
83
+ # corresponding beta users left unmatched). All alpha members have had their
84
+ # proposals accepted by a beta user.
85
+ #
86
+ # The resulting solution is
87
+ #
88
+ # A => O
89
+ # B => P
90
+ # C => N
91
+ # D => M
92
+ # E => L
93
+ # L => E
94
+ # M => D
95
+ # N => C
96
+ # O => A
97
+ # P => B
98
+ #
99
+
100
+ require_relative "../phase_runner"
101
+
102
+ class StableMatching
103
+ class Marriage
104
+ class PhaseIRunner < StableMatching::PhaseRunner
105
+ def initialize(alpha_preferences, beta_preferences, opts = {})
106
+ @alpha_preferences = alpha_preferences
107
+ @beta_preferences = beta_preferences
108
+
109
+ @logger = opts.fetch(:logger)
110
+ end
111
+
112
+ def run
113
+ while @alpha_preferences.unmatched.any?
114
+ @alpha_preferences.unmatched.each do |partner|
115
+ top_choice = partner.first_preference
116
+ simulate_proposal(partner, top_choice)
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end