graphmatcher 0.3.4 → 0.3.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- metadata +11 -26
- data/lib/graphmatcher.rb +0 -291
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1a07b061aee6d3c909fa21e7dcc3ed815c556b358f0e233338614f97dc26d90f
|
4
|
+
data.tar.gz: ac1149e8f375be540ba8d77d77276f9d309df1dc44c7e7057eef9def8ff38be8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: be30bea5f4045cefb90288e632e43e4ea9d2472879c2e4d339fef667ddf842f863601064052e969bb1f7e6afa8f3d17daa5457ed52208e311faf76575ada5ca4
|
7
|
+
data.tar.gz: 6ce6b7bd8a89fbc44c06079c014353220ed8f5cc3e938040bb7da9eb9b2226d967c11eadef2e1fe50c95989fbfd303d637d90cb057ea948adc3c785ba48ad142
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: graphmatcher
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Emre Unlu
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-12-
|
11
|
+
date: 2020-12-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -42,44 +42,30 @@ dependencies:
|
|
42
42
|
name: rspec
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- - "
|
45
|
+
- - ">="
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version:
|
47
|
+
version: '0'
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
|
-
- - "
|
52
|
+
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version:
|
54
|
+
version: '0'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: ruby-prof
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
|
-
- - "
|
59
|
+
- - ">="
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version: 0
|
61
|
+
version: '0'
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
|
-
- - "
|
67
|
-
- !ruby/object:Gem::Version
|
68
|
-
version: 0.16.2
|
69
|
-
- !ruby/object:Gem::Dependency
|
70
|
-
name: rspec-prof
|
71
|
-
requirement: !ruby/object:Gem::Requirement
|
72
|
-
requirements:
|
73
|
-
- - "~>"
|
74
|
-
- !ruby/object:Gem::Version
|
75
|
-
version: 0.0.7
|
76
|
-
type: :development
|
77
|
-
prerelease: false
|
78
|
-
version_requirements: !ruby/object:Gem::Requirement
|
79
|
-
requirements:
|
80
|
-
- - "~>"
|
66
|
+
- - ">="
|
81
67
|
- !ruby/object:Gem::Version
|
82
|
-
version: 0
|
68
|
+
version: '0'
|
83
69
|
description: An effective subgraph matching gem based on DualIso algorithm of M. Saltz
|
84
70
|
et al.
|
85
71
|
email:
|
@@ -87,8 +73,7 @@ email:
|
|
87
73
|
executables: []
|
88
74
|
extensions: []
|
89
75
|
extra_rdoc_files: []
|
90
|
-
files:
|
91
|
-
- lib/graphmatcher.rb
|
76
|
+
files: []
|
92
77
|
homepage: https://github.com/forvelin/graphmatcher
|
93
78
|
licenses:
|
94
79
|
- MIT
|
data/lib/graphmatcher.rb
DELETED
@@ -1,291 +0,0 @@
|
|
1
|
-
require 'logger'
|
2
|
-
# require 'ruby-prof'
|
3
|
-
|
4
|
-
# @query_graph & @data_graph: Arrays which are represented as an
|
5
|
-
# array such as g=[[[1,2],[3],[3],[]],['x','y','y','z']]. g[0] is
|
6
|
-
# adjacency array which points children of given vertex of array index,
|
7
|
-
# -e.g. vertex 0 has 1 and 2 as children- and g[1] is labels for vertices.
|
8
|
-
# -e.g. vertex 0 has label 'x'.
|
9
|
-
#
|
10
|
-
# @limit: Upper limit for number of matches. Procedure terminates after
|
11
|
-
# reaching to an upper limit.
|
12
|
-
# @max_allowed_time: Maximum allowed time for procedure to run.
|
13
|
-
# @self_loops: Boolean value for representing if query is cyclic or not.
|
14
|
-
class Graphmatcher
|
15
|
-
@@logger = Logger.new(STDOUT)
|
16
|
-
@@logger.level = Logger::FATAL
|
17
|
-
|
18
|
-
def initialize(args)
|
19
|
-
@query_graph = args[:query_graph].to_a
|
20
|
-
@data_graph = args[:data_graph].to_a
|
21
|
-
@limit = (args[:limit] || 1).to_i
|
22
|
-
@max_allowed_time = (args[:max_allowed_time] || 4.000).to_f
|
23
|
-
@cost_matrix = args[:cost_matrix] || nil
|
24
|
-
@self_loops = args[:self_loops] || false
|
25
|
-
validate!
|
26
|
-
end
|
27
|
-
|
28
|
-
# Function for generating feasible matches for query
|
29
|
-
# graph based on labels of vertices of data graph.
|
30
|
-
def label_match
|
31
|
-
data_labels = @data_graph[1]
|
32
|
-
query_labels = @query_graph[1]
|
33
|
-
|
34
|
-
feasible = query_labels.map.with_index do |ql, _index|
|
35
|
-
data_labels.each_index.select { |i| data_labels[i] == ql }
|
36
|
-
end
|
37
|
-
|
38
|
-
if @cost_matrix
|
39
|
-
|
40
|
-
feasible = assess_cost(feasible, @cost_matrix)
|
41
|
-
|
42
|
-
feasible = feasible.select { |f| f[1] }.map do |feasible_set|
|
43
|
-
feasible_set.sort_by { |f| f[1] }
|
44
|
-
end
|
45
|
-
|
46
|
-
feasible = feasible.map do |f_set|
|
47
|
-
f_set.map do |f|
|
48
|
-
f[0]
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
53
|
-
@@logger.info('Label matches(phi) are: ' + feasible.to_s)
|
54
|
-
feasible
|
55
|
-
end
|
56
|
-
|
57
|
-
# Public interface for Graphmatcher class.
|
58
|
-
#
|
59
|
-
# @matches: Array of matching indices of query graph in data graph.
|
60
|
-
def find_matches
|
61
|
-
@matches = []
|
62
|
-
@t0 = Time.now.to_f
|
63
|
-
phi = label_match
|
64
|
-
|
65
|
-
dual_iso(dual_simulation(phi), 0)
|
66
|
-
# @@logger.info("FINISHED matches=#{@matches}")
|
67
|
-
if @cost_matrix
|
68
|
-
# if cost matrix is available, get costs of found matches.
|
69
|
-
|
70
|
-
@matches = assess_cost(@matches, @cost_matrix)
|
71
|
-
|
72
|
-
# sort matches by sum of costs of matched resources.
|
73
|
-
# MATCHES
|
74
|
-
# [ [[1,100],[2,10]],[[3,500],[4,800]] ]
|
75
|
-
# MATCH COSTS
|
76
|
-
# 110 1300
|
77
|
-
|
78
|
-
# The behaviour here is important !
|
79
|
-
# Sum of costs vs. max of costs, depends which one is relevant.
|
80
|
-
|
81
|
-
@matches.reject! { |match_set| match_set.map { |e| e[1] }.include?(nil) }
|
82
|
-
|
83
|
-
@matches = @matches.sort_by do |match_set|
|
84
|
-
match_set.reduce(0) { |sum, e| sum + e[1] }
|
85
|
-
end
|
86
|
-
|
87
|
-
end
|
88
|
-
@matches
|
89
|
-
end
|
90
|
-
|
91
|
-
def get_resource_property(_match, property)
|
92
|
-
truncated_data_graph = @data_graph.truncate
|
93
|
-
@matches.map { |match_set| match_set.map { |match| truncated_data_graph[match[0]][2][property] } }
|
94
|
-
end
|
95
|
-
|
96
|
-
def get_resource_cost(costs, resource_position, query_index)
|
97
|
-
# costs = { resource_id => { query_index => cost } }
|
98
|
-
# e.g.
|
99
|
-
# costs = { 40 => { 0 => 5, 1 => 10 } }
|
100
|
-
if costs[resource_position][query_index]
|
101
|
-
cost = (costs[resource_position][query_index])
|
102
|
-
cost
|
103
|
-
end
|
104
|
-
end
|
105
|
-
|
106
|
-
def assess_cost(matches, costs)
|
107
|
-
# resource_graph =
|
108
|
-
# [
|
109
|
-
# [[],[],[],[],[],[],[]], #adj.
|
110
|
-
# ['SPO2','NRF52','SPO2','NRF52','SPO2','NRF52','SPO2'], #types
|
111
|
-
# ['img_x','img_y','img_z','img_t','img_z','img_q','img_z'], #images
|
112
|
-
# ['12','52','25','61','74','95','11'] #resource_id
|
113
|
-
# ]
|
114
|
-
|
115
|
-
# request_graph =
|
116
|
-
# [
|
117
|
-
# [[],[]],
|
118
|
-
# ['NRF52','NRF52'],
|
119
|
-
# ['img_y','img_z'],
|
120
|
-
# ['NODE_A','HUB_A']
|
121
|
-
# ]
|
122
|
-
|
123
|
-
# costs = {
|
124
|
-
# 52 => {0 => 0, 1 => 50} , #y
|
125
|
-
# 61 => {0 => 30, 1 => 70} , #t
|
126
|
-
# 95 => {0 => 40, 1 => 55} , #q
|
127
|
-
# }
|
128
|
-
|
129
|
-
# matches = [
|
130
|
-
# [[1],[2]],[[1],[4]],[[1],[6]],
|
131
|
-
# [[3],[2]],[[3],[4]],[[3],[6]],
|
132
|
-
# [[5],[2]],[[5],[4]],[[5],[6]]
|
133
|
-
# ]
|
134
|
-
|
135
|
-
matches.map do |match_set| # [ [1],[2] ]
|
136
|
-
match_set.flatten.map.with_index do |match, query_index| # 1
|
137
|
-
[match, get_resource_cost(costs, match, query_index).to_f]
|
138
|
-
end
|
139
|
-
end
|
140
|
-
end
|
141
|
-
|
142
|
-
# INFO: Function that uses parameter phi -which is generated by label_match-
|
143
|
-
# to determine which matches of data have expected relations in query graph.
|
144
|
-
# phi = ...
|
145
|
-
def dual_simulation(phi)
|
146
|
-
# One directional adjacency array for data graph and query graphs.
|
147
|
-
data_children = @data_graph[0]
|
148
|
-
query_children = @query_graph[0]
|
149
|
-
# @@logger.info("Data children: #{data_children.to_s}")
|
150
|
-
# @@logger.info("Query children: #{query_children.to_s}")
|
151
|
-
changed = true
|
152
|
-
while changed
|
153
|
-
changed = false
|
154
|
-
return nil if (Time.now.to_f - @t0) > @max_allowed_time
|
155
|
-
|
156
|
-
# children = query_edges
|
157
|
-
# q_index = query_vertex_index
|
158
|
-
query_children.each_with_index do |children, q_index|
|
159
|
-
# query_child = query_edge_target
|
160
|
-
children.each do |query_child|
|
161
|
-
# Create a temporary phi object.
|
162
|
-
temp_phi = []
|
163
|
-
# Loop over candidates of each vertex in data graph.
|
164
|
-
to_delete = []
|
165
|
-
|
166
|
-
phi[q_index].map do |child| # loop 3
|
167
|
-
# @@logger.debug("u=#{q_index}, u_c=#{query_child}, child=#{child}")
|
168
|
-
|
169
|
-
# Find intersection of children of 'child' in data graph and
|
170
|
-
# candidates of 'query child' in data graph.
|
171
|
-
phi_intersection = data_children[child] & phi[query_child]
|
172
|
-
# @@logger.debug("datachildren[child]=#{data_children[child]}")
|
173
|
-
# @@logger.debug("phi[query_child]=#{phi[query_child]}")
|
174
|
-
# @@logger.debug("Intersection=#{phi_intersection}")
|
175
|
-
if phi_intersection.nil? || phi_intersection.empty?
|
176
|
-
to_delete.push(child)
|
177
|
-
return phi if phi[q_index].empty?
|
178
|
-
|
179
|
-
changed = true
|
180
|
-
end
|
181
|
-
temp_phi |= phi_intersection
|
182
|
-
end
|
183
|
-
|
184
|
-
unless to_delete.empty?
|
185
|
-
to_delete.each do |td|
|
186
|
-
phi[q_index].delete(td)
|
187
|
-
end
|
188
|
-
end
|
189
|
-
return phi if temp_phi.flatten.empty?
|
190
|
-
|
191
|
-
changed = true if temp_phi.size < phi[query_child].size
|
192
|
-
if @self_loops && query_child == q_index
|
193
|
-
phi[query_child] &= temp_phi
|
194
|
-
else
|
195
|
-
# @@logger.debug("phi=#{phi} and phi[#{query_child}]=#{temp_phi}")
|
196
|
-
phi[query_child] = temp_phi
|
197
|
-
end
|
198
|
-
end
|
199
|
-
end
|
200
|
-
end
|
201
|
-
@@logger.info("Returning phi=#{phi}")
|
202
|
-
phi
|
203
|
-
end
|
204
|
-
|
205
|
-
# INFO: Function call to collect matches from phi object.
|
206
|
-
# phi = ...
|
207
|
-
# depth = ...
|
208
|
-
# matches = ...
|
209
|
-
def dual_iso(phi, depth)
|
210
|
-
if depth == @query_graph[0].length
|
211
|
-
unless phi.nil? || phi.empty?
|
212
|
-
@matches <<
|
213
|
-
if phi.include?([]) # Unable to match this vertex in graph.
|
214
|
-
[nil]
|
215
|
-
else
|
216
|
-
phi
|
217
|
-
end
|
218
|
-
end
|
219
|
-
elsif !(phi.nil? || phi.empty?)
|
220
|
-
phi[depth].sort_by { |value| @cost_matrix ? (@cost_matrix[value][depth] || Float::INFINITY) : next }.each do |value|
|
221
|
-
next if contains(phi, depth, value)
|
222
|
-
|
223
|
-
# keys are indices 0...n, values are possible values for that index
|
224
|
-
phicopy = phi.map(&:clone)
|
225
|
-
# @@logger.info("phicopy=#{phicopy},depth=#{depth},value=#{value}")
|
226
|
-
phicopy[depth] = [value]
|
227
|
-
if @matches.length >= @limit
|
228
|
-
@@logger.info("FINISHED matches=#{@matches}")
|
229
|
-
return @matches
|
230
|
-
end
|
231
|
-
dual_iso(dual_simulation(phicopy), depth + 1)
|
232
|
-
end
|
233
|
-
end
|
234
|
-
end
|
235
|
-
|
236
|
-
# INFO: Checks if vertex J is contained in any of previous matches.
|
237
|
-
# TODO: Change with find method.
|
238
|
-
def contains(phi, depth, vertex_j)
|
239
|
-
false if depth <= 0
|
240
|
-
(0..depth - 1).each do |i|
|
241
|
-
# @@logger.info("phi[#{i}]=#{phi[i]},depth=#{depth},vertex_j=#{vertex_j}")
|
242
|
-
return true if phi[i].include?(vertex_j)
|
243
|
-
end
|
244
|
-
false
|
245
|
-
end
|
246
|
-
|
247
|
-
# EXPERIMENTAL
|
248
|
-
# INFO: Produce a GraphViz-compliant directed graph syntax.
|
249
|
-
# INFO: Needs dot/graphviz tools installed as a dependency.
|
250
|
-
# TODO: Unable to handle multiple results, color each result different.
|
251
|
-
# Indices are IDs, labels are labels adjencency array is outgoing edges.
|
252
|
-
def dot_graph(data, subgraph = nil, prefix = '')
|
253
|
-
output = ['digraph {']
|
254
|
-
data.transpose.each_with_index do |node, id|
|
255
|
-
output <<
|
256
|
-
["#{id} [label=\"#{node[1]}##{id}\"]",
|
257
|
-
"#{id}->{#{node[0].join(' ')}}"].join("\n")
|
258
|
-
end
|
259
|
-
if subgraph
|
260
|
-
subgraph.each_with_index do |node, _id|
|
261
|
-
output << "#{node} [fontcolor=\"Red\"]"
|
262
|
-
end
|
263
|
-
end
|
264
|
-
output << '}'
|
265
|
-
tstamp = Time.new.to_i.to_s
|
266
|
-
File.write("#{prefix}#{tstamp}.dot", output.join("\n"))
|
267
|
-
dot_produce = ['dot', '-Tpng', "#{prefix}#{tstamp}.dot",
|
268
|
-
'-o', "#{prefix}#{tstamp}.png"].join(' ')
|
269
|
-
`#{dot_produce}`
|
270
|
-
end
|
271
|
-
|
272
|
-
def validate!
|
273
|
-
unless @query_graph.is_a?(Array) && @data_graph.is_a?(Array)
|
274
|
-
raise ArgumentError,
|
275
|
-
'Type mismatch for graphs in initialization !'
|
276
|
-
end
|
277
|
-
unless @limit.is_a?(Numeric) && @max_allowed_time.is_a?(Numeric)
|
278
|
-
raise ArgumentError,
|
279
|
-
'Type mismatch for limit or timeout value in initialization !'
|
280
|
-
end
|
281
|
-
unless @query_graph.length >= 2 && @data_graph.length >= 2
|
282
|
-
raise ArgumentError,
|
283
|
-
'Input graphs must have at least two dimensions !'
|
284
|
-
end
|
285
|
-
unless @query_graph.map(&:length).uniq.size == 1 &&
|
286
|
-
@data_graph.map(&:length).uniq.size == 1
|
287
|
-
raise ArgumentError,
|
288
|
-
'Input graphs\' adjencency and label arrays must be sized equal !'
|
289
|
-
end
|
290
|
-
end
|
291
|
-
end
|