ruby-graphviz 0.9.16 → 0.9.17
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/AUTHORS +1 -0
- data/README.rdoc +64 -0
- data/examples/graphml/attributes.ext.graphml +12 -0
- data/examples/graphml/attributes.graphml +40 -0
- data/examples/graphml/cluster.graphml +75 -0
- data/examples/graphml/hyper.graphml +29 -0
- data/examples/graphml/nested.graphml +54 -0
- data/examples/graphml/port.graphml +32 -0
- data/examples/graphml/simple.graphml +30 -0
- data/examples/sample57.rb +6 -0
- data/examples/theory/PERT.png +0 -0
- data/examples/theory/matrix.png +0 -0
- data/examples/theory/pert.rb +47 -0
- data/examples/theory/tests.rb +81 -0
- data/lib/graphviz.rb +45 -8
- data/lib/graphviz/constants.rb +43 -40
- data/lib/graphviz/edge.rb +22 -5
- data/lib/graphviz/elements.rb +39 -0
- data/lib/graphviz/graphml.rb +218 -0
- data/lib/graphviz/math/matrix.rb +221 -0
- data/lib/graphviz/node.rb +11 -0
- data/lib/graphviz/theory.rb +252 -0
- data/lib/graphviz/types.rb +4 -0
- data/lib/graphviz/types/gv_double.rb +20 -0
- metadata +24 -4
@@ -0,0 +1,221 @@
|
|
1
|
+
class GraphViz
|
2
|
+
class Math
|
3
|
+
def self.Matrix( line, column = 0, val = 0 )
|
4
|
+
GraphViz::Math::Matrix.new(line, column, val)
|
5
|
+
end
|
6
|
+
|
7
|
+
class CoordinateError < RuntimeError
|
8
|
+
end
|
9
|
+
class ValueError < RuntimeError
|
10
|
+
end
|
11
|
+
|
12
|
+
class Matrix
|
13
|
+
def initialize( line_or_array, column = 0, val = 0 )
|
14
|
+
if line_or_array.kind_of?(Array)
|
15
|
+
line = line_or_array.size
|
16
|
+
column = nil
|
17
|
+
line_or_array.size.times do |l|
|
18
|
+
unless line_or_array[l].kind_of?(Array)
|
19
|
+
raise ArgumentError, "Wrong matrix definition"
|
20
|
+
end
|
21
|
+
column = line_or_array[l].size if column.nil?
|
22
|
+
unless line_or_array[l].size == column
|
23
|
+
raise ArgumentError, "Wrong matrix definition"
|
24
|
+
end
|
25
|
+
line_or_array[l].size.times do |c|
|
26
|
+
unless line_or_array[l][c].kind_of?(Numeric)
|
27
|
+
raise ValueError, "Element at [#{l+1}, #{c+1}] is not a number"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
@matrix = line_or_array
|
33
|
+
@line = line
|
34
|
+
@column = column
|
35
|
+
elsif line_or_array.kind_of?(Integer) and column > 0
|
36
|
+
@matrix = Array.new(line_or_array)
|
37
|
+
@matrix.size.times do |l|
|
38
|
+
@matrix[l] = Array.new(column)
|
39
|
+
@matrix[l].size.times do |c|
|
40
|
+
@matrix[l][c] = val
|
41
|
+
end
|
42
|
+
end
|
43
|
+
@line = line_or_array
|
44
|
+
@column = column
|
45
|
+
else
|
46
|
+
raise ArgumentError, "Wrong matrix definition"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def [](line, column)
|
51
|
+
unless (0...@line).to_a.include?(line-1)
|
52
|
+
raise CoordinateError, "Line out of range (#{line} for 1..#{@line})!"
|
53
|
+
end
|
54
|
+
unless (0...@column).to_a.include?(column-1)
|
55
|
+
raise CoordinateError, "Column out of range (#{column} for 1..#{@column})!"
|
56
|
+
end
|
57
|
+
@matrix[line-1][column-1]
|
58
|
+
end
|
59
|
+
|
60
|
+
def []=( line, column, val )
|
61
|
+
unless (0...@line).to_a.include?(line-1)
|
62
|
+
raise CoordinateError, "Line out of range (#{line} for 1..#{@line})!"
|
63
|
+
end
|
64
|
+
unless (0...@column).to_a.include?(column-1)
|
65
|
+
raise CoordinateError, "Column out of range (#{column} for 1..#{@column})!"
|
66
|
+
end
|
67
|
+
@matrix[line-1][column-1] = val
|
68
|
+
end
|
69
|
+
|
70
|
+
def matrix
|
71
|
+
@matrix
|
72
|
+
end
|
73
|
+
alias :to_a :matrix
|
74
|
+
|
75
|
+
def columns
|
76
|
+
@column
|
77
|
+
end
|
78
|
+
|
79
|
+
def lines
|
80
|
+
@line
|
81
|
+
end
|
82
|
+
|
83
|
+
def to_s
|
84
|
+
size = bigger
|
85
|
+
out = ""
|
86
|
+
@line.times do |line|
|
87
|
+
out << "["
|
88
|
+
@column.times do |column|
|
89
|
+
out << sprintf(" %1$*2$s", @matrix[line][column].to_s, size)
|
90
|
+
end
|
91
|
+
out << "]\n"
|
92
|
+
end
|
93
|
+
return out
|
94
|
+
end
|
95
|
+
|
96
|
+
def -(m)
|
97
|
+
matrix = GraphViz::Math::Matrix.new( @line, @column )
|
98
|
+
@line.times do |line|
|
99
|
+
@column.times do |column|
|
100
|
+
matrix[line+1, column+1] = self[line+1, column+1] - m[line+1, column+1]
|
101
|
+
end
|
102
|
+
end
|
103
|
+
return matrix
|
104
|
+
end
|
105
|
+
|
106
|
+
def *(m)
|
107
|
+
matrix = GraphViz::Math::Matrix.new( @line, @line )
|
108
|
+
|
109
|
+
@line.times do |line|
|
110
|
+
@line.times do |column|
|
111
|
+
l = self.line(line+1)
|
112
|
+
c = m.column(column+1)
|
113
|
+
v = 0
|
114
|
+
l.size.times do |i|
|
115
|
+
v += l[i]*c[i]
|
116
|
+
end
|
117
|
+
matrix[line+1,column+1] = v
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
return matrix
|
122
|
+
end
|
123
|
+
|
124
|
+
def line( line )
|
125
|
+
unless (0...@line).to_a.include?(line-1)
|
126
|
+
raise CoordinateError, "Line out of range (#{line} for 1..#{@line})!"
|
127
|
+
end
|
128
|
+
@matrix[line-1]
|
129
|
+
end
|
130
|
+
|
131
|
+
def column( column )
|
132
|
+
col = []
|
133
|
+
unless (0...@column).to_a.include?(column-1)
|
134
|
+
raise CoordinateError, "Column out of range (#{column} for 1..#{@column})!"
|
135
|
+
end
|
136
|
+
@line.times do |line|
|
137
|
+
col << self[line+1, column]
|
138
|
+
end
|
139
|
+
|
140
|
+
return col
|
141
|
+
end
|
142
|
+
|
143
|
+
def transpose
|
144
|
+
matrix = GraphViz::Math::Matrix.new( @column, @line )
|
145
|
+
@line.times do |line|
|
146
|
+
@column.times do |column|
|
147
|
+
matrix[column+1, line+1] = self[line+1, column+1]
|
148
|
+
end
|
149
|
+
end
|
150
|
+
return matrix
|
151
|
+
end
|
152
|
+
|
153
|
+
def ==(m)
|
154
|
+
equal = true
|
155
|
+
@line.times do |line|
|
156
|
+
@column.times do |column|
|
157
|
+
equal &&= (m[line+1, column+1] == self[line+1, column+1])
|
158
|
+
end
|
159
|
+
end
|
160
|
+
return equal
|
161
|
+
end
|
162
|
+
|
163
|
+
def remove_line(n)
|
164
|
+
unless (0...@line).to_a.include?(n-1)
|
165
|
+
raise CoordinateError, "Line out of range (#{n} for 1..#{@line})!"
|
166
|
+
end
|
167
|
+
|
168
|
+
matrix = GraphViz::Math::Matrix.new( @line - 1, @column )
|
169
|
+
nline = 0
|
170
|
+
@line.times do |line|
|
171
|
+
next if line == n - 1
|
172
|
+
@column.times do |column|
|
173
|
+
matrix[nline+1, column+1] = self[line+1, column+1]
|
174
|
+
end
|
175
|
+
nline += 1
|
176
|
+
end
|
177
|
+
return matrix
|
178
|
+
end
|
179
|
+
|
180
|
+
def remove_column(n)
|
181
|
+
unless (0...@column).to_a.include?(n-1)
|
182
|
+
raise CoordinateError, "Column out of range (#{n} for 1..#{@column})!"
|
183
|
+
end
|
184
|
+
|
185
|
+
matrix = GraphViz::Math::Matrix.new( @line, @column - 1 )
|
186
|
+
@line.times do |line|
|
187
|
+
ncolumn = 0
|
188
|
+
@column.times do |column|
|
189
|
+
next if column == n - 1
|
190
|
+
matrix[line+1, ncolumn+1] = self[line+1, column+1]
|
191
|
+
ncolumn += 1
|
192
|
+
end
|
193
|
+
end
|
194
|
+
return matrix
|
195
|
+
end
|
196
|
+
|
197
|
+
def sum_of_column(n)
|
198
|
+
column(n).inject(0){|sum,item| sum + item}
|
199
|
+
end
|
200
|
+
|
201
|
+
def sum_of_line(n)
|
202
|
+
line(n).inject(0){|sum,item| sum + item}
|
203
|
+
end
|
204
|
+
|
205
|
+
def set_matrix(m) #:nodoc:
|
206
|
+
@matrix = m
|
207
|
+
end
|
208
|
+
|
209
|
+
private
|
210
|
+
def bigger
|
211
|
+
b = 0
|
212
|
+
@matrix.each do |line|
|
213
|
+
line.each do |column|
|
214
|
+
b = column.to_s.size if column.to_s.size > b
|
215
|
+
end
|
216
|
+
end
|
217
|
+
return b
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
data/lib/graphviz/node.rb
CHANGED
@@ -35,6 +35,7 @@ class GraphViz
|
|
35
35
|
@xNodeName = xNodeName
|
36
36
|
@oGParrent = oGParrent
|
37
37
|
@oAttrNode = GraphViz::Attrs::new( nil, "node", NODESATTRS )
|
38
|
+
@index = nil
|
38
39
|
end
|
39
40
|
|
40
41
|
#
|
@@ -51,6 +52,16 @@ class GraphViz
|
|
51
52
|
def id
|
52
53
|
@xNodeName.clone
|
53
54
|
end
|
55
|
+
|
56
|
+
#
|
57
|
+
# Return the index of the node
|
58
|
+
#
|
59
|
+
def index
|
60
|
+
@index
|
61
|
+
end
|
62
|
+
def index=(i) #:nodoc:
|
63
|
+
@index = i if @index == nil
|
64
|
+
end
|
54
65
|
|
55
66
|
#
|
56
67
|
# Set value +xAttrValue+ to the node attribut +xAttrName+
|
@@ -0,0 +1,252 @@
|
|
1
|
+
require 'graphviz/math/matrix'
|
2
|
+
|
3
|
+
class GraphViz
|
4
|
+
class Theory
|
5
|
+
def initialize( graph )
|
6
|
+
@graph = graph
|
7
|
+
end
|
8
|
+
|
9
|
+
#
|
10
|
+
# Return the adjancy matrix of the graph
|
11
|
+
#
|
12
|
+
def adjancy_matrix
|
13
|
+
matrix = GraphViz::Math::Matrix.new( @graph.node_count, @graph.node_count )
|
14
|
+
|
15
|
+
@graph.each_edge { |e|
|
16
|
+
x = @graph.get_node(e.node_one( false )).index
|
17
|
+
y = @graph.get_node(e.node_two( false )).index
|
18
|
+
matrix[x+1, y+1] = 1
|
19
|
+
matrix[y+1, x+1] = 1 if @graph.type == "graph"
|
20
|
+
}
|
21
|
+
|
22
|
+
return matrix
|
23
|
+
end
|
24
|
+
|
25
|
+
#
|
26
|
+
# Return the incidence matrix of the graph
|
27
|
+
#
|
28
|
+
def incidence_matrix
|
29
|
+
tail = (@graph.type == "digraph") ? -1 : 1
|
30
|
+
matrix = GraphViz::Math::Matrix.new( @graph.node_count, @graph.edge_count )
|
31
|
+
|
32
|
+
@graph.each_edge { |e|
|
33
|
+
x = e.index
|
34
|
+
|
35
|
+
nstart = @graph.get_node(e.node_one( false )).index
|
36
|
+
nend = @graph.get_node(e.node_two( false )).index
|
37
|
+
|
38
|
+
matrix[nstart+1, x+1] = 1
|
39
|
+
matrix[nend+1, x+1] = tail
|
40
|
+
matrix[nend+1, x+1] = 2 if nstart == nend
|
41
|
+
}
|
42
|
+
|
43
|
+
return matrix
|
44
|
+
end
|
45
|
+
|
46
|
+
#
|
47
|
+
# Return the degree of the given node
|
48
|
+
#
|
49
|
+
def degree( node )
|
50
|
+
degree = 0
|
51
|
+
name = node
|
52
|
+
if node.kind_of?(GraphViz::Node)
|
53
|
+
name = node.id
|
54
|
+
end
|
55
|
+
|
56
|
+
@graph.each_edge do |e|
|
57
|
+
degree += 1 if e.node_one(false) == name or e.node_two(false) == name
|
58
|
+
end
|
59
|
+
|
60
|
+
return degree
|
61
|
+
end
|
62
|
+
|
63
|
+
#
|
64
|
+
# Return the laplacian matrix of the graph
|
65
|
+
#
|
66
|
+
def laplacian_matrix
|
67
|
+
return degree_matrix - adjancy_matrix
|
68
|
+
end
|
69
|
+
|
70
|
+
#
|
71
|
+
# Return <tt>true</tt> if the graph if symmetric, <tt>false</tt> otherwise
|
72
|
+
#
|
73
|
+
def symmetric?
|
74
|
+
adjancy_matrix == adjancy_matrix.transpose
|
75
|
+
end
|
76
|
+
|
77
|
+
#
|
78
|
+
# moore_dijkstra(source, destination)
|
79
|
+
#
|
80
|
+
def moore_dijkstra( dep, arv )
|
81
|
+
dep = @graph.get_node(dep) unless dep.kind_of?(GraphViz::Node)
|
82
|
+
arv = @graph.get_node(arv) unless arv.kind_of?(GraphViz::Node)
|
83
|
+
|
84
|
+
m = distance_matrix
|
85
|
+
n = @graph.node_count
|
86
|
+
# Table des sommets à choisir
|
87
|
+
c = [dep.index]
|
88
|
+
# Table des distances
|
89
|
+
d = []
|
90
|
+
d[dep.index] = 0
|
91
|
+
|
92
|
+
# Table des predecesseurs
|
93
|
+
pred = []
|
94
|
+
|
95
|
+
@graph.each_node do |name, k|
|
96
|
+
if k != dep
|
97
|
+
d[k.index] = m[dep.index+1,k.index+1]
|
98
|
+
pred[k.index] = dep
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
while c.size < n
|
103
|
+
# trouver y tel que d[y] = min{d[k]; k sommet tel que k n'appartient pas à c}
|
104
|
+
mini = 1.0/0.0
|
105
|
+
y = nil
|
106
|
+
@graph.each_node do |name, k|
|
107
|
+
next if c.include?(k.index)
|
108
|
+
if d[k.index] < mini
|
109
|
+
mini = d[k.index]
|
110
|
+
y = k
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# si ce minimum est ∞ alors sortir de la boucle fin si
|
115
|
+
break unless mini.to_f.infinite?.nil?
|
116
|
+
|
117
|
+
c << y.index
|
118
|
+
@graph.each_node do |name, k|
|
119
|
+
next if c.include?(k.index)
|
120
|
+
if d[k.index] > d[y.index] + m[y.index+1,k.index+1]
|
121
|
+
d[k.index] = d[y.index] + m[y.index+1,k.index+1]
|
122
|
+
pred[k.index] = y
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# Construction du chemin le plus court
|
128
|
+
ch = []
|
129
|
+
k = arv
|
130
|
+
while k.index != dep.index
|
131
|
+
ch.unshift(k.id)
|
132
|
+
k = pred[k.index]
|
133
|
+
end
|
134
|
+
ch.unshift(dep.id)
|
135
|
+
|
136
|
+
if d[arv.index].to_f.infinite?
|
137
|
+
return nil
|
138
|
+
else
|
139
|
+
return( {
|
140
|
+
:path => ch,
|
141
|
+
:distance => d[arv.index]
|
142
|
+
} )
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
#
|
147
|
+
# Return a liste of range
|
148
|
+
#
|
149
|
+
# If the returned array include nil values, there is one or more circuits in the graph
|
150
|
+
#
|
151
|
+
def range
|
152
|
+
matrix = adjancy_matrix
|
153
|
+
unseen = (1..matrix.columns).to_a
|
154
|
+
result = Array.new(matrix.columns)
|
155
|
+
r = 0
|
156
|
+
|
157
|
+
range_recursion( matrix, unseen, result, r )
|
158
|
+
end
|
159
|
+
|
160
|
+
#
|
161
|
+
# Return the critical path for a PERT network
|
162
|
+
#
|
163
|
+
# If the given graph is not a PERT network, return nul
|
164
|
+
#
|
165
|
+
def critical_path
|
166
|
+
return nil if range.include?(nil) or @graph.type != "digraph"
|
167
|
+
r = [ [0, [1]] ]
|
168
|
+
|
169
|
+
critical_path_recursion( distance_matrix, adjancy_matrix, r, [], 0 ).inject( {:distance => 0, :path => []} ) { |r, item|
|
170
|
+
(r[:distance] < item[0]) ? { :distance => item[0], :path => item[1] } : r
|
171
|
+
}
|
172
|
+
end
|
173
|
+
|
174
|
+
private
|
175
|
+
def distance_matrix
|
176
|
+
type = @graph.type
|
177
|
+
matrix = GraphViz::Math::Matrix.new( @graph.node_count, @graph.node_count, (1.0/0.0) )
|
178
|
+
|
179
|
+
@graph.each_edge { |e|
|
180
|
+
x = @graph.get_node(e.node_one( false )).index
|
181
|
+
y = @graph.get_node(e.node_two( false )).index
|
182
|
+
unless x == y
|
183
|
+
weight = ((e[:weight].nil?) ? 1 : e[:weight].to_f)
|
184
|
+
matrix[x+1, y+1] = weight
|
185
|
+
matrix[y+1, x+1] = weight if type == "graph"
|
186
|
+
end
|
187
|
+
}
|
188
|
+
|
189
|
+
return matrix
|
190
|
+
end
|
191
|
+
|
192
|
+
def degree_matrix
|
193
|
+
matrix = GraphViz::Math::Matrix.new( @graph.node_count, @graph.node_count )
|
194
|
+
@graph.each_node do |name, node|
|
195
|
+
i = node.index
|
196
|
+
matrix[i+1, i+1] = degree(node)
|
197
|
+
end
|
198
|
+
return matrix
|
199
|
+
end
|
200
|
+
|
201
|
+
def range_recursion(matrix, unseen, result, r)
|
202
|
+
remove = []
|
203
|
+
matrix.columns.times do |c|
|
204
|
+
if matrix.sum_of_column(c+1) == 0
|
205
|
+
result[unseen[c]-1] = r
|
206
|
+
remove.unshift( c + 1 )
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
remove.each do |rem|
|
211
|
+
matrix = matrix.remove_line(rem).remove_column(rem)
|
212
|
+
unseen.delete_at(rem-1)
|
213
|
+
end
|
214
|
+
|
215
|
+
if matrix.columns == 1 and matrix.lines == 1
|
216
|
+
if matrix.sum_of_column(1) == 0
|
217
|
+
result[unseen[0]-1] = r+1
|
218
|
+
end
|
219
|
+
elsif remove.size > 0
|
220
|
+
range_recursion( matrix, unseen, result, r+1 )
|
221
|
+
end
|
222
|
+
|
223
|
+
return result
|
224
|
+
end
|
225
|
+
|
226
|
+
def index_of_item( array, item )
|
227
|
+
array.inject( [0,[]] ){|a,i|
|
228
|
+
a[1] << a[0] if i == item
|
229
|
+
a[0] += 1
|
230
|
+
a
|
231
|
+
}[1]
|
232
|
+
end
|
233
|
+
|
234
|
+
def critical_path_recursion( d, a, r, result, level )
|
235
|
+
r.each do |p|
|
236
|
+
node = p[1][-1]
|
237
|
+
index_of_item( a.line(node), 1 ).each do |c|
|
238
|
+
succ = c+1
|
239
|
+
|
240
|
+
cpath = [ (p[0] + d[node,succ]), (p[1].clone << succ) ]
|
241
|
+
|
242
|
+
if index_of_item( a.line(succ), 1 ).size > 0
|
243
|
+
capth = critical_path_recursion( d, a, [cpath], result, level+1 )
|
244
|
+
else
|
245
|
+
result << cpath
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
return result
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|