graph_matching 0.0.1

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