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.
Files changed (3) hide show
  1. checksums.yaml +4 -4
  2. metadata +11 -26
  3. data/lib/graphmatcher.rb +0 -291
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 16aafb8ddede700354cfac5795c95c8f16ecf3c4076b082d4e409b7b211d4310
4
- data.tar.gz: 742e07a51018dae3dc9de0b046a82e9f35b39825784c9f126964685f77366e3e
3
+ metadata.gz: 1a07b061aee6d3c909fa21e7dcc3ed815c556b358f0e233338614f97dc26d90f
4
+ data.tar.gz: ac1149e8f375be540ba8d77d77276f9d309df1dc44c7e7057eef9def8ff38be8
5
5
  SHA512:
6
- metadata.gz: 16f27d89dae6474a68ec4ed01c800f0047f9f5f46967771172a7a3150174359bddd84d4b5d1467410abfe2f8a8e35bf6458aaf1a1a951d4e5b8524d03259a94f
7
- data.tar.gz: bd84006820c720fecfd948113423a90e8aa980ac281e9342a7ac5b38874c4f5e84412d9c5a32ff8a27d29260c8cb01f03b1317f3041a58d994efc6341f1da359
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
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-19 00:00:00.000000000 Z
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: 3.5.0
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: 3.5.0
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.16.2
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.0.7
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
@@ -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