graphmatcher 0.3.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/graphmatcher.rb +291 -0
  3. metadata +115 -0
@@ -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
@@ -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: []