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