graphmatcher 0.3.4 → 0.3.5
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.
- 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
|