graphr 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,18 @@
1
+ Graph-related Ruby classes written by Robert Feld in 2001.
2
+
3
+ This program is free software: you can redistribute it and/or modify
4
+ it under the terms of the GNU General Public License as published by
5
+ the Free Software Foundation, either version 3 of the License, or
6
+ (at your option) any later version.
7
+
8
+ This program is distributed in the hope that it will be useful,
9
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ GNU General Public License for more details.
12
+
13
+ This license also applies to the included Stanford CoreNLP files.
14
+
15
+ You should have received a copy of the GNU General Public License
16
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+ Author: Robert Feldt, feldt@ce.chalmers.se. All rights reserved.
@@ -0,0 +1,46 @@
1
+ ##graphr
2
+
3
+ ###About
4
+
5
+ Graph-related Ruby classes [written by Robert Feld](http://rockit.sourceforge.net/subprojects/graphr/) in 2001, but never released as a gem.
6
+
7
+ * DirectedGraph - fairly extensive directed graph class.
8
+ * DotGraphPrinter - output a graph in GraphViz's dot format and invoke dot.
9
+
10
+ ###Example Usage
11
+
12
+ ```ruby
13
+ # Lets crank out a simple graph...
14
+ require 'graph/graphviz_dot'
15
+
16
+ # In this simple example we don't even have a "real" graph
17
+ # just an Array with the links. The optional third element
18
+ # of a link is link information. The nodes in this graph are
19
+ # implicit in the links. If we had additional nodes that were
20
+ # not linked we would supply them in an array as 2nd parameter.
21
+ links = [[:start, 1, "*"], [1, 1, "a"], [1, 2, "~a"], [2, :stop, "*"]]
22
+ dgp = DotGraphPrinter.new(links)
23
+
24
+ # We specialize the printer to change the shape of nodes
25
+ # based on their names.
26
+ dgp.node_shaper = proc do |n|
27
+ ["start", "stop"].include?(n.to_s) ? "doublecircle" : "box"
28
+ end
29
+
30
+ # We can also set the attributes on individual nodes and edges.
31
+ # These settings override the default shapers and labelers.
32
+ dgp.set_node_attributes(2, :shape => "diamond")
33
+
34
+ # Add URL link from node (this only work in some output formats?)
35
+ # Note the extra quotes needed!
36
+ dgp.set_node_attributes(2, :URL => '"node2.html"')
37
+
38
+ # And now output to files
39
+ dgp.write_to_file("g.png", "png") # Generate png file
40
+ dgp.orientation = "landscape" # Dot problem with PS orientation
41
+ dgp.write_to_file("g.ps") # Generate postscript file
42
+ ```
43
+
44
+ ###License
45
+
46
+ Copyright (c) 2001 Robert Feldt, feldt@ce.chalmers.se. All rights reserved. Distributed under GPL. See LICENSE.
data/RELEASE ADDED
@@ -0,0 +1,46 @@
1
+ 2012-07-23, LM
2
+ * Released as 0.2.0
3
+ * First version published as a gem.
4
+
5
+ 2001-11-16, RF
6
+ * Released 0.1.9
7
+ * Added DirectedGraph#strongly_connected_components and
8
+ DirectedGraph#num_vertices with tests, all written by Tanaka Akira.
9
+ Thanks Tanaka-san! (Or is it Akira-san?)
10
+ * Released 0.1.8
11
+ * Added test that negative hash values are never used in
12
+ node names. Dot can't handle them.
13
+ * Released 0.1.7
14
+ * Added "n" char in front of the node names so that they can
15
+ be used in well-formed XML docs (nums not allowed in first char).
16
+ Thanks to Tobias Reif for this one.
17
+ * Extended example in README to show how URL's can be added to nodes.
18
+
19
+ 2001-11-01, RF
20
+ * Use hash value as node number in dot files instead of id value,
21
+ since the latter is not the same for strings even though they
22
+ are equal.
23
+ * Added node attributes in the same way as edge attributes.
24
+
25
+ 2001-10-26, RF
26
+ * Released as 0.1.3.
27
+ * Redesign. Skipped DotGraph and DotGraphFormatter class and moved
28
+ everything to new class DotGRaphPrinter.
29
+ * Size does not have a default value so dot uses its default.
30
+ * Default value for orientation is "porttrait" which fits most
31
+ image output formats (except postscript!).
32
+
33
+ 2001-10-26, RF
34
+ * Release 0.1.2
35
+ * Added DotGraphFormatter#set_edge_attributes
36
+ * Added unit tests for DotGraphFormatter
37
+ * Added accessors to DotGraphFormatter for orientation and size
38
+ * Added this Changelog
39
+
40
+ 2001-10-23, RF
41
+ * Fixed minor bugs relating to file structure.
42
+
43
+ 2001-10-23, Robert Feldt (RF)
44
+ * First version 0.1.0 released.
45
+ * Created from files in rockit. Extracted since people asked for
46
+ support for GraphViz's dot.
@@ -0,0 +1,6 @@
1
+ task :test do
2
+ $:.unshift './test'
3
+ require File.basename('test/test.rb')
4
+ end
5
+
6
+ task :default => :test
@@ -0,0 +1,70 @@
1
+ class Array
2
+ def equality_uniq
3
+ uniq_elements = []
4
+ self.each {|e| uniq_elements.push(e) unless uniq_elements.index(e)}
5
+ uniq_elements
6
+ end
7
+
8
+ def delete_at_indices(indices = [])
9
+ not_deleted = Array.new
10
+ self.each_with_index {|e,i| not_deleted.push(e) if !indices.include?(i)}
11
+ not_deleted
12
+ end
13
+ end
14
+
15
+ class DefaultInitArray < Array
16
+ def initialize(*args, &initblock)
17
+ super(*args)
18
+ @initblock = initblock
19
+ end
20
+
21
+ def [](index)
22
+ super(index) || (self[index] = @initblock.call(index))
23
+ end
24
+ end
25
+
26
+ class ArrayOfArrays < DefaultInitArray
27
+ @@create_array = proc{|i| Array.new}
28
+ def initialize(*args)
29
+ super(*args, &@@create_array)
30
+ end
31
+ end
32
+
33
+ class ArrayOfHashes < DefaultInitArray
34
+ @@create_hash = proc{|i| Hash.new}
35
+ def initialize(*args)
36
+ super(*args, &@@create_hash)
37
+ end
38
+ end
39
+
40
+ # Hash which takes a block that is called to give a default value when a key
41
+ # has the value nil in the hash.
42
+ class DefaultInitHash < Hash
43
+ def initialize(*args, &initblock)
44
+ super(*args)
45
+ @initblock = initblock
46
+ end
47
+
48
+ def [](key)
49
+ super(key) || (self[key] = @initblock.call(key))
50
+ end
51
+ end
52
+
53
+ unless Object.constants.include?("TimesClass")
54
+ TimesClass = (RUBY_VERSION < "1.7") ? Time : Process
55
+ end
56
+
57
+ def time_and_puts(string, &block)
58
+ if $TIME_AND_PUTS_VERBOSE
59
+ print string; STDOUT.flush
60
+ end
61
+ starttime = [Time.new, TimesClass.times]
62
+ block.call
63
+ endtime = [Time.new, TimesClass.times]
64
+ duration = endtime[0] - starttime[0]
65
+ begin
66
+ load = [((endtime[1].utime+endtime[1].stime)-(starttime[1].utime+starttime[1].stime))/duration*100.0, 100.0].min
67
+ puts " (%.2f s %.2f%%)" % [duration, load] if $TIME_AND_PUTS_VERBOSE
68
+ rescue FloatDomainError
69
+ end
70
+ end
@@ -0,0 +1,476 @@
1
+ require 'graph/base_extensions'
2
+ require 'graph/graphviz_dot'
3
+
4
+ class HashOfHash < DefaultInitHash
5
+ def initialize(&initBlock)
6
+ super do
7
+ if initBlock
8
+ DefaultInitHash.new(&initBlock)
9
+ else
10
+ Hash.new
11
+ end
12
+ end
13
+ end
14
+ end
15
+
16
+ GraphLink = Struct.new("GraphLink", :from, :to, :info)
17
+ class GraphLink
18
+ def inspect
19
+ info_str = info ? info.inspect + "-" : ""
20
+ "#{from.inspect}-#{info_str}>#{to.inspect}"
21
+ end
22
+ end
23
+
24
+ class GraphTraversalException < Exception
25
+ attr_reader :node, :links, :link_info
26
+ def initialize(node, links, linkInfo)
27
+ @node, @links, @link_info = node, links, linkInfo
28
+ super(message)
29
+ end
30
+
31
+ def message
32
+ "There is no link from #{@node.inspect} having info #{@link_info.inspect} (valid links are #{@links.inspect})"
33
+ end
34
+ alias inspect message
35
+ end
36
+
37
+ class DirectedGraph
38
+ # This is a memory expensive variant that manages several additional
39
+ # information data structures to cut down on processing when the graph
40
+ # has been built.
41
+
42
+ attr_reader :links
43
+
44
+ def initialize
45
+ @link_map = HashOfHash.new {Array.new} # [from][to] -> array of links
46
+ @links = Array.new # All links in one array
47
+ @is_root = Hash.new # true iff root node
48
+ @is_leaf = Hash.new # true iff leaf node
49
+ end
50
+
51
+ def nodes
52
+ @is_root.keys
53
+ end
54
+
55
+ def add_node(node)
56
+ unless include_node?(node)
57
+ @is_root[node] = @is_leaf[node] = true
58
+ end
59
+ end
60
+
61
+ def root?(node)
62
+ @is_root[node]
63
+ end
64
+
65
+ def leaf?(node)
66
+ @is_leaf[node]
67
+ end
68
+
69
+ def include_node?(node)
70
+ @is_root.has_key?(node)
71
+ end
72
+
73
+ def links_from_to(from, to)
74
+ @link_map[from][to]
75
+ end
76
+
77
+ def links_from(node)
78
+ @link_map[node].map {|to, links| links}.flatten
79
+ end
80
+
81
+ def children(node)
82
+ @link_map[node].keys.select {|k| @link_map[node][k].length > 0}
83
+ end
84
+
85
+ # (Forced) add link will always add link even if there are already links
86
+ # between the nodes.
87
+ def add_link(from, to, informationOnLink = nil)
88
+ add_link_nodes(from, to)
89
+ link = GraphLink.new(from, to, informationOnLink)
90
+ links_from_to(from, to).push link
91
+ add_to_links(link)
92
+ link
93
+ end
94
+
95
+ def add_link_nodes(from, to)
96
+ add_node(from)
97
+ add_node(to)
98
+ @is_leaf[from] = @is_root[to] = false
99
+ end
100
+
101
+ # Add link if not already linked
102
+ def link_nodes(from, to, info = nil)
103
+ links_from_to?(from, to) ? nil : add_link(from, to, info)
104
+ end
105
+
106
+ def links_from_to?(from, to)
107
+ not links_from_to(from, to).empty?
108
+ end
109
+ alias linked? links_from_to?
110
+
111
+ def add_to_links(link)
112
+ @links.push link
113
+ end
114
+ private :add_to_links
115
+
116
+ def each_reachable_node_once_depth_first(node, inclusive = true, &block)
117
+ children(node).each do |c|
118
+ recurse_each_reachable_depth_first_visited(c, Hash.new, &block)
119
+ end
120
+ block.call(node) if inclusive
121
+ end
122
+ alias each_reachable_node each_reachable_node_once_depth_first
123
+
124
+ def recurse_each_reachable_depth_first_visited(node, visited, &block)
125
+ visited[node] = true
126
+ children(node).each do |c|
127
+ unless visited[c]
128
+ recurse_each_reachable_depth_first_visited(c, visited, &block)
129
+ end
130
+ end
131
+ block.call(node)
132
+ end
133
+
134
+ def each_reachable_node_once_breadth_first(node, inclusive = true, &block)
135
+ block.call(node) if inclusive
136
+ children(node).each do |c|
137
+ recurse_each_reachable_breadth_first_visited(c, Hash.new, &block)
138
+ end
139
+ end
140
+ alias each_reachable_node each_reachable_node_once_depth_first
141
+
142
+ def recurse_each_reachable_breadth_first_visited(node, visited, &block)
143
+ visited[node] = true
144
+ block.call(node)
145
+ children(node).each do |c|
146
+ unless visited[c]
147
+ recurse_each_reachable_breadth_first_visited(c, visited, &block)
148
+ end
149
+ end
150
+ end
151
+
152
+ def root_nodes
153
+ @is_root.reject {|key,val| val == false}.keys
154
+ end
155
+ alias_method :roots, :root_nodes
156
+
157
+ def leaf_nodes
158
+ @is_leaf.reject {|key,val| val == false}.keys
159
+ end
160
+ alias_method :leafs, :leaf_nodes
161
+
162
+ def internal_node?(node)
163
+ !root?(node) and !leaf?(node)
164
+ end
165
+
166
+ def internal_nodes
167
+ nodes.reject {|n| root?(n) or leaf?(n)}
168
+ end
169
+
170
+ def recurse_cyclic?(node, visited)
171
+ visited[node] = true
172
+ children(node).each do |c|
173
+ return true if visited[c] || recurse_cyclic?(c, visited)
174
+ end
175
+ false
176
+ end
177
+
178
+ def cyclic?
179
+ visited = Hash.new
180
+ root_nodes.each {|root| return true if recurse_cyclic?(root, visited)}
181
+ false
182
+ end
183
+
184
+ def acyclic?
185
+ not cyclic?
186
+ end
187
+
188
+ def transition(state, linkInfo)
189
+ link = links_from(state).detect {|l| l.info == linkInfo}
190
+ begin
191
+ link.to
192
+ rescue Exception
193
+ raise GraphTraversalException.new(state, links_from(state), linkInfo)
194
+ end
195
+ end
196
+
197
+ def traverse(fromState, alongLinksWithInfo = [])
198
+ state, len = fromState, alongLinksWithInfo.length
199
+ alongLinksWithInfo = alongLinksWithInfo.clone
200
+ while len > 0
201
+ state = transition(state, alongLinksWithInfo.shift)
202
+ len -= 1
203
+ end
204
+ state
205
+ end
206
+
207
+ def to_dot(nodeShaper = nil, nodeLabeler = nil, linkLabeler = nil)
208
+ dgp = DotGraphPrinter.new(links, nodes)
209
+ dgp.node_shaper = nodeShaper if nodeShaper
210
+ dgp.node_labeler = nodeLabeler if nodeLabeler
211
+ dgp.link_labeler = linkLabeler if linkLabeler
212
+ dgp
213
+ end
214
+
215
+ def to_postscript_file(filename, nodeShaper = nil, nodeLabeler = nil,
216
+ linkLabeler = nil)
217
+ to_dot(nodeShaper, nodeLabeler, linkLabeler).write_to_file(filename)
218
+ end
219
+
220
+ # Floyd-Warshal algorithm which should be O(n^3) where n is the number of
221
+ # nodes. We can probably work a bit on the constant factors!
222
+ def transitive_closure_floyd_warshal
223
+ vertices = nodes
224
+ tcg = DirectedGraph.new
225
+ num_nodes = vertices.length
226
+
227
+ # Direct links
228
+ for k in (0...num_nodes)
229
+ for s in (0...num_nodes)
230
+ vk, vs = vertices[k], vertices[s]
231
+ if vk == vs
232
+ tcg.link_nodes(vk,vs)
233
+ elsif linked?(vk, vs)
234
+ tcg.link_nodes(vk,vs)
235
+ end
236
+ end
237
+ end
238
+
239
+ # Indirect links
240
+ for i in (0...num_nodes)
241
+ for j in (0...num_nodes)
242
+ for k in (0...num_nodes)
243
+ vi, vj, vk = vertices[i], vertices[j], vertices[k]
244
+ if not tcg.linked?(vi,vj)
245
+ tcg.link_nodes(vi, vj) if linked?(vi,vk) and linked?(vk,vj)
246
+ end
247
+ end
248
+ end
249
+ end
250
+ tcg
251
+ end
252
+ alias_method :transitive_closure, :transitive_closure_floyd_warshal
253
+
254
+ def num_vertices
255
+ @is_root.size
256
+ end
257
+ alias num_nodes num_vertices
258
+
259
+ # strongly_connected_components uses the algorithm described in
260
+ # following paper.
261
+ # @Article{Tarjan:1972:DFS,
262
+ # author = "R. E. Tarjan",
263
+ # key = "Tarjan",
264
+ # title = "Depth First Search and Linear Graph Algorithms",
265
+ # journal = "SIAM Journal on Computing",
266
+ # volume = "1",
267
+ # number = "2",
268
+ # pages = "146--160",
269
+ # month = jun,
270
+ # year = "1972",
271
+ # CODEN = "SMJCAT",
272
+ # ISSN = "0097-5397 (print), 1095-7111 (electronic)",
273
+ # bibdate = "Thu Jan 23 09:56:44 1997",
274
+ # bibsource = "Parallel/Multi.bib, Misc/Reverse.eng.bib",
275
+ # }
276
+ def strongly_connected_components
277
+ order_cell = [0]
278
+ order_hash = {}
279
+ node_stack = []
280
+ components = []
281
+
282
+ order_hash.default = -1
283
+
284
+ nodes.each {|node|
285
+ if order_hash[node] == -1
286
+ recurse_strongly_connected_components(node, order_cell, order_hash, node_stack, components)
287
+ end
288
+ }
289
+
290
+ components
291
+ end
292
+
293
+ def recurse_strongly_connected_components(node, order_cell, order_hash, node_stack, components)
294
+ order = (order_cell[0] += 1)
295
+ reachable_minimum_order = order
296
+ order_hash[node] = order
297
+ stack_length = node_stack.length
298
+ node_stack << node
299
+
300
+ links_from(node).each {|link|
301
+ nextnode = link.to
302
+ nextorder = order_hash[nextnode]
303
+ if nextorder != -1
304
+ if nextorder < reachable_minimum_order
305
+ reachable_minimum_order = nextorder
306
+ end
307
+ else
308
+ sub_minimum_order = recurse_strongly_connected_components(nextnode, order_cell, order_hash, node_stack, components)
309
+ if sub_minimum_order < reachable_minimum_order
310
+ reachable_minimum_order = sub_minimum_order
311
+ end
312
+ end
313
+ }
314
+
315
+ if order == reachable_minimum_order
316
+ scc = node_stack[stack_length .. -1]
317
+ node_stack[stack_length .. -1] = []
318
+ components << scc
319
+ scc.each {|n|
320
+ order_hash[n] = num_vertices
321
+ }
322
+ end
323
+ return reachable_minimum_order;
324
+ end
325
+ end
326
+
327
+ # Parallel propagation in directed acyclic graphs. Should be faster than
328
+ # traversing all links from each start node if the graph is dense so that
329
+ # many traversals can be merged.
330
+ class DagPropagator
331
+ def initialize(directedGraph, startNodes, &propagationBlock)
332
+ @graph, @block = directedGraph, propagationBlock
333
+ init_start_nodes(startNodes)
334
+ @visited = Hash.new
335
+ end
336
+
337
+ def init_start_nodes(startNodes)
338
+ @startnodes = startNodes
339
+ end
340
+
341
+ def propagate
342
+ @visited.clear
343
+ propagate_recursive
344
+ end
345
+
346
+ def propagate_recursive
347
+ next_start_nodes = Array.new
348
+ @startnodes.each do |parent|
349
+ @visited[parent] = true
350
+ @graph.children(parent).each do |child|
351
+ @block.call(parent, child)
352
+ unless @visited[child] or next_start_nodes.include?(child)
353
+ next_start_nodes.push(child)
354
+ end
355
+ end
356
+ end
357
+ if next_start_nodes.length > 0
358
+ @startnodes = next_start_nodes
359
+ propagate_recursive
360
+ end
361
+ end
362
+ end
363
+
364
+ # Directed graph with fast traversal from children to parents (back)
365
+ class BackLinkedDirectedGraph < DirectedGraph
366
+ def initialize(*args)
367
+ super
368
+ @back_link_map = HashOfHash.new {Array.new} # [to][from] -> array of links
369
+ @incoming_links_info = DefaultInitHash.new {Array.new}
370
+ end
371
+
372
+ def add_link(from, to, informationOnLink = nil)
373
+ link = super
374
+ links_to_from(to, from).push link
375
+ if informationOnLink and
376
+ !@incoming_links_info[to].include?(informationOnLink)
377
+ @incoming_links_info[to].push informationOnLink
378
+ end
379
+ link
380
+ end
381
+
382
+ def incoming_links_info(node)
383
+ @incoming_links_info[node]
384
+ end
385
+
386
+ def back_transition(node, backLinkInfo)
387
+ link = links_to(node).detect {|l| l.info == backLinkInfo}
388
+ begin
389
+ link.from
390
+ rescue Exception
391
+ raise GraphTraversalException.new(node, links_to(node), backLinkInfo)
392
+ end
393
+ end
394
+
395
+ def back_traverse(state, alongLinksWithInfo = [])
396
+ len = alongLinksWithInfo.length
397
+ alongLinksWithInfo = alongLinksWithInfo.clone
398
+ while len > 0
399
+ state = back_transition(state, alongLinksWithInfo.pop)
400
+ len -= 1
401
+ end
402
+ state
403
+ end
404
+
405
+ def links_to(node)
406
+ @back_link_map[node].map {|from, links| links}.flatten
407
+ end
408
+
409
+ protected
410
+
411
+ def links_to_from(to, from)
412
+ @back_link_map[to][from]
413
+ end
414
+ end
415
+
416
+ def calc_masks(start, stop, masks = Array.new)
417
+ mask = 1 << start
418
+ (start..stop).each {|i| masks[i] = mask; mask <<= 1}
419
+ masks
420
+ end
421
+
422
+ class BooleanMatrix
423
+ def initialize(objects)
424
+ @index, @objects, @matrix = Hash.new, objects, Array.new
425
+ cnt = 0
426
+ objects.each do |o|
427
+ @index[o] = cnt
428
+ @matrix[cnt] = 0 # Use Integers to represent the booleans
429
+ cnt += 1
430
+ end
431
+ @num_obects = cnt
432
+ end
433
+
434
+ @@masks_max = 1000
435
+ @@masks = calc_masks(0,@@masks_max)
436
+
437
+ def mask(index)
438
+ mask = @@masks[index]
439
+ unless mask
440
+ calc_masks(@@masks_max+1, index, @@masks)
441
+ mask = @masks[index]
442
+ end
443
+ mask
444
+ end
445
+
446
+ def or(index1, index2)
447
+ @matrix[index1] |= @matrix[index2]
448
+ end
449
+
450
+ def indices(anInteger)
451
+ index = 0
452
+ while anInteger > 0
453
+ yeild(index) if anInteger & 1
454
+ anInteger >>= 1
455
+ index += 1
456
+ end
457
+ end
458
+
459
+ def directed_graph
460
+ dg = Directedgraph.new
461
+ @matrix.each_with_index do |v,i|
462
+ indices(v) do |index|
463
+ dg.link_nodes(@objects[i], @objects[index])
464
+ end
465
+ end
466
+ dg
467
+ end
468
+
469
+ def transitive_closure
470
+ for i in (0..@num_obects)
471
+ for j in (0..@num_obects)
472
+
473
+ end
474
+ end
475
+ end
476
+ end
@@ -0,0 +1,182 @@
1
+ class DotGraphPrinter
2
+ attr_accessor :orientation, :size, :color
3
+
4
+ # The following can be set to blocks of code that gives a default
5
+ # value for the node shapes, node labels and link labels, respectively.
6
+ attr_accessor :node_shaper, :node_labeler, :link_labeler
7
+
8
+ # A node shaper maps each node to a string describing its shape.
9
+ # Valid shapes are:
10
+ # "ellipse" (default)
11
+ # "box"
12
+ # "circle"
13
+ # "plaintext" (no outline)
14
+ # "doublecircle"
15
+ # "diamond"
16
+ # Not yet supported or untested once are:
17
+ # "polygon", "record", "epsf"
18
+ @@default_node_shaper = proc{|n| "box"}
19
+
20
+ @@default_node_labeler = proc{|n|
21
+ if Symbol===n
22
+ n.id2name
23
+ elsif String===n
24
+ n
25
+ else
26
+ n.inspect
27
+ end
28
+ }
29
+
30
+ @@default_link_labeler = proc{|info| info ? info.inspect : nil}
31
+
32
+ # links is either array of
33
+ # arrays [fromNode, toNode [, infoOnLink]], or
34
+ # objects with attributes :from, :to, :info
35
+ # nodes is array of node objects
36
+ # All nodes used in the links are used as nodes even if they are not
37
+ # in the "nodes" parameters.
38
+ def initialize(links = [], nodes = [])
39
+ @links, @nodes = links, add_nodes_in_links(links, nodes)
40
+ @node_attributes, @edge_attributes = Hash.new, Hash.new
41
+ set_default_values
42
+ end
43
+
44
+ def set_default_values
45
+ @color = "black"
46
+ @size = "9,11"
47
+ @orientation = "portrait"
48
+ @node_shaper = @@default_node_shaper
49
+ @node_labeler = @@default_node_labeler
50
+ @link_labeler = @@default_link_labeler
51
+ end
52
+
53
+ def write_to_file(filename, fileType = "ps")
54
+ dotfile = temp_filename(filename)
55
+ File.open(dotfile, "w") {|f| f.write to_dot_specification}
56
+ system "dot -T#{fileType} -o #{filename} #{dotfile}"
57
+ File.delete(dotfile)
58
+ end
59
+
60
+ def set_edge_attributes(anEdge, aHash)
61
+ # TODO check if attributes are valid dot edge attributes
62
+ edge = find_edge(anEdge)
63
+ set_attributes(edge, @edge_attributes, true, aHash)
64
+ end
65
+
66
+ def set_node_attributes(aNode, aHash)
67
+ # TODO check if attributes are valid dot node attributes
68
+ set_attributes(aNode, @node_attributes, true, aHash)
69
+ end
70
+
71
+ def to_dot_specification
72
+ set_edge_labels(@links)
73
+ set_node_labels_and_shape(@nodes)
74
+ "digraph G {\n" +
75
+ graph_parameters_to_dot_specification +
76
+ @nodes.uniq.map {|n| format_node(n)}.join(";\n") + ";\n" +
77
+ @links.uniq.map {|l| format_link(l)}.join(";\n") + ";\n" +
78
+ "}"
79
+ end
80
+
81
+ protected
82
+
83
+ def find_edge(anEdge)
84
+ @links.each do |link|
85
+ return link if source_and_dest(link) == source_and_dest(anEdge)
86
+ end
87
+ end
88
+
89
+ def set_attributes(key, hash, override, newAttributeHash)
90
+ h = hash[key] || Hash.new
91
+ newAttributeHash = all_keys_to_s(newAttributeHash)
92
+ newAttributeHash.each do |k, value|
93
+ h[k] = value unless h[k] and !override
94
+ end
95
+ hash[key] = h
96
+ end
97
+
98
+ def graph_parameters_to_dot_specification
99
+ "graph [\n" +
100
+ (self.size ? " size = #{@size.inspect},\n" : "") +
101
+ (self.orientation ? " orientation = #{@orientation},\n" : "") +
102
+ (self.color ? " color = #{@color}\n" : "") +
103
+ "]\n"
104
+ end
105
+
106
+ def each_node_in_links(links)
107
+ links.each do |l|
108
+ src, dest = source_and_dest(l)
109
+ yield src
110
+ yield dest
111
+ end
112
+ end
113
+
114
+ def add_nodes_in_links(links, nodes)
115
+ new_nodes = []
116
+ each_node_in_links(links) {|node| new_nodes.push node}
117
+ (nodes + new_nodes).uniq
118
+ end
119
+
120
+ def all_keys_to_s(aHash)
121
+ # MAYBE reuse existing hash?
122
+ Hash[*(aHash.map{|p| p[0] = p[0].to_s; p}.flatten)]
123
+ end
124
+
125
+ def set_edge_labels(edges)
126
+ edges.each do |edge|
127
+ src, dest, info = get_link_data(edge)
128
+ if info
129
+ label = @link_labeler.call(info)
130
+ set_attributes(edge, @edge_attributes, false, :label =>label) if label
131
+ end
132
+ end
133
+ end
134
+
135
+ def set_node_labels_and_shape(nodes)
136
+ nodes.each do |node|
137
+ set_attributes(node, @node_attributes, false,
138
+ :label => @node_labeler.call(node).inspect,
139
+ :shape => @node_shaper.call(node).inspect)
140
+ end
141
+ end
142
+
143
+ def get_link_data(link)
144
+ begin
145
+ return link.from, link.to, link.info
146
+ rescue Exception
147
+ return link[0], link[1], link[2]
148
+ end
149
+ end
150
+
151
+ def source_and_dest(link)
152
+ get_link_data(link)[0,2]
153
+ end
154
+
155
+ def format_attributes(attributes)
156
+ return "" unless attributes
157
+ strings = attributes.map {|a, v| "#{a}=#{v}"}
158
+ strings.length > 0 ? (" [" + strings.join(", ") + "]") : ("")
159
+ end
160
+
161
+ def mangle_node_name(node)
162
+ "n" + node.hash.abs.inspect
163
+ end
164
+
165
+ def format_link(link)
166
+ from, to, info = get_link_data(link)
167
+ mangle_node_name(from) + " -> " + mangle_node_name(to) +
168
+ format_attributes(@edge_attributes[link])
169
+ end
170
+
171
+ def format_node(node)
172
+ mangle_node_name(node) + format_attributes(@node_attributes[node])
173
+ end
174
+
175
+ def temp_filename(base = "tmp")
176
+ tmpfile = base + rand(100000).inspect
177
+ while test(?f, tmpfile)
178
+ tmpfile = base + rand(100000).inspect
179
+ end
180
+ tmpfile
181
+ end
182
+ end
@@ -0,0 +1,3 @@
1
+ class GraphR
2
+ VERSION = "0.2.0"
3
+ end
@@ -0,0 +1,215 @@
1
+ require 'graph/directed_graph'
2
+
3
+ class TestDirectedGraph < Test::Unit::TestCase
4
+ def setup
5
+ @dg = DirectedGraph.new
6
+ [[1,2],[2,3],[3,2],[2,4]].each_with_index do |(src,dest),i|
7
+ @dg.add_link(src, dest, i)
8
+ end
9
+ end
10
+
11
+ def test_initialize
12
+ assert_kind_of(DirectedGraph, @dg)
13
+ end
14
+
15
+ def test_links
16
+ assert_equals(4, @dg.links.length)
17
+ assert_equals([1,2,2,3], @dg.links.map {|l| l.from}.sort)
18
+ assert_equals([2,2,3,4], @dg.links.map {|l| l.to}.sort)
19
+ assert_equals([0,1,2,3], @dg.links.map {|l| l.info}.sort)
20
+ end
21
+
22
+ def test_nodes
23
+ assert_equals([1,2,3,4], @dg.nodes.sort)
24
+ end
25
+
26
+ def test_root?
27
+ assert_equals(true, @dg.root?(1))
28
+ assert_equals(false, @dg.root?(2))
29
+ assert_equals(false, @dg.root?(3))
30
+ assert_equals(false, @dg.root?(4))
31
+ end
32
+
33
+ def test_roots
34
+ assert_equals([1], @dg.roots)
35
+ end
36
+
37
+ def test_leaf?
38
+ assert_equals(false, @dg.leaf?(1))
39
+ assert_equals(false, @dg.leaf?(2))
40
+ assert_equals(false, @dg.leaf?(3))
41
+ assert_equals(true, @dg.leaf?(4))
42
+ end
43
+
44
+ def test_leafs
45
+ assert_equals([4], @dg.leafs)
46
+ end
47
+
48
+ def test_internal_nodes
49
+ assert_equals([2,3], @dg.internal_nodes.sort)
50
+ end
51
+
52
+ def test_acyclic?
53
+ assert_equals(false, @dg.acyclic?)
54
+
55
+ dg = DirectedGraph.new
56
+ [[1,2],[2,3],[2,4]].each {|f,t| dg.add_link(f,t)}
57
+ assert_equals(true, dg.acyclic?)
58
+ end
59
+
60
+ def test_cyclic?
61
+ assert_equals(true, @dg.cyclic?)
62
+
63
+ dg = DirectedGraph.new
64
+ [[1,2],[2,3],[2,4]].each {|f,t| dg.add_link(f,t)}
65
+ assert_equals(false, dg.cyclic?)
66
+ end
67
+
68
+ def test_each_reachable_node_once_depth_first
69
+ # Test inclusive visit
70
+ visited_nodes = Array.new
71
+ @dg.each_reachable_node_once_depth_first(1) {|n| visited_nodes.push(n)}
72
+ assert_equals(4, visited_nodes.length)
73
+ assert_equals([3,4], visited_nodes[0..1].sort)
74
+ assert_equals([2,1], visited_nodes[2..3])
75
+
76
+ # Test exclusive visit
77
+ visited_nodes.clear
78
+ @dg.each_reachable_node_once_depth_first(1, false) do |n|
79
+ visited_nodes.push(n)
80
+ end
81
+ assert_equals(3, visited_nodes.length)
82
+ assert_equals([3,4], visited_nodes[0..1].sort)
83
+ assert_equals([2], visited_nodes[2..2])
84
+ end
85
+
86
+ def test_each_reachable_node_once_breadth_first
87
+ # Test inclusive visit
88
+ visited_nodes = Array.new
89
+ @dg.each_reachable_node_once_breadth_first(1) {|n| visited_nodes.push(n)}
90
+ assert_equals(4, visited_nodes.length)
91
+ assert_equals([1,2], visited_nodes[0..1])
92
+ assert_equals([3,4], visited_nodes[2..3].sort)
93
+
94
+ # Test exclusive visit
95
+ visited_nodes.clear
96
+ @dg.each_reachable_node_once_breadth_first(1, false) do |n|
97
+ visited_nodes.push(n)
98
+ end
99
+ assert_equals(3, visited_nodes.length)
100
+ assert_equals([3,4], visited_nodes[1..2].sort)
101
+ assert_equals([2], visited_nodes[0..0])
102
+ end
103
+
104
+ def test_link_from_to
105
+ assert_equals(1, @dg.links_from_to(1,2).length)
106
+ assert_equals(1, @dg.links_from_to(2,3).length)
107
+ assert_equals(1, @dg.links_from_to(3,2).length)
108
+ assert_equals(1, @dg.links_from_to(2,4).length)
109
+ assert_equals(0, @dg.links_from_to(4,2).length)
110
+ assert_equals(0, @dg.links_from_to(4,1).length)
111
+ assert_equals(0, @dg.links_from_to(2,1).length)
112
+ assert_equals(0, @dg.links_from_to(3,1).length)
113
+ end
114
+
115
+ def test_link_from_to?
116
+ assert_equals(true, @dg.links_from_to?(1,2))
117
+ assert_equals(true, @dg.links_from_to?(2,3))
118
+ assert_equals(true, @dg.links_from_to?(3,2))
119
+ assert_equals(true, @dg.links_from_to?(2,4))
120
+ assert_equals(false, @dg.links_from_to?(2,1))
121
+ assert_equals(false, @dg.links_from_to?(3,1))
122
+ assert_equals(false, @dg.links_from_to?(4,1))
123
+ assert_equals(false, @dg.links_from_to?(4,2))
124
+ end
125
+
126
+ def test_link_nodes
127
+ len = @dg.links.length
128
+ @dg.link_nodes(1,2)
129
+ assert_equals(len, @dg.links.length)
130
+ @dg.link_nodes(1,4)
131
+ assert_equals(len+1, @dg.links.length)
132
+ end
133
+
134
+ def temp_filename(suffix = "", prefix = "tmp")
135
+ filename = prefix + rand(100000).inspect + suffix
136
+ while test(?f, filename)
137
+ filename = prefix + rand(100000).inspect + suffix
138
+ end
139
+ filename
140
+ end
141
+
142
+ def test_to_dot
143
+ d = @dg.to_dot
144
+ assert_kind_of(DotGraphPrinter, d)
145
+ d.write_to_file(filename = temp_filename(".ps"))
146
+ assert(test(?f, filename))
147
+ File.delete(filename)
148
+ end
149
+
150
+ def test_to_postscript_file
151
+ filename = temp_filename(".ps")
152
+ d = @dg.to_postscript_file(filename)
153
+ assert(test(?f, filename))
154
+ File.delete(filename)
155
+ end
156
+
157
+ def test_transition
158
+ assert_equals(2, @dg.transition(1, 0))
159
+ assert_equals(3, @dg.transition(2, 1))
160
+ assert_equals(2, @dg.transition(3, 2))
161
+ assert_equals(4, @dg.transition(2, 3))
162
+ assert_exception(GraphTraversalException) {@dg.transition(2,-1)}
163
+ end
164
+
165
+ def test_traverse
166
+ assert_equals(4, @dg.traverse(1,[0,3]))
167
+ assert_equals(3, @dg.traverse(1,[0,1]))
168
+ assert_equals(2, @dg.traverse(1,[0,1,2]))
169
+ assert_equals(4, @dg.traverse(1,a = [0,1,2,3]))
170
+ assert_equals([0,1,2,3], a)
171
+ assert_equals(1, @dg.traverse(1,[]))
172
+ assert_exception(GraphTraversalException) {@dg.traverse(1,[0,1,2,-1])}
173
+ end
174
+
175
+ DecoratedNode = Struct.new("DecoratedNode", :num, :set)
176
+
177
+ def test_propagation
178
+ dg = DirectedGraph.new
179
+ nodes = Hash.new
180
+ [1,2,3,4].each {|n| nodes[n] = DecoratedNode.new(n,[n])}
181
+ [[1,2],[2,3],[3,2],[2,4]].each_with_index do |ft,i|
182
+ dg.add_link(nodes[ft[0]], nodes[ft[1]], i)
183
+ end
184
+ prop = DagPropagator.new(dg, dg.roots) {|p, c| c.set |= p.set}
185
+ prop.propagate
186
+ nodes = dg.nodes.sort {|n1, n2| n1.num <=> n2.num}
187
+ assert_equals([1], nodes[0].set)
188
+ assert_equals([1,2,3], nodes[1].set.sort)
189
+ assert_equals([1,2,3], nodes[2].set.sort)
190
+ # Note that cycles are not correctly handled! If they were the set below
191
+ # should be [1,2,3,4]!
192
+ assert_equals([1,2,4], nodes[3].set.sort)
193
+ end
194
+
195
+ def test_transitive_closure
196
+ tcg = @dg.transitive_closure
197
+ assert_kind_of(DirectedGraph, tcg)
198
+ assert_equals([1,2,3,4], tcg.children(1).sort)
199
+ assert_equals([2,3,4], tcg.children(2).sort)
200
+ assert_equals([2,3,4], tcg.children(3).sort)
201
+ assert_equals([4], tcg.children(4).sort)
202
+ end
203
+
204
+ def test_num_vertices
205
+ assert_equals(4, @dg.num_vertices)
206
+ end
207
+
208
+ def test_strongly_connected_components
209
+ components = @dg.strongly_connected_components
210
+ assert_equal(3, components.length)
211
+ assert_equal([[4], [2, 3], [1]], components.map {|ns| ns.sort})
212
+ end
213
+ end
214
+
215
+ RUNIT::CUI::TestRunner.run(TestDirectedGraph.suite) if $0 == __FILE__
@@ -0,0 +1,140 @@
1
+ require 'graph/graphviz_dot'
2
+
3
+ class TestDotGraphPrinter < Test::Unit::TestCase
4
+ def test_initialize
5
+ dgf = DotGraphPrinter.new
6
+ assert_kind_of(DotGraphPrinter, dgf)
7
+ end
8
+
9
+ # TODO refactor
10
+ def assert_dotgraph(dotGraph,
11
+ expectedNodeAttributes = {},
12
+ expectedEdgeAttributes = {},
13
+ expectedParameters = nil)
14
+ if expectedNodeAttributes
15
+ expectedNodeAttributes =
16
+ replace_nodes_with_their_mangled_names(expectedNodeAttributes)
17
+ end
18
+ if expectedEdgeAttributes
19
+ expectedEdgeAttributes =
20
+ replace_nodes_with_their_mangled_names(expectedEdgeAttributes, true)
21
+ end
22
+ lines = dotGraph.to_dot_specification.split("\n")
23
+ assert_equals("digraph G {", lines[0])
24
+ assert_equals("}", lines[-1])
25
+ lines[1..-2].each do |line|
26
+ if line =~ /(\w+)\s*->\s*(\w+)\s*(\[.*\])?/ && expectedEdgeAttributes
27
+ source, dest = $1, $2
28
+ expected = expectedEdgeAttributes[[source, dest]]
29
+ if expected
30
+ if $3
31
+ attributes = $3[1..-2].split(/\s*,\s*/)
32
+ assert_edge_attributes(expected, attributes)
33
+ else
34
+ assert(false, "There are no attributes but we expected #{expected.inspect}")
35
+ end
36
+ end
37
+ elsif line =~ /(\w+)\s+(\[.*\])?$/ && expectedNodeAttributes
38
+ nodeid, attributes = $1, $2[1..-2].split(", ")
39
+ assert_node_attributes(expectedNodeAttributes[nodeid], attributes)
40
+ elsif line =~ /(\w+)\s*=\s*([\w\"]+)/ && expectedParameters
41
+ parameter, value = $1, $2
42
+ expected = expectedParameters[parameter]
43
+ assert_equals(expected, $2) if expected
44
+ end
45
+ end
46
+ end
47
+
48
+ def assert_edge_attributes(expected, actualAttributes)
49
+ actualAttributes.each do |attr|
50
+ attr =~ /(\w+)\s*=\s*(\w+)/
51
+ assert_equals(expected[$1], $2) if expected[$1]
52
+ end
53
+ end
54
+
55
+ def mangle_node_name(node)
56
+ "n" + node.hash.inspect
57
+ end
58
+
59
+ # The objects are referred to with their ids in the DotGraph, so we
60
+ # must map them to ids.
61
+ def replace_nodes_with_their_mangled_names(aHash, edges = false)
62
+ h = Hash.new
63
+ if edges
64
+ aHash.each do |k, val|
65
+ k[0] = mangle_node_name(k[0])
66
+ k[1] = mangle_node_name(k[1])
67
+ h[k] = val
68
+ end
69
+ else
70
+ aHash.each {|key, val| h[mangle_node_name(key)] = val}
71
+ end
72
+ h
73
+ end
74
+
75
+ def assert_node_attributes(expected, attributes)
76
+ return unless expected
77
+ attributes.each do |attr|
78
+ attr =~ /(\w+)=([\w\"]+)/
79
+ assert_equals(expected[$1], $2) if expected[$1]
80
+ end
81
+ end
82
+
83
+ def test_format
84
+ dgp = DotGraphPrinter.new([[1,2]])
85
+ assert_dotgraph(dgp, {1 => {"shape" => '"box"', "label" => '"1"'},
86
+ 2 => {"shape" => '"box"', "label" => '"2"'}})
87
+ end
88
+
89
+ def test_custom_node_shape
90
+ dgp = DotGraphPrinter.new([[1,2]])
91
+ dgp.node_shaper = proc{|n| n < 2 ? "doublecircle" : "ellipse"}
92
+ assert_dotgraph(dgp, {1 => {"shape" => '"doublecircle"', "label" => '"1"'},
93
+ 2 => {"shape" => '"ellipse"', "label" => '"2"'}})
94
+ end
95
+
96
+ def test_changing_orientation
97
+ dgp = DotGraphPrinter.new([[1,2]])
98
+ dgp.orientation = "portrait"
99
+ assert_dotgraph(dgp, nil, nil, {"orientation" => "portrait"})
100
+
101
+ dgp.orientation = "landscape"
102
+ assert_dotgraph(dgp, nil, nil, {"orientation" => "landscape"})
103
+ end
104
+
105
+ def test_setting_edge_attributes
106
+ # If we want a graph with arrows going up we can achieve this by
107
+ # * reversing all edges, and
108
+ # * adding an edge attribute "dir=back", which changes the direction
109
+ # of the arrow.
110
+ #
111
+ # This is the only way to acheive this according to the dot user manual
112
+ # in section 2.4 on page 9.
113
+ #
114
+ # Below is an example.
115
+ #
116
+ edges = [[1,2]]
117
+ reversed_edges = edges.map {|e| e.reverse}
118
+ dgp = DotGraphPrinter.new
119
+ reversed_edges.each do |edge|
120
+ dgp.set_edge_attributes(edge, "dir" => "back")
121
+ end
122
+
123
+ assert_dotgraph(dgp, nil, {[2,1] => {"dir" => "back"}})
124
+ end
125
+
126
+ def test_node_url_attribute
127
+ dgp = DotGraphPrinter.new [[1,2]]
128
+ dgp.set_node_attributes(1, :URL => "node1.html")
129
+ assert_dotgraph(dgp, {1 => {"URL" => '"node1.html"'}})
130
+ end
131
+
132
+ def test_handles_negative_hash_values
133
+ dgp = DotGraphPrinter.new [[-1,-2]]
134
+ dgp.set_node_attributes(-11, :URL => "node1.html")
135
+ assert_dotgraph(dgp, {-11 => {"URL" => '"node1.html"'}})
136
+ assert_equals(nil, dgp.to_dot_specification =~ /n-\d+/)
137
+ end
138
+ end
139
+
140
+ RUNIT::CUI::TestRunner.run(TestDotGraphPrinter.suite) if $0 == __FILE__
@@ -0,0 +1,3 @@
1
+ require 'test/unit'
2
+ $:.unshift File.join(File.dirname(__FILE__), "..", "lib")
3
+ Dir["./tests/*.rb"].each { |f| require f unless f.include?('test.rb') }
metadata ADDED
@@ -0,0 +1,59 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: graphr
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Robert Feld
9
+ - Louis Mullie
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2012-07-24 00:00:00.000000000 Z
14
+ dependencies: []
15
+ description: ! ' Graph-related Ruby classes, including a fairly extensive directed
16
+ graph class, and an interface to Graphviz'' DOT graph generator. '
17
+ email:
18
+ - feldt@ce.chalmers.se
19
+ - louis.mullie@gmail.com
20
+ executables: []
21
+ extensions: []
22
+ extra_rdoc_files: []
23
+ files:
24
+ - lib/graph/base_extensions.rb
25
+ - lib/graph/directed_graph.rb
26
+ - lib/graph/graphviz_dot.rb
27
+ - lib/graph/Rakefile
28
+ - lib/graph/version.rb
29
+ - tests/directed_graph.rb
30
+ - tests/graphviz_dot.rb
31
+ - tests/test.rb
32
+ - README.md
33
+ - LICENSE
34
+ - RELEASE
35
+ homepage: https://github.com/louismullie/graphr
36
+ licenses: []
37
+ post_install_message:
38
+ rdoc_options: []
39
+ require_paths:
40
+ - lib
41
+ required_ruby_version: !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ! '>='
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ required_rubygems_version: !ruby/object:Gem::Requirement
48
+ none: false
49
+ requirements:
50
+ - - ! '>='
51
+ - !ruby/object:Gem::Version
52
+ version: '0'
53
+ requirements: []
54
+ rubyforge_project:
55
+ rubygems_version: 1.8.24
56
+ signing_key:
57
+ specification_version: 3
58
+ summary: Graph-related Ruby classes.
59
+ test_files: []