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,35 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'graph_matching/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'graph_matching'
8
+ spec.version = GraphMatching::VERSION
9
+ spec.authors = ['Jared Beck']
10
+ spec.email = ['jared@jaredbeck.com']
11
+ spec.summary = 'Finds maximum matchings in undirected graphs.'
12
+ spec.description = <<-EOS
13
+ Efficient algorithms for maximum cardinality
14
+ and weighted matchings in undirected graphs.
15
+ EOS
16
+ spec.homepage = 'https://github.com/jaredbeck/graph_matching'
17
+ spec.license = 'MIT'
18
+
19
+ spec.files = `git ls-files -z`.split("\x0")
20
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
21
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
22
+ spec.require_paths = ['lib']
23
+
24
+ spec.required_ruby_version = '>= 1.9.3'
25
+
26
+ spec.add_runtime_dependency 'rgl', '~> 0.5.0'
27
+
28
+ spec.add_development_dependency 'bundler'
29
+ spec.add_development_dependency 'pry'
30
+ spec.add_development_dependency 'pry-nav'
31
+ spec.add_development_dependency 'rspec-core', '~> 3.2'
32
+ spec.add_development_dependency 'rspec-expectations', '~> 3.2'
33
+ spec.add_development_dependency 'rspec-mocks', '~> 3.2'
34
+ spec.add_development_dependency 'rubocop'
35
+ end
@@ -0,0 +1,15 @@
1
+ # encoding: utf-8
2
+
3
+ require 'graph_matching/core_ext/set'
4
+ require 'graph_matching/graph/graph'
5
+ require 'graph_matching/graph/bigraph'
6
+ require 'graph_matching/graph/weighted_graph'
7
+ require 'graph_matching/graph/weighted_bigraph'
8
+ require 'graph_matching/errors'
9
+ require 'graph_matching/version'
10
+ require 'graph_matching/visualize'
11
+
12
+ # Efficient algorithms for maximum cardinality and weighted
13
+ # matchings in undirected graphs.
14
+ module GraphMatching
15
+ end
@@ -0,0 +1,23 @@
1
+ # encoding: utf-8
2
+
3
+ require_relative '../assertion'
4
+
5
+ module GraphMatching
6
+ module Algorithm
7
+
8
+ # All matching algorithms operate on a graph, hence the
9
+ # common constructor.
10
+ class MatchingAlgorithm
11
+ attr_reader :g
12
+
13
+ def initialize(graph)
14
+ @g = graph
15
+ end
16
+
17
+ def assert(obj)
18
+ Assertion.new(obj)
19
+ end
20
+ end
21
+
22
+ end
23
+ end
@@ -0,0 +1,118 @@
1
+ # encoding: utf-8
2
+
3
+ require_relative '../matching'
4
+ require_relative 'matching_algorithm'
5
+
6
+ module GraphMatching
7
+ module Algorithm
8
+
9
+ # `MCMBipartite` implements Maximum Cardinality Matching in
10
+ # bipartite graphs.
11
+ class MCMBipartite < MatchingAlgorithm
12
+
13
+ def initialize(graph)
14
+ assert(graph).is_a(Graph::Bigraph)
15
+ super
16
+ end
17
+
18
+ def match
19
+ u, v = g.partition
20
+ m = []
21
+
22
+ while true
23
+
24
+ # Begin each stage by clearing all labels and marks
25
+ t = []
26
+ predecessors = {}
27
+ aug_path = nil
28
+
29
+ # Label unmatched vertexes in U with label R. These R-vertexes
30
+ # are candidates for the start of an augmenting path.
31
+ unmarked = r = u.select { |i| m[i] == nil }
32
+
33
+ # While there are unmarked R-vertexes
34
+ while aug_path.nil? && start = unmarked.pop
35
+
36
+ # Follow the unmatched edges (if any) to vertexes in V
37
+ # ignoring any V-vertexes already labeled T
38
+ unlabeled_across_unmatched_edges_from(start, g, m ,t).each do |vi|
39
+ t << vi
40
+ predecessors[vi] = start
41
+
42
+ # If there are matched edges, follow each to a vertex
43
+ # in U and label the U-vertex with R. Otherwise,
44
+ # backtrack to construct an augmenting path.
45
+ adj_u_in_m = matched_adjacent(vi, start, g, m)
46
+
47
+ adj_u_in_m.each do |ui|
48
+ r << ui
49
+ predecessors[ui] = vi
50
+ end
51
+
52
+ if adj_u_in_m.empty?
53
+ aug_path = backtrack_from(vi, predecessors)
54
+ break
55
+ end
56
+ end
57
+ end
58
+
59
+ if aug_path.nil?
60
+ break
61
+ else
62
+ m = augment(m, aug_path)
63
+ end
64
+ end
65
+
66
+ Matching.gabow(m)
67
+ end
68
+
69
+ private
70
+
71
+ def assert_valid_aug_path(path)
72
+ unless path.length >= 2 && path.length.even?
73
+ raise "Invalid augmenting path"
74
+ end
75
+ end
76
+
77
+ def augment(m, path)
78
+ assert_valid_aug_path(path)
79
+ ix = 0
80
+ while ix < path.length
81
+ i = path[ix]
82
+ j = path[ix + 1]
83
+ mi = m[i]
84
+ mj = m[j]
85
+ m[mi] = nil unless mi == nil
86
+ m[mj] = nil unless mj == nil
87
+ m[i] = j
88
+ m[j] = i
89
+ ix += 2
90
+ end
91
+ m
92
+ end
93
+
94
+ def backtrack_from(end_vertex, predecessors)
95
+ augmenting_path = [end_vertex]
96
+ while predecessors.has_key?(augmenting_path.last)
97
+ augmenting_path.push(predecessors[augmenting_path.last])
98
+ end
99
+ augmenting_path
100
+ end
101
+
102
+ def matched_adjacent(from, except, g, m)
103
+ g.adjacent_vertices(from).select { |i|
104
+ i != except && m[from] == i
105
+ }
106
+ end
107
+
108
+ # Look across unmatched edges from `v` to find vertexes
109
+ # not labeled `t`.
110
+ def unlabeled_across_unmatched_edges_from(v, g, m, t)
111
+ g.adjacent_vertices(v).select { |i|
112
+ m[v] != i && !t.include?(i)
113
+ }
114
+ end
115
+
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,289 @@
1
+ # encoding: utf-8
2
+
3
+ require_relative '../directed_edge_set'
4
+ require_relative '../matching'
5
+ require_relative 'matching_algorithm'
6
+
7
+ module GraphMatching
8
+ module Algorithm
9
+
10
+ # `MCMGeneral` implements Maximum Cardinality Matching in
11
+ # general graphs (as opposed to bipartite).
12
+ class MCMGeneral < MatchingAlgorithm
13
+
14
+ # An LFlag represents a flag on an edge during Gabow's `l` function.
15
+ class LFlag
16
+ attr_reader :edge
17
+ def initialize(edge)
18
+ @edge = edge
19
+ end
20
+ end
21
+
22
+ def initialize(graph)
23
+ assert(graph).is_a(Graph::Graph)
24
+ super
25
+ end
26
+
27
+ def match
28
+ return Matching.new if g.empty?
29
+ raise DisconnectedGraph unless g.connected?
30
+ e(g)
31
+ end
32
+
33
+ private
34
+
35
+ # `e` constructs a maximum matching on a graph. It starts a
36
+ # search for an augmenting path to each unmatched vertex u.
37
+ # It scans edges of the graph, deciding to assign new labels
38
+ # or to augment the matching.
39
+ def e(g)
40
+ first = []
41
+ label = []
42
+ mate = []
43
+
44
+ # E0. [Initialize.] Read the graph into adjacency lists,
45
+ # numbering the vertices 1 to V and the edges V + 1 to
46
+ # V + 2W. Create a dummy vertex 0 For 0 <= i <= V, set
47
+ # LABEL(u) <- -1, MATE(i) <- 0 (all vertices are nonouter
48
+ # and unmatched) Set u <- 0
49
+
50
+ label.fill(-1, 0, g.size + 1)
51
+ mate.fill(0, 0, g.size + 1)
52
+ u = 0
53
+
54
+ # El. [Find unmatched vertex ] Set u = u + 1. If u > V,
55
+ # halt; MATE contains a maximum matching Otherwise, if vertex
56
+ # u is matched, repeat step E1 Otherwise (u is unmatched, so
57
+ # assign a start label and begin a new search)
58
+ # set LABEL(u) = FIRST(u) = 0
59
+
60
+ while true do
61
+ u += 1
62
+ break if u > g.size
63
+ if mate[u] != 0
64
+ next # repeat E1
65
+ else
66
+ label[u] = first[u] = 0
67
+ end
68
+
69
+ # E2 [Choose an edge ] Choose an edge xy, where x is an outer
70
+ # vertex. (An edge vw may be chosen twice in a search--once
71
+ # with x = v, and once with x = w.) If no such edge exists,
72
+ # go to E7. (Edges xy can be chosen in an arbitrary order. A
73
+ # possible choice method is "breadth-first": an outer vertex
74
+ # x = x1 is chosen, and edges (x1,y) are chosen in succeeding
75
+ # executions of E2, when all such edges have been chosen, the
76
+ # vertex x2 that was labeled immediately after x1 is chosen,
77
+ # and the process is repeated for x = x2. This breadth-first
78
+ # method requires that Algorithm E maintain a list of outer
79
+ # vertices, x1, x2, ...)
80
+
81
+ searching = true
82
+ visited_nodes = Set.new
83
+ visited_edges = DirectedEdgeSet.new(g.size)
84
+ q = OrderedSet[u]
85
+ while searching && !q.empty?
86
+ x = q.deq
87
+ visited_nodes.add(x)
88
+ adjacent = g.adjacent_vertex_set(x)
89
+ discovered = adjacent - visited_edges.adjacent_vertices(x)
90
+
91
+ discovered.each do |y|
92
+ visited_edges.add(x, y)
93
+
94
+ # E3. [Augment the matching.] If y is unmatched and y != u,
95
+ # set MATE(y) = x, call R(x, y): then go to E7 (R
96
+ # completes the augment along path (y)*P(x))
97
+
98
+ if mate[y] == 0 && y != u
99
+ mate[y] = x
100
+ r(x, y, label, mate)
101
+ searching = false # go to E7
102
+ break
103
+
104
+ # E4. [Assign edge labels.] If y is outer, call L, then go to
105
+ # E2 (L assigns edge label n(xy) to nonouter vertices in P(x)
106
+ # and P(y))
107
+
108
+ elsif outer?(label[y])
109
+ l(x, y, first, label, mate, q, visited_nodes)
110
+
111
+ # E5. [Assign a vertex label.] Set v <- MATE(y). If v is
112
+ # nonouter, set LABEL(v) <- x, FIRST(v) <- y, and go to E2
113
+ #
114
+ # E6. [Get next edge.] Go to E2 (y is nonouter and MATE(y) is
115
+ # outer, so edge xy adds nothing).
116
+
117
+ else
118
+ v = mate[y]
119
+ if label[v] == -1 # nonouter
120
+ label[v] = x
121
+ first[v] = y
122
+ end
123
+ unless visited_nodes.include?(v)
124
+ q.enq(v)
125
+ end
126
+ end
127
+
128
+ end
129
+ end
130
+
131
+ #
132
+ # E7. [Stop the search] Set LABEL(O) <- -1. For all outer
133
+ # vertices i set LABEL(i) <- LABEL(MATE(i)) <- -1 Then go
134
+ # to E1 (now all vertexes are nonouter for the next search).
135
+ #
136
+
137
+ label[0] = -1
138
+ label.each_with_index do |obj, ix|
139
+ if ix > 0 && outer?(obj)
140
+ label[ix] = label[mate[ix]] = -1
141
+ end
142
+ end
143
+
144
+ end # while e0_loop
145
+
146
+ Matching.gabow(mate)
147
+ end
148
+
149
+ def edge_label?(label_value)
150
+ label_value.is_a?(RGL::Edge::UnDirectedEdge)
151
+ end
152
+
153
+ # L assigns the edge label n(xy) to nonouter vertices. Edge xy
154
+ # joins outer vertices x, y. L sets join to the first nonouter
155
+ # vertex in both P(x) and P(y). Then it labels all nonouter
156
+ # vertices preceding join in P(x) or P(y).
157
+ def l(x, y, first, label, mate, q, visited_nodes)
158
+
159
+ # L0. [Initialize.] Set r <- FIRST(x), s <= FIRST(y).
160
+ # If r = s, return (no vertices can be labeled).
161
+ # Otherwise flag r and s. (Steps L1-L2 find join by advancing
162
+ # alternately along paths P(x) and P(y). Flags are assigned
163
+ # to nonouter vertices r in these paths. This is done by
164
+ # setting LABEL(r) to a negative edge number, LABEL(r) <- -n(xy).
165
+ # This way, each invocation of L uses a distinct flag value.)
166
+
167
+ r = first[x]
168
+ s = first[y]
169
+
170
+ if r == s
171
+ return # no vertices can be labeled
172
+ else
173
+ label[r] = LFlag.new(n(x, y))
174
+ end
175
+
176
+ # L1. [Switch paths ] If s != 0, interchange r and s, r <-> s
177
+ # (r is a flagged nonouter vertex, alternately in P(x) and P(y)).
178
+
179
+ finding_join = true
180
+ while finding_join
181
+ if s != 0
182
+ temp = r
183
+ r = s
184
+ s = temp
185
+ end
186
+
187
+ # L2. [Next nonouter vertex.] Set r <- FIRST(LABEL(MATE(r)))
188
+ # (r is set to the next nonouter vertex in P(x) or P(y)). If
189
+ # r is not flagged, flag r and go to L1 Otherwise set
190
+ # join <- r and go to L3.
191
+
192
+ r = first[label[mate[r]]]
193
+ if label[r].is_a?(LFlag)
194
+ join = r
195
+ finding_join = false
196
+ else
197
+ label[r] = LFlag.new(n(x, y))
198
+ end
199
+ end
200
+
201
+ # L3. [Label vertices in P(x), P(y).] (All nonouter vertexes
202
+ # between x and join, or y and join, will be assigned edge
203
+ # labels. See Figure 4(a).) Set v <- FIRST(x) and do L4. Then
204
+ # set v <- FIRST(y) and do L4. Then go to L5.
205
+
206
+ [first[x], first[y]].each do |v|
207
+
208
+ # L4 [Label v] If v != join, set LABEL(v) <- n(xy), FIRST(v) <- join,
209
+ # v <- FIRST(LABEL(MATE(v))) and repeat step L4
210
+ # Otherwise continue as specified in L3.
211
+
212
+ until v == join
213
+ label[v] = n(x, y)
214
+ unless visited_nodes.include?(v)
215
+ q.enq(v)
216
+ end
217
+ first[v] = join
218
+ v = first[label[mate[v]]]
219
+ end
220
+ end
221
+
222
+ # L5 [Update FIRST] For each outer vertex i, if FIRST(i) is
223
+ # outer, set FIRST(i) <- join. (Join is now the first nonouter
224
+ # vertex in P(i))
225
+
226
+ label.each_with_index do |l, i|
227
+ if i > 0 && outer?(l) && outer?(label[first[i]])
228
+ first[i] = join
229
+ end
230
+ end
231
+
232
+ # L6. [Done] Return
233
+ end
234
+
235
+ # Gabow (1976) describes a function `n` which returns the number
236
+ # of the edge from `x` to `y`. Because we are using RGL, and
237
+ # not implementing our own adjacency lists, we can simply return
238
+ # an RGL::Edge::UnDirectedEdge.
239
+ def n(x, y)
240
+ RGL::Edge::UnDirectedEdge.new(x, y)
241
+ end
242
+
243
+ def outer?(label_value)
244
+ !label_value.is_a?(Integer) || label_value >= 0
245
+ end
246
+
247
+ # R(v, w) rematches edges in the augmenting path. Vertex v is
248
+ # outer. Part of path (w) * P(v) is in the augmenting path. It
249
+ # gets re-matched by R(v, w) (Although R sets MATE(v) <- w, it
250
+ # does not set MATE(w) <- v. This is done in step E3 or another
251
+ # call to R.) R is a recursive routine.
252
+ def r(v, w, label, mate)
253
+
254
+ # R1. [Match v to w ] Set t <- MATE(v), MATE(v) <- w.
255
+ # If MATE(t) != v, return (the path is completely re-matched)
256
+
257
+ t = mate[v]
258
+ mate[v] = w
259
+ return if mate[t] != v
260
+
261
+ # R2. [Rematch a path.] If v has a vertex label, set
262
+ # MATE(t) <- LABEL(v), call R(LABEL(v), t) recursively, and
263
+ # then return.
264
+
265
+ if vertex_label?(label[v])
266
+ mate[t] = label[v]
267
+ r(label[v], t, label, mate)
268
+
269
+ # R3. [Rematch two paths.] (Vertex v has an edge label)
270
+ # Set x, y to vertices so LABEL(v) = n(xy), call R(x, y)
271
+ # recursively, call R(y, x) recursively, and then return.
272
+
273
+ elsif edge_label?(label[v])
274
+ x, y = label[v].to_a
275
+ r(x, y, label, mate)
276
+ r(y, x, label, mate)
277
+ else
278
+ fail "Vertex #{v} has an unexpected label type"
279
+ end
280
+ end
281
+
282
+ def vertex_label?(label_value)
283
+ label_value.is_a?(Integer) && label_value > 0
284
+ end
285
+
286
+ end
287
+
288
+ end
289
+ end