graphr 0.2.0

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.
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: []