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,17 @@
1
+ # encoding: utf-8
2
+
3
+ require_relative 'weighted'
4
+ require_relative '../algorithm/mwm_general'
5
+
6
+ module GraphMatching
7
+ module Graph
8
+ # A graph whose edges have weights. See `Weighted`.
9
+ class WeightedGraph < Graph
10
+ include Weighted
11
+
12
+ def maximum_weighted_matching(max_cardinality)
13
+ Algorithm::MWMGeneral.new(self).match(max_cardinality)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,29 @@
1
+ # encoding: utf-8
2
+
3
+ module GraphMatching
4
+ # Converts the vertices of a graph to integers. Many graph
5
+ # matching algorithms require integer vertexes.
6
+ module IntegerVertexes
7
+ # Converts the vertices of `graph` to positive nonzero integers.
8
+ # For example, given a graph (a=b), returns a new graph (1=2).
9
+ # It also returns a legend, which maps the integers to the
10
+ # original vertexes.
11
+ #
12
+ def self.to_integers(graph)
13
+ fail ArgumentError unless graph.is_a?(RGL::MutableGraph)
14
+ legend = {}
15
+ reverse_legend = {}
16
+ new_graph = graph.class.new
17
+ graph.vertices.each_with_index do |vertex, ix|
18
+ legend[ix + 1] = vertex
19
+ reverse_legend[vertex] = ix + 1
20
+ end
21
+ graph.edges.each do |edge|
22
+ source = reverse_legend[edge.source]
23
+ target = reverse_legend[edge.target]
24
+ new_graph.add_edge(source, target)
25
+ end
26
+ return new_graph, legend
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,120 @@
1
+ # encoding: utf-8
2
+
3
+ module GraphMatching
4
+ # > In .. graph theory, a matching .. in a graph is a set of
5
+ # > edges without common vertices.
6
+ # > https://en.wikipedia.org/wiki/Matching_%28graph_theory%29
7
+ class Matching
8
+ # Gabow (1976) uses a simple array to store his matching. It
9
+ # has one element for each vertex in the graph. The value of
10
+ # each element is either the number of another vertex (Gabow
11
+ # uses sequential integers for vertex numbering) or a zero if
12
+ # unmatched. So, `.gabow` returns a `Matching` initialized
13
+ # from such an array.
14
+ def self.gabow(mate)
15
+ m = new
16
+ mate.each_with_index do |n1, ix|
17
+ next if n1.nil? || n1 == 0
18
+ n2 = mate[n1]
19
+ if n2 == ix
20
+ m.add([n1, n2])
21
+ end
22
+ end
23
+ m
24
+ end
25
+
26
+ # Van Rantwijk's matching is constructed from two arrays,
27
+ # `mate` and `endpoint`.
28
+ #
29
+ # - `endpoint` is an array where each edge is represented by
30
+ # two consecutive elements, which are vertex numbers.
31
+ # - `mate` is an array whose indexes are vertex numbers, and
32
+ # whose values are `endpoint` indexes, or `nil` if the vertex
33
+ # is single (unmatched).
34
+ #
35
+ # A matched vertex `v`'s partner is `endpoint[mate[v]]`.
36
+ #
37
+ def self.from_endpoints(endpoint, mate)
38
+ m = Matching.new
39
+ mate.each do |p|
40
+ m.add([endpoint[p], endpoint[p ^ 1]]) unless p.nil?
41
+ end
42
+ m
43
+ end
44
+
45
+ def self.[](*edges)
46
+ new.tap { |m| edges.each { |e| m.add(e) } }
47
+ end
48
+
49
+ def initialize
50
+ @ary = []
51
+ end
52
+
53
+ def [](i)
54
+ @ary[i]
55
+ end
56
+
57
+ def add(e)
58
+ i, j = e
59
+ @ary[i] = j
60
+ @ary[j] = i
61
+ end
62
+
63
+ def delete(e)
64
+ i, j = e
65
+ @ary[i] = nil
66
+ @ary[j] = nil
67
+ end
68
+
69
+ # `edges` returns an array of undirected edges, represented as
70
+ # two-element arrays.
71
+ def edges
72
+ undirected_edges.map(&:to_a)
73
+ end
74
+
75
+ def empty?
76
+ @ary.all?(&:nil?)
77
+ end
78
+
79
+ def edge?(e)
80
+ i, j = e
81
+ !@ary[i].nil? && @ary[i] == j && @ary[j] == i
82
+ end
83
+
84
+ def vertex?(v)
85
+ @ary.include?(v)
86
+ end
87
+
88
+ # `size` returns number of edges
89
+ def size
90
+ @ary.compact.size / 2
91
+ end
92
+
93
+ def to_a
94
+ result = []
95
+ skip = []
96
+ @ary.each_with_index { |e, i|
97
+ unless e.nil? || skip.include?(i)
98
+ result << [i, e]
99
+ skip << e
100
+ end
101
+ }
102
+ result
103
+ end
104
+
105
+ # Given a `Weighted` graph `g`, returns the sum of edge weights.
106
+ def weight(g)
107
+ edges.map { |e| g.w(e) }.reduce(0, :+)
108
+ end
109
+
110
+ def undirected_edges
111
+ @ary.each_with_index.inject(Set.new) { |set, (el, ix)|
112
+ el.nil? ? set : set.add(RGL::Edge::UnDirectedEdge.new(el, ix))
113
+ }
114
+ end
115
+
116
+ def vertexes
117
+ @ary.compact
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,59 @@
1
+ # encoding: utf-8
2
+
3
+ module GraphMatching
4
+ # An `OrderedSet` acts like a `Set`, but preserves insertion order.
5
+ # Internally, a `Hash` is used because, as of Ruby 1.9, it
6
+ # preserves insertion order. The Set library happens to be built
7
+ # upon a Hash currently but this might change in the future.
8
+ class OrderedSet
9
+ include Enumerable
10
+
11
+ # `.[]` returns a new ordered set containing the given objects.
12
+ # This mimics the signature of `Set.[]` and `Array.[]`.
13
+ def self.[](*args)
14
+ new.merge(args)
15
+ end
16
+
17
+ def initialize
18
+ @hash = {}
19
+ end
20
+
21
+ # `add` `o` unless it already exists, preserving inserting order.
22
+ # This mimics the signature of `Set#add`. See alias `#enq`.
23
+ def add(o)
24
+ @hash[o] = true
25
+ end
26
+ alias_method :enq, :add
27
+
28
+ def deq
29
+ @hash.keys.first.tap do |k| @hash.delete(k) end
30
+ end
31
+
32
+ def each
33
+ @hash.each do |k, _v| yield k end
34
+ end
35
+
36
+ def empty?
37
+ @hash.empty?
38
+ end
39
+
40
+ # `merge` the elements of the given enumerable object to the set
41
+ # and returns self. This mimics the signature of `Set#merge`.
42
+ def merge(enum)
43
+ enum.each do |e| add(e) end
44
+ self
45
+ end
46
+
47
+ # Removes the last element and returns it, or nil if empty.
48
+ # This mimics `Array#pop`. See related `#deq`.
49
+ def pop
50
+ @hash.keys.last.tap do |k| @hash.delete(k) end
51
+ end
52
+
53
+ # `push` appends the given object(s) and returns self. This
54
+ # mimics the signature of `Array#push`.
55
+ def push(*args)
56
+ merge(args)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,6 @@
1
+ # encoding: utf-8
2
+
3
+ # no-doc
4
+ module GraphMatching
5
+ VERSION = "0.0.1"
6
+ end
@@ -0,0 +1,93 @@
1
+ # encoding: utf-8
2
+
3
+ require 'open3'
4
+ require 'rgl/rdot'
5
+
6
+ module GraphMatching
7
+ # Renders `GraphMatching::Graph` objects using `graphviz`.
8
+ class Visualize
9
+ TMP_DIR = '/tmp/graph_matching'
10
+ USR_BIN_ENV = '/usr/bin/env'
11
+
12
+ attr_reader :graph
13
+
14
+ def initialize(graph)
15
+ @graph = graph
16
+ end
17
+
18
+ # `dot` returns a string representing the graph, in .dot format.
19
+ # http://www.graphviz.org/content/dot-language
20
+ def dot
21
+ RGL::DOT::Graph.new('elements' => dot_edges).to_s
22
+ end
23
+
24
+ # `png` writes a ".png" file with graphviz and opens it
25
+ def png(base_filename)
26
+ check_that_dot_is_installed
27
+ mk_tmp_dir
28
+ abs_path = "#{TMP_DIR}/#{base_filename}.png"
29
+ write_png(abs_path)
30
+ system "open #{abs_path}"
31
+ end
32
+
33
+ private
34
+
35
+ def check_that_dot_is_installed
36
+ return if dot_installed?
37
+ $stderr.puts "Executable not found: dot"
38
+ $stderr.puts "Please install graphviz"
39
+ exit(1)
40
+ end
41
+
42
+ def dot_edge(u, v, label)
43
+ RGL::DOT::Edge.new(
44
+ { 'from' => u, 'to' => v, 'label' => label },
45
+ ['label']
46
+ )
47
+ end
48
+
49
+ def dot_edges
50
+ graph.edges.map { |e| dot_edge(e.source, e.target, dot_edge_label(e)) }
51
+ end
52
+
53
+ def dot_edge_label(edge)
54
+ graph.is_a?(GraphMatching::Graph::Weighted) ? graph.w([*edge]) : nil
55
+ end
56
+
57
+ def assert_usr_bin_env_exists
58
+ return if File.exist?(USR_BIN_ENV)
59
+ $stderr.puts "File not found: #{USR_BIN_ENV}"
60
+ exit(1)
61
+ end
62
+
63
+ # `dot_installed?` returns true if `dot` is installed, otherwise
64
+ # false. Note that `system` returns true if the command gives
65
+ # zero exit status, false for non-zero exit status.
66
+ def dot_installed?
67
+ assert_usr_bin_env_exists
68
+ system "#{USR_BIN_ENV} which dot > /dev/null"
69
+ end
70
+
71
+ def mk_tmp_dir
72
+ Dir.mkdir(TMP_DIR) unless Dir.exist?(TMP_DIR)
73
+ end
74
+
75
+ def safe_vertex(v)
76
+ if v.is_a?(Integer)
77
+ v
78
+ elsif v.respond_to?(:to_dot)
79
+ v.to_dot
80
+ else
81
+ v.to_s.gsub(/[^a-zA-Z0-9]/, '')
82
+ end
83
+ end
84
+
85
+ def write_png(abs_path)
86
+ _so, se, st = Open3.capture3("dot -T png > #{abs_path}", stdin_data: dot)
87
+ return if st.success?
88
+ $stderr.puts "Failed to generate .png"
89
+ $stderr.puts se
90
+ exit(1)
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env bash
2
+
3
+ BENCHMARK_DIR='benchmark/mcm_bipartite/complete_bigraphs'
4
+
5
+ if [ ! -d "$BENCHMARK_DIR" ]; then
6
+ echo "Directory not found: $BENCHMARK_DIR" 1>&2
7
+ exit 1
8
+ fi
9
+
10
+ echo "Benchmarking .."
11
+ ruby -I lib "$BENCHMARK_DIR/benchmark.rb" > "$BENCHMARK_DIR/time2.data"
12
+
13
+ echo "Plotting .."
14
+ gnuplot "$BENCHMARK_DIR/compare.gnuplot"
15
+ open "$BENCHMARK_DIR/plot_compare.png"
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env bash
2
+
3
+ BENCHMARK_DIR='benchmark/mcm_bipartite/complete_bigraphs'
4
+
5
+ if [ ! -d "$BENCHMARK_DIR" ]; then
6
+ echo "Directory not found: $BENCHMARK_DIR" 1>&2
7
+ exit 1
8
+ fi
9
+
10
+ rm "$BENCHMARK_DIR/plot_compare.png"
11
+ mv "$BENCHMARK_DIR/time2.data" "$BENCHMARK_DIR/time.data"
12
+ gnuplot "$BENCHMARK_DIR/plot.gnuplot"
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env bash
2
+
3
+ BENCHMARK_DIR='benchmark/mwm_general/complete_graphs'
4
+
5
+ if [ ! -d "$BENCHMARK_DIR" ]; then
6
+ echo "Directory not found: $BENCHMARK_DIR" 1>&2
7
+ exit 1
8
+ fi
9
+
10
+ echo "Benchmarking .."
11
+ ruby -I lib "$BENCHMARK_DIR/benchmark.rb" > "$BENCHMARK_DIR/time2.data"
12
+
13
+ echo "Plotting .."
14
+ gnuplot "$BENCHMARK_DIR/compare.gnuplot"
15
+ open "$BENCHMARK_DIR/plot_compare.png"
@@ -0,0 +1,28 @@
1
+ # No shebang here. Run with:
2
+ #
3
+ # ruby -I lib profile/mwm_general/profile.rb
4
+
5
+ require 'graph_matching'
6
+ require 'ruby-prof'
7
+
8
+ def complete_graph(n)
9
+ g = GraphMatching::Graph::WeightedGraph.new
10
+ n_edges = (1 .. n - 1).reduce(:+)
11
+ 0.upto(n - 2) do |i|
12
+ (i + 1).upto(n - 1) do |j|
13
+ g.add_edge(i, j)
14
+ g.set_w([i, j], rand(n_edges))
15
+ end
16
+ end
17
+ g
18
+ end
19
+
20
+ g = complete_graph(100)
21
+ GC.disable
22
+ RubyProf.start
23
+ g.maximum_weighted_matching(true)
24
+ result = RubyProf.stop
25
+ GC.enable
26
+
27
+ printer = RubyProf::FlatPrinter.new(result)
28
+ printer.print(STDOUT)
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env bash
2
+
3
+ BENCHMARK_DIR='benchmark/mwm_general/complete_graphs'
4
+
5
+ if [ ! -d "$BENCHMARK_DIR" ]; then
6
+ echo "Directory not found: $BENCHMARK_DIR" 1>&2
7
+ exit 1
8
+ fi
9
+
10
+ rm "$BENCHMARK_DIR/plot_compare.png"
11
+ mv "$BENCHMARK_DIR/time2.data" "$BENCHMARK_DIR/time.data"
12
+ gnuplot "$BENCHMARK_DIR/plot.gnuplot"
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,86 @@
1
+ Stage 1
2
+
3
+ 5 -- 7
4
+ / || ||
5
+ 1 = 3 || o
6
+ / \ ||
7
+ r 6
8
+ \ /
9
+ 2 = 4
10
+ /
11
+ a
12
+
13
+ S = [ 1 by r, 2 by r, 5 by 3, 6 by 3 ]
14
+ T = [ 3 by 1, 4 by 1 ]
15
+
16
+ Stage 2
17
+
18
+ 1 = B -- 7
19
+ / | ||
20
+ r | o
21
+ \ |
22
+ 2 = 4
23
+ /
24
+ a
25
+
26
+ S = [ 1 by r, 2 by r, 5 by 3, 6 by 3 ]
27
+ T = [ 3 by 1, 4 by 1 ]
28
+
29
+ Stage 3
30
+
31
+ B -- 7
32
+ / ||
33
+ a o
34
+
35
+ Find augmenting path from r, *through B* to a
36
+
37
+ 1. AP = (a,B)
38
+ 1. expand B
39
+
40
+ 1 = B -- 7
41
+ / | ||
42
+ r | o
43
+ \ |
44
+ 2 = 4
45
+ /
46
+ a
47
+
48
+ 1. Kusner says: "propagating the augmenting path through the expansion steps"
49
+ 1. AP = (a,2), etc.. but how to find "etc"? We know we want an
50
+ alternating path, so starting at 2, find a matched edge.
51
+ 1. AP = a2, 24
52
+ 1. AP = a2, 24, 4B
53
+ 1. B is a blossom. Expand it.
54
+
55
+ 5 -- 7
56
+ / || ||
57
+ 1 = 3 || o
58
+ / \ ||
59
+ r 6
60
+ \ /
61
+ 2 = 4
62
+ /
63
+ a
64
+
65
+ 1. AP = a2, 24, 46
66
+ 1. AP = a2, 24, 46, 65
67
+ 1. Should we follow (5,3) or (5,7), or both?
68
+ 1. Following (5,7) doesn't reach r, and if we follow it,
69
+ either depth-first or breadth-first, we'll learn that.
70
+ 1. AP = a2, 24, 46, 65, 53
71
+ 1. AP = a2, 24, 46, 65, 53, 31
72
+ 1. AP = a2, 24, 46, 65, 53, 31, 1r
73
+ 1. Augmenting path: a, 2, 4, 6, 5, 3, 1, r
74
+
75
+ 5 -- 7
76
+ //| ||
77
+ 1 - 3 | o
78
+ // \|
79
+ r 6
80
+ \ //
81
+ 2 - 4
82
+ //
83
+ a
84
+
85
+ 1. size of matching = 5. Decide is maximum cardinality
86
+ matching. (How?)