graphmatcher 0.3.5 → 0.3.8

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