graphmatcher 0.3.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/graphmatcher.rb +291 -0
- metadata +115 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: df74dc48331e3e6e2da3e20632fc57523fa9533de57b5c5832ca8593723875b0
|
4
|
+
data.tar.gz: af3f0b36a377af1fee96780b87758c1be26d30eca6f51595a061dc7c3388d70b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 435335f2b8c67527a6707a2c602927f76edffa4e19aef61d4a0d653ec7570c42c6043aab1c317056d861f504c4390b01b1bf8d2e3ee219f4c961d7acf4521788
|
7
|
+
data.tar.gz: 11d9c7bd8046c2aa57303b586fae4668a177f75d14201142d6cd008c8e06e5622700ab7c4cc354ead82bf79abe0fc59553614d6afbb1dfa0f667edb6ff37fb18
|
data/lib/graphmatcher.rb
ADDED
@@ -0,0 +1,291 @@
|
|
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
|
metadata
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: graphmatcher
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.3.3
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Emre Unlu
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-10-21 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.10'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.10'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 3.5.0
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 3.5.0
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: ruby-prof
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.16.2
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
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
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 0.0.7
|
83
|
+
description: An effective subgraph matching gem based on DualIso algorithm of M. Saltz
|
84
|
+
et al.
|
85
|
+
email:
|
86
|
+
- emre@eunlu.com
|
87
|
+
executables: []
|
88
|
+
extensions: []
|
89
|
+
extra_rdoc_files: []
|
90
|
+
files:
|
91
|
+
- lib/graphmatcher.rb
|
92
|
+
homepage: https://github.com/forvelin/graphmatcher
|
93
|
+
licenses:
|
94
|
+
- MIT
|
95
|
+
metadata: {}
|
96
|
+
post_install_message:
|
97
|
+
rdoc_options: []
|
98
|
+
require_paths:
|
99
|
+
- lib
|
100
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
101
|
+
requirements:
|
102
|
+
- - ">="
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: '0'
|
105
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - ">="
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
requirements: []
|
111
|
+
rubygems_version: 3.1.2
|
112
|
+
signing_key:
|
113
|
+
specification_version: 4
|
114
|
+
summary: Subgraph matching based on DualIso algorithm.
|
115
|
+
test_files: []
|