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,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?)