graph_matching 0.0.1

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.
Files changed (94) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +20 -0
  3. data/.rubocop.yml +112 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +9 -0
  6. data/Gemfile +4 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +205 -0
  9. data/Rakefile +9 -0
  10. data/benchmark/mcm_bipartite/complete_bigraphs/benchmark.rb +33 -0
  11. data/benchmark/mcm_bipartite/complete_bigraphs/compare.gnuplot +19 -0
  12. data/benchmark/mcm_bipartite/complete_bigraphs/edges_times_vertexes.data +500 -0
  13. data/benchmark/mcm_bipartite/complete_bigraphs/plot.gnuplot +21 -0
  14. data/benchmark/mcm_bipartite/complete_bigraphs/plot.png +0 -0
  15. data/benchmark/mcm_bipartite/complete_bigraphs/time.data +499 -0
  16. data/benchmark/mcm_general/complete_graphs/benchmark.rb +30 -0
  17. data/benchmark/mcm_general/complete_graphs/plot.gnuplot +19 -0
  18. data/benchmark/mcm_general/complete_graphs/plot.png +0 -0
  19. data/benchmark/mcm_general/complete_graphs/time.data +499 -0
  20. data/benchmark/mcm_general/complete_graphs/v_cubed.data +500 -0
  21. data/benchmark/mwm_bipartite/complete_bigraphs/benchmark.rb +43 -0
  22. data/benchmark/mwm_bipartite/complete_bigraphs/nmN.data +499 -0
  23. data/benchmark/mwm_bipartite/complete_bigraphs/nmN.xlsx +0 -0
  24. data/benchmark/mwm_bipartite/complete_bigraphs/plot.gnuplot +22 -0
  25. data/benchmark/mwm_bipartite/complete_bigraphs/plot.png +0 -0
  26. data/benchmark/mwm_bipartite/complete_bigraphs/time.data +299 -0
  27. data/benchmark/mwm_bipartite/misc/calc_d2/benchmark.rb +29 -0
  28. data/benchmark/mwm_general/complete_graphs/benchmark.rb +32 -0
  29. data/benchmark/mwm_general/complete_graphs/compare.gnuplot +19 -0
  30. data/benchmark/mwm_general/complete_graphs/mn_log_n.data +299 -0
  31. data/benchmark/mwm_general/complete_graphs/mn_log_n.xlsx +0 -0
  32. data/benchmark/mwm_general/complete_graphs/plot.gnuplot +22 -0
  33. data/benchmark/mwm_general/complete_graphs/plot.png +0 -0
  34. data/benchmark/mwm_general/complete_graphs/time.data +299 -0
  35. data/benchmark/mwm_general/incomplete_graphs/benchmark.rb +39 -0
  36. data/benchmark/mwm_general/incomplete_graphs/plot.gnuplot +22 -0
  37. data/benchmark/mwm_general/incomplete_graphs/plot.png +0 -0
  38. data/benchmark/mwm_general/incomplete_graphs/time_10_pct.data +299 -0
  39. data/benchmark/mwm_general/incomplete_graphs/time_20_pct.data +299 -0
  40. data/benchmark/mwm_general/incomplete_graphs/time_30_pct.data +299 -0
  41. data/graph_matching.gemspec +35 -0
  42. data/lib/graph_matching.rb +15 -0
  43. data/lib/graph_matching/algorithm/matching_algorithm.rb +23 -0
  44. data/lib/graph_matching/algorithm/mcm_bipartite.rb +118 -0
  45. data/lib/graph_matching/algorithm/mcm_general.rb +289 -0
  46. data/lib/graph_matching/algorithm/mwm_bipartite.rb +147 -0
  47. data/lib/graph_matching/algorithm/mwm_general.rb +1086 -0
  48. data/lib/graph_matching/algorithm/mwmg_delta_assertions.rb +94 -0
  49. data/lib/graph_matching/assertion.rb +41 -0
  50. data/lib/graph_matching/core_ext/set.rb +36 -0
  51. data/lib/graph_matching/directed_edge_set.rb +31 -0
  52. data/lib/graph_matching/errors.rb +23 -0
  53. data/lib/graph_matching/graph/bigraph.rb +37 -0
  54. data/lib/graph_matching/graph/graph.rb +63 -0
  55. data/lib/graph_matching/graph/weighted.rb +112 -0
  56. data/lib/graph_matching/graph/weighted_bigraph.rb +17 -0
  57. data/lib/graph_matching/graph/weighted_graph.rb +17 -0
  58. data/lib/graph_matching/integer_vertexes.rb +29 -0
  59. data/lib/graph_matching/matching.rb +120 -0
  60. data/lib/graph_matching/ordered_set.rb +59 -0
  61. data/lib/graph_matching/version.rb +6 -0
  62. data/lib/graph_matching/visualize.rb +93 -0
  63. data/profile/mcm_bipartite/compare.sh +15 -0
  64. data/profile/mcm_bipartite/publish.sh +12 -0
  65. data/profile/mwm_general/compare.sh +15 -0
  66. data/profile/mwm_general/profile.rb +28 -0
  67. data/profile/mwm_general/publish.sh +12 -0
  68. data/research/1965_edmonds.pdf +0 -0
  69. data/research/1975_even_kariv.pdf +0 -0
  70. data/research/1976_gabow.pdf +0 -0
  71. data/research/1980_micali_vazirani.pdf +0 -0
  72. data/research/1985_gabow.pdf +0 -0
  73. data/research/2002_tarjan.pdf +0 -0
  74. data/research/2013_zwick.pdf +0 -0
  75. data/research/examples/unweighted_general/1.txt +86 -0
  76. data/research/goodwin.pdf +0 -0
  77. data/research/kavathekar-scribe.pdf +0 -0
  78. data/research/kusner.pdf +0 -0
  79. data/research/van_rantwijk/mwm_example.py +19 -0
  80. data/research/van_rantwijk/mwmatching.py +945 -0
  81. data/spec/graph_matching/algorithm/matching_algorithm_spec.rb +14 -0
  82. data/spec/graph_matching/algorithm/mcm_bipartite_spec.rb +98 -0
  83. data/spec/graph_matching/algorithm/mcm_general_spec.rb +159 -0
  84. data/spec/graph_matching/algorithm/mwm_bipartite_spec.rb +82 -0
  85. data/spec/graph_matching/algorithm/mwm_general_spec.rb +439 -0
  86. data/spec/graph_matching/graph/bigraph_spec.rb +73 -0
  87. data/spec/graph_matching/graph/graph_spec.rb +53 -0
  88. data/spec/graph_matching/graph/weighted_spec.rb +29 -0
  89. data/spec/graph_matching/integer_vertexes_spec.rb +21 -0
  90. data/spec/graph_matching/matching_spec.rb +89 -0
  91. data/spec/graph_matching/visualize_spec.rb +38 -0
  92. data/spec/graph_matching_spec.rb +9 -0
  93. data/spec/spec_helper.rb +26 -0
  94. metadata +263 -0
@@ -0,0 +1,94 @@
1
+ module GraphMatching
2
+ module Algorithm
3
+ # Can be mixed into MWMGeneral to add runtime assertions
4
+ # about the data structures used for delta2/delta3 calculations.
5
+ #
6
+ # > Check delta2/delta3 computation after every substage;
7
+ # > only works on integer weights, slows down the algorithm to O(n^4).
8
+ # > (Van Rantwijk, mwmatching.py, line 34)
9
+ #
10
+ module MWMGDeltaAssertions
11
+ def calc_delta_with_assertions(*args)
12
+ # > Verify data structures for delta2/delta3 computation.
13
+ # > (Van Rantwijk, mwmatching.py, line 739)
14
+ check_delta2
15
+ check_delta3
16
+ calc_delta_without_assertions(*args)
17
+ end
18
+
19
+ # > Check optimized delta2 against a trivial computation.
20
+ # > (Van Rantwijk, mwmatching.py, line 580)
21
+ def check_delta2
22
+ (0 ... @nvertex).each do |v|
23
+ if @label[@in_blossom[v]] == MWMGeneral::LBL_FREE
24
+ bd = nil
25
+ bk = nil
26
+ @neighb_end[v].each do |p|
27
+ k = p / 2 # Note: floor division
28
+ w = @endpoint[p]
29
+ if @label[@in_blossom[w]] == MWMGeneral::LBL_S
30
+ d = slack(k)
31
+ if bk.nil? || d < bd
32
+ bk = k
33
+ bd = d
34
+ end
35
+ end
36
+ end
37
+ option1 = bk.nil? && @best_edge[v].nil?
38
+ option2 = !@best_edge[v].nil? && bd == slack(@best_edge[v])
39
+ unless option1 || option2
40
+ fail "Assertion failed: Free vertex #{v}"
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ # > Check optimized delta3 against a trivial computation.
47
+ # > (Van Rantwijk, mwmatching.py, line 598)
48
+ def check_delta3
49
+ bk = nil
50
+ bd = nil
51
+ tbk = nil
52
+ tbd = nil
53
+ (0 ... 2 * @nvertex).each do |b|
54
+ if @blossom_parent[b].nil? && @label[b] == MWMGeneral::LBL_S
55
+ blossom_leaves(b).each do |v|
56
+ @neighb_end[v].each do |p|
57
+ k = p / 2 # Note: floor division
58
+ w = @endpoint[p]
59
+ if @in_blossom[w] != b &&
60
+ @label[@in_blossom[w]] == MWMGeneral::LBL_S
61
+ d = slack(k)
62
+ if bk.nil? || d < bd
63
+ bk = k
64
+ bd = d
65
+ end
66
+ end
67
+ end
68
+ end
69
+ unless @best_edge[b].nil?
70
+ i, j = @edges[@best_edge[b]].to_a
71
+ unless @in_blossom[i] == b || @in_blossom[j] == b
72
+ fail 'Assertion failed'
73
+ end
74
+ unless @in_blossom[i] != b || @in_blossom[j] != b
75
+ fail 'Assertion failed'
76
+ end
77
+ unless @label[@in_blossom[i]] == MWMGeneral::LBL_S &&
78
+ @label[@in_blossom[j]] == MWMGeneral::LBL_S
79
+ fail 'Assertion failed'
80
+ end
81
+ if tbk.nil? || slack(@best_edge[b]) < tbd
82
+ tbk = @best_edge[b]
83
+ tbd = slack(@best_edge[b])
84
+ end
85
+ end
86
+ end
87
+ end
88
+ unless bd == tbd
89
+ fail 'Assertion failed'
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,41 @@
1
+ # encoding: utf-8
2
+
3
+ module GraphMatching
4
+ # Provides expressive methods for common runtime assertions, e.g.
5
+ #
6
+ # assert(banana).is_a(Fruit)
7
+ #
8
+ class Assertion
9
+ attr_reader :obj
10
+
11
+ def initialize(obj)
12
+ @obj = obj
13
+ end
14
+
15
+ def eq(other)
16
+ unless obj == other
17
+ fail "Expected #{other}, got #{obj}"
18
+ end
19
+ end
20
+
21
+ def gte(other)
22
+ unless obj >= other
23
+ fail "Expected #{obj} to be >= #{other}"
24
+ end
25
+ end
26
+
27
+ # rubocop:disable Style/PredicateName
28
+ def is_a(klass)
29
+ unless obj.is_a?(klass)
30
+ fail TypeError, "Expected #{klass}, got #{obj.class}"
31
+ end
32
+ end
33
+ # rubocop:enable Style/PredicateName
34
+
35
+ def not_nil
36
+ if obj.nil?
37
+ fail "Unexpected nil"
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,36 @@
1
+ # encoding: utf-8
2
+
3
+ require 'set'
4
+
5
+ # There are some methods we'd like to use which were not added
6
+ # until ruby 2.1. Fortunately, they are implemented in ruby,
7
+ # so we can simply copy them. If we ever drop support for ruby 2.0,
8
+ # this file can be deleted.
9
+
10
+ unless Set.instance_methods.include?(:intersect?)
11
+
12
+ # no-doc
13
+ class Set
14
+ # Returns true if the set and the given set have at least one
15
+ # element in common.
16
+ # http://www.ruby-doc.org/stdlib-2.2.0/libdoc/set/rdoc/Set.html#method-i-intersect-3F
17
+ def intersect?(set)
18
+ unless set.is_a?(Set)
19
+ fail ArgumentError, "value must be a set"
20
+ end
21
+ if size < set.size
22
+ any? { |o| set.include?(o) }
23
+ else
24
+ set.any? { |o| include?(o) }
25
+ end
26
+ end
27
+
28
+ # Returns true if the set and the given set have no element in
29
+ # common. This method is the opposite of intersect?.
30
+ # http://www.ruby-doc.org/stdlib-2.2.0/libdoc/set/rdoc/Set.html#method-i-disjoint-3F
31
+ def disjoint?(set)
32
+ !intersect?(set)
33
+ end
34
+ end
35
+
36
+ end
@@ -0,0 +1,31 @@
1
+ # encoding: utf-8
2
+
3
+ module GraphMatching
4
+ # A `DirectedEdgeSet` is simply a set of directed edges in a
5
+ # graph. Whether the graph is actually directed or not is
6
+ # irrelevant, we can still discuss directed edges in an undirected
7
+ # graph.
8
+ #
9
+ # The naive implementation would be to use ruby's `Set` and RGL's
10
+ # `DirectedEdge`. This class is optimized to use a 2D array
11
+ # instead. The sub-array at index i represents a set (or subset)
12
+ # of vertexes adjacent to i.
13
+ #
14
+ class DirectedEdgeSet
15
+ def initialize(graph_size)
16
+ @edges = Array.new(graph_size + 1) { [] }
17
+ end
18
+
19
+ def add(v, w)
20
+ edges[v] << w
21
+ end
22
+
23
+ def adjacent_vertices(v)
24
+ edges[v]
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :edges
30
+ end
31
+ end
@@ -0,0 +1,23 @@
1
+ # encoding: utf-8
2
+
3
+ module GraphMatching
4
+ class GraphMatchingError < StandardError
5
+ end
6
+
7
+ # no-doc
8
+ class InvalidVertexNumbering < GraphMatchingError
9
+ def initialize(msg = nil)
10
+ msg ||= <<-EOS
11
+ Expected vertexes to be consecutive positive integers \
12
+ starting with zero
13
+ EOS
14
+ super(msg)
15
+ end
16
+ end
17
+
18
+ class DisconnectedGraph < GraphMatchingError
19
+ end
20
+
21
+ class NotBipartite < GraphMatchingError
22
+ end
23
+ end
@@ -0,0 +1,37 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rgl/bipartite'
4
+ require_relative 'graph'
5
+ require_relative '../algorithm/mcm_bipartite'
6
+
7
+ module GraphMatching
8
+ module Graph
9
+ # A bipartite graph (or bigraph) is a graph whose vertices can
10
+ # be divided into two disjoint sets U and V such that every
11
+ # edge connects a vertex in U to one in V.
12
+ class Bigraph < Graph
13
+ def maximum_cardinality_matching
14
+ Algorithm::MCMBipartite.new(self).match
15
+ end
16
+
17
+ # `partition` either returns two disjoint (complementary)
18
+ # proper subsets of vertexes or raises a NotBipartite error.
19
+ #
20
+ # An empty graph is partitioned into two empty sets. This
21
+ # seems natural, but unfortunately is not the behavior of
22
+ # RGL's new `bipartite_sets` function. So, we have to check
23
+ # for the empty case, but at least we don't have to implement
24
+ # the algorithm ourselves anymore!
25
+ #
26
+ def partition
27
+ if empty?
28
+ [Set.new, Set.new]
29
+ else
30
+ arrays = bipartite_sets
31
+ fail NotBipartite if arrays.nil?
32
+ [Set.new(arrays[0]), Set.new(arrays[1])]
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,63 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rgl/adjacency'
4
+ require 'rgl/connected_components'
5
+ require 'set'
6
+ require_relative '../algorithm/mcm_general'
7
+ require_relative '../ordered_set'
8
+
9
+ autoload(:SecureRandom, 'securerandom')
10
+
11
+ module GraphMatching
12
+ module Graph
13
+ # Base class for all graphs.
14
+ class Graph < RGL::AdjacencyGraph
15
+ def self.[](*args)
16
+ super.tap(&:vertexes_must_be_integers)
17
+ end
18
+
19
+ def initialize(*args)
20
+ super
21
+ vertexes_must_be_integers
22
+ end
23
+
24
+ # `adjacent_vertex_set` is the same as `adjacent_vertices`
25
+ # except it returns a `Set` instead of an `Array`. This is
26
+ # an optimization, performing in O(n), whereas passing
27
+ # `adjacent_vertices` to `Set.new` would be O(2n).
28
+ def adjacent_vertex_set(v)
29
+ s = Set.new
30
+ each_adjacent(v) do |u| s.add(u) end
31
+ s
32
+ end
33
+
34
+ def connected?
35
+ count = 0
36
+ each_connected_component { count += 1 }
37
+ count == 1
38
+ end
39
+
40
+ def maximum_cardinality_matching
41
+ Algorithm::MCMGeneral.new(self).match
42
+ end
43
+
44
+ def max_v
45
+ vertexes.max
46
+ end
47
+
48
+ def print
49
+ base_filename = SecureRandom.hex(16)
50
+ Visualize.new(self).png(base_filename)
51
+ end
52
+
53
+ def vertexes
54
+ to_a
55
+ end
56
+
57
+ def vertexes_must_be_integers
58
+ return if vertices.none? { |v| !v.is_a?(Integer) }
59
+ fail ArgumentError, 'All vertexes must be integers'
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,112 @@
1
+ # encoding: utf-8
2
+
3
+ module GraphMatching
4
+ module Graph
5
+ # The `Weighted` module is mixed into undirected graphs to
6
+ # support edge weights. Directed graphs are not supported.
7
+ #
8
+ # Data Structure
9
+ # --------------
10
+ #
11
+ # Weights are stored in a 2D array. The weight of an edge i,j
12
+ # is stored twice, at `[i][j]` and `[j][i]`.
13
+ #
14
+ # Storing the weight twice wastes memory. A symmetrical matrix
15
+ # can be stored in a 1D array (http://bit.ly/1DMfLM3)
16
+ # However, translating the 2D coordinates into a 1D index
17
+ # marginally increases the cost of access, and this is a read-heavy
18
+ # structure, so maybe the extra memory is an acceptable trade-off.
19
+ # It's also conceptually simpler, for what that's worth.
20
+ #
21
+ # If directed graphs were supported (they are not) this 2D array
22
+ # would be an obvious choice.
23
+ #
24
+ # Algorithms which operate on weighted graphs are tightly
25
+ # coupled to this data structure due to optimizations.
26
+ #
27
+ module Weighted
28
+ def self.included(base)
29
+ base.extend ClassMethods
30
+ base.class_eval do
31
+ attr_accessor :weight
32
+ end
33
+ end
34
+
35
+ # no-doc
36
+ module ClassMethods
37
+ # `.[]` is the recommended, convenient constructor for
38
+ # weighted graphs. Each argument should be an array with
39
+ # three integers; the first two represent the edge, the
40
+ # third, the weight.
41
+ def [](*args)
42
+ assert_weighted_edges(args)
43
+ weightless_edges = args.map { |e| e.slice(0..1) }
44
+ g = super(*weightless_edges.flatten)
45
+ g.init_weights
46
+ args.each do |edge|
47
+ i, j, weight = edge[0] - 1, edge[1] - 1, edge[2]
48
+ g.weight[i][j] = weight
49
+ g.weight[j][i] = weight
50
+ end
51
+ g
52
+ end
53
+
54
+ # `assert_weighted_edges` asserts that `ary` is an array
55
+ # whose elements are all arrays of exactly three elements.
56
+ # (The first two represent the edge, the third, the weight)
57
+ def assert_weighted_edges(ary)
58
+ return if ary.is_a?(Array) && ary.all?(&method(:weighted_edge?))
59
+ fail 'Invalid array of weighted edges'
60
+ end
61
+
62
+ # `weighted_edge?` returns true if `e` is an array whose
63
+ # first two elements are integers, and whose third element
64
+ # is a real number.
65
+ def weighted_edge?(e)
66
+ e.is_a?(Array) &&
67
+ e.length == 3 &&
68
+ e[0, 2].all? { |i| i.is_a?(Integer) } &&
69
+ e[2].is_a?(Integer) || e[2].is_a?(Float)
70
+ end
71
+ end
72
+
73
+ def init_weights
74
+ @weight = Array.new(num_vertices) { |_| Array.new(num_vertices) }
75
+ end
76
+
77
+ def max_w
78
+ edges.map { |edge| w(edge.to_a) }.max
79
+ end
80
+
81
+ # Returns the weight of an edge. Accessing `#weight` is much
82
+ # faster, so this method should only be used where
83
+ # clarity outweighs performance.
84
+ def w(edge)
85
+ i, j = edge
86
+ fail ArgumentError, "Invalid edge: #{edge}" if i.nil? || j.nil?
87
+ fail "Edge not found: #{edge}" unless has_edge?(*edge)
88
+ init_weights if @weight.nil?
89
+ @weight[i - 1][j - 1]
90
+ end
91
+
92
+ # `set_w` sets a single weight. It not efficient, and is
93
+ # only provided for situations where constructing the entire
94
+ # graph with `.[]` is not convenient.
95
+ def set_w(edge, weight)
96
+ if edge[0].nil? || edge[1].nil?
97
+ fail ArgumentError, "Invalid edge: #{edge}"
98
+ end
99
+ unless weight.is_a?(Integer)
100
+ fail TypeError, "Edge weight must be integer"
101
+ end
102
+ init_weights if @weight.nil?
103
+ i, j = edge[0] - 1, edge[1] - 1
104
+ fail "Edge not found: #{edge}" unless has_edge?(*edge)
105
+ @weight[i] ||= []
106
+ @weight[j] ||= []
107
+ @weight[i][j] = weight
108
+ @weight[j][i] = weight
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,17 @@
1
+ # encoding: utf-8
2
+
3
+ require_relative 'weighted'
4
+ require_relative '../algorithm/mwm_bipartite'
5
+
6
+ module GraphMatching
7
+ module Graph
8
+ # A bigraph whose edges have weights. See `Weighted`.
9
+ class WeightedBigraph < Bigraph
10
+ include Weighted
11
+
12
+ def maximum_weighted_matching
13
+ Algorithm::MWMBipartite.new(self).match
14
+ end
15
+ end
16
+ end
17
+ end