logic_tools 0.2.4 → 0.3.0

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.
@@ -0,0 +1,481 @@
1
+ #########################################################################
2
+ # Algorithm for computing the minimal column covers of a boolean matrix #
3
+ #########################################################################
4
+
5
+ # require 'set'
6
+ require 'timeout'
7
+
8
+ module LogicTools
9
+
10
+ ## Converts a +product+ of sum to a sum of product.
11
+ #
12
+ # NOTE: * Both the input are outputs are represented as array of arrays.
13
+ def to_sum_product_array(product)
14
+ return product[0].map {|term| [term] } if product.size == 1
15
+ # Generate the initial terms.
16
+ sum = product[0].product(product[1])
17
+ sum.each {|term| term.sort!.uniq! }
18
+ sum.uniq!
19
+ # Fill then with each factor to the resulting sum of product.
20
+ # print "sum = #{sum}, product=#{product}\n"
21
+ (2..(product.size-1)).each do |i|
22
+ sum.map! do |term|
23
+ # # print "mapping #{product[i]}\n"
24
+ set = []
25
+ product[i].each do |fact|
26
+ if term.include?(fact) then
27
+ set << term unless set.include?(term)
28
+ else
29
+ nterm = term.clone
30
+ nterm << fact
31
+ nterm.sort!
32
+ set << nterm
33
+ end
34
+ end
35
+ set
36
+ end
37
+ sum.flatten!(1)
38
+ # print "then sum=#{sum}\n"
39
+ sum.uniq!
40
+ # print "now sum=#{sum}\n"
41
+ # pid, size = `ps ax -o pid,rss | grep -E "^[[:space:]]*#{$$}"`.strip.split.map(&:to_i)
42
+ # print "memory usage=#{size}\n"
43
+ end
44
+ # print "\n"
45
+ return sum
46
+ end
47
+
48
+
49
+ ## Class for storing and counting occurences of objects.
50
+ class HashCounter < Hash
51
+
52
+ ## Creates a new hash counter.
53
+ def initialize
54
+ self.default = 0
55
+ end
56
+
57
+ ## Increments the number of +element+.
58
+ def inc(element)
59
+ self[elem] += 1
60
+ end
61
+
62
+ ## Decrements the number of +element+.
63
+ def dec(element)
64
+ if (self[elem] -= 1) == 0 then
65
+ # No more instance of the element, remove the entry.
66
+ self.delete(elem)
67
+ end
68
+ end
69
+ end
70
+
71
+
72
+ ## Class for applying branch and bound for extracting from a product of sums
73
+ # the smallest term of the corresponding sum of products.
74
+ #
75
+ # Attributes:
76
+ # +product+:: the product to extract the term from.
77
+ # +cur_term+:: the current term.
78
+ # +best_term+:: the best term found.
79
+ # +best_cost+:: the best cost.
80
+ # +deadline+:: time before returning the current best solution.
81
+ # +time+:: initial time.
82
+ class SmallestSumTerm
83
+
84
+ ## Creates the solver for a product.
85
+ def initialize(product, deadline = Float::INFINITY)
86
+ @product = product
87
+ @cur_term = []
88
+ # @cur_term = HashCounter.new
89
+ @best_term = nil
90
+ @best_cost = @cur_cost = Float::INFINITY
91
+ @deadline = deadline
92
+ end
93
+
94
+ ## Selects a +term+ for solution.
95
+ def make_best(term)
96
+ # print "make_best\n"
97
+ @best_term = term.uniq
98
+ @best_cost = @best_term.size
99
+ end
100
+
101
+ ## Bounds a partial +term+.
102
+ #
103
+ # # NOTE: may modify term through uniq! (for performance purpose.)
104
+ # NOTE: It is assumed that term is hash-like
105
+ def bound(term)
106
+ # if Time.now - @time >= @deadline and
107
+ # @best_cost < Float::INFINITY then
108
+ # # Time over, force a high cost.
109
+ # return Float::INFINITY
110
+ # end
111
+ if (term.size >= @best_cost) then
112
+ return term.uniq.size
113
+ else
114
+ return term.size
115
+ end
116
+ # return term.size
117
+ end
118
+
119
+ ## Solves the problem using branch and bound.
120
+ def solve()
121
+ # Solve the problem throughly.
122
+ begin
123
+ Timeout::timeout(@deadline) {
124
+ self.branch(0)
125
+ }
126
+ rescue Timeout::Error
127
+ # Time out, is there a solution?
128
+ # print "Timeout!\n"
129
+ unless @best_term
130
+ # No, quickly create one including the first element
131
+ # of each factor.
132
+ @best_term = @product.map {|fact| fact[0] }
133
+ @best_term.uniq!
134
+ end
135
+ end
136
+ return @best_term
137
+ end
138
+
139
+ ## Branch in the branch and bound algorithm.
140
+ def branch(pi)
141
+ # # Start the timer if required.
142
+ # @time = Time.now if (pi == 0)
143
+ # # Check the deadline.
144
+ # if Time.now - @time >= @deadline and
145
+ # @best_cost < Float::INFINITY then
146
+ # # Time over, end here.
147
+ # return @best_term
148
+ # end
149
+ # Bound the current term.
150
+ if (self.bound(@cur_term) < @best_cost)
151
+ # Better bound, can go on with the current solution.
152
+ if pi == @product.size then
153
+ # But this is the end, so update the best term.
154
+ make_best(@cur_term)
155
+ else
156
+ # Still a possible solution, recurse.
157
+ @product[pi].each do |elem|
158
+ @cur_term.push(elem)
159
+ # @cur_term.inc(elem)
160
+ # solve(pi+1)
161
+ branch(pi+1)
162
+ @cur_term.pop
163
+ # @cur_term.dec(elem)
164
+ end
165
+ end
166
+ end
167
+ return @best_term
168
+ end
169
+ end
170
+
171
+
172
+ ## Extracts from a +product+ of sums the smallest term of the corresponding
173
+ # sum of products.
174
+ #
175
+ # NOTE: * Both the input are outputs are represented as array of arrays.
176
+ # * Uses a branch and bound algorithm.
177
+ def smallest_sum_term(product, deadline = Float::INFINITY)
178
+ return [product[0][0]] if product.size == 1
179
+
180
+ # Create the solver and applies it
181
+ return SmallestSumTerm.new(product,deadline).solve
182
+ end
183
+
184
+ ## Computes the minimal column covers of a boolean +matrix+.
185
+ #
186
+ # If +smallest+ is set to one, the method returns the smallest minimal
187
+ # column cover instead.
188
+ #
189
+ # The +matrix+ is assumed to be an array of string, each string
190
+ # representing a boolean row ("0" for false and "1" for true).
191
+ def minimal_column_covers(matrix, smallest = false,
192
+ deadline = Float::INFINITY)
193
+ # print "matrix=#{matrix}\n"
194
+
195
+ # Step 1: reduce the matrix for faster processing.
196
+ # First put appart the essential columns.
197
+ essentials = []
198
+ matrix.each do |row|
199
+ col = nil
200
+ row.each_byte.with_index do |c,i|
201
+ # if c == "1" then
202
+ if c == 49 then
203
+ if col then
204
+ # The row has several "1", no essential column there.
205
+ col = nil
206
+ break
207
+ end
208
+ col = i
209
+ end
210
+ end
211
+ # An essential column is found.
212
+ essentials << col if col
213
+ end
214
+ essentials.uniq!
215
+ # print "essentials = #{essentials}\n"
216
+ # The remove the rows covered by essential columns.
217
+ keep = [ true ] * matrix.size
218
+ essentials.each do |col|
219
+ matrix.each.with_index do |row,i|
220
+ # keep[i] = false if row[col] == "1"
221
+ keep[i] = false if row.getbyte(col) == 49
222
+ end
223
+ end
224
+ # print "keep = #{keep}\n"
225
+ reduced = matrix.select.with_index {|row,i| keep[i] }
226
+ # print "matrix = #{matrix}\n"
227
+ # print "reduced = #{reduced}\n"
228
+ if reduced.empty? then
229
+ # Essentials columns are enough for the cover, end here.
230
+ if smallest then
231
+ return essentials
232
+ else
233
+ return [ essentials ]
234
+ end
235
+ end
236
+
237
+ to_optimize = false
238
+ removed_columns = []
239
+ begin
240
+ to_optimize = false
241
+ # Then remove the dominating rows
242
+ reduced.uniq!
243
+ reduced = reduced.select.with_index do |row0,i|
244
+ ! reduced.find.with_index do |row1,j|
245
+ if i == j then
246
+ false
247
+ else
248
+ # The row is dominating if in includes another row.
249
+ res = row0.each_byte.with_index.find do |c,j|
250
+ # row1[j] == "1" and c == "0"
251
+ row1.getbyte(j) == 49 and c == 48
252
+ end
253
+ # Not dominating if res
254
+ !res
255
+ end
256
+ end
257
+ end
258
+
259
+ # # Finally remove the dominated columns if only one column cover
260
+ # # is required.
261
+ # if smallest and reduced.size >= 1 then
262
+ # size = reduced[0].size
263
+ # size.times.reverse_each do |col0|
264
+ # next if removed_columns.include?(col0)
265
+ # size.times do |col1|
266
+ # next if col0 == col1
267
+ # # The column is dominated if it is included into another.
268
+ # res = reduced.find do |row|
269
+ # row[col0] == "1" and row[col1] == "0"
270
+ # end
271
+ # # Not dominated if res
272
+ # unless res
273
+ # to_optimize = true
274
+ # # print "removing column=#{col0}\n"
275
+ # # Dominated, remove it
276
+ # reduced.each { |row| row[col0] = "0" }
277
+ # removed_columns << col0
278
+ # end
279
+ # end
280
+ # end
281
+ # end
282
+ end while(to_optimize)
283
+
284
+ # print "now reduced=#{reduced}\n"
285
+
286
+ # Step 2: Generate the Petrick's product.
287
+ product = []
288
+ reduced.each do |row|
289
+ term = []
290
+ # Get the columns covering the row.
291
+ row.each_byte.with_index do |bit,i|
292
+ # term << i if bit == "1"
293
+ term << i if bit == 49
294
+ end
295
+ product << term unless term.empty?
296
+ end
297
+
298
+
299
+ if smallest then
300
+ if product.empty? then
301
+ return essentials
302
+ end
303
+ cover = smallest_sum_term(product,deadline)
304
+ if essentials then
305
+ # print "essentials =#{essentials} cover=#{cover}\n"
306
+ essentials.each {|cube| cover.unshift(cube) }
307
+ return cover
308
+ else
309
+ return cover
310
+ end
311
+ end
312
+
313
+ # print "product=#{product}\n"
314
+ if product.empty? then
315
+ sum = product
316
+ else
317
+ product.each {|fact| fact.sort!.uniq! }
318
+ product.sort!.uniq!
319
+ # print "product=#{product}\n"
320
+ sum = to_sum_product_array(product)
321
+ # print "sum=#{sum}\n"
322
+ sum.each {|term| term.uniq! }
323
+ sum.uniq!
324
+ sum.sort_by! {|term| term.size }
325
+ # print "sum=#{sum}\n"
326
+ end
327
+
328
+ # # Add the essentials to the result and return it.
329
+ # if smallest then
330
+ # # print "smallest_cover=#{smallest_cover}, essentials=#{essentials}\n"
331
+ # return essentials if sum.empty?
332
+ # # Look for the smallest cover
333
+ # sum.sort_by! { |cover| cover.size }
334
+ # if essentials then
335
+ # return sum[0] + essentials
336
+ # else
337
+ # return sum[0]
338
+ # end
339
+ # else
340
+ sum.map! { |cover| essentials + cover }
341
+ return sum
342
+ # end
343
+ end
344
+
345
+
346
+
347
+ # ## Computes the minimal column covers of a boolean +matrix+.
348
+ # #
349
+ # # If +smallest+ is set to one, the method returns the smallest minimal
350
+ # # column cover instead.
351
+ # #
352
+ # # The +matrix+ is assumed to be an array of string, each string
353
+ # # representing a boolean row ("0" for false and "1" for true).
354
+ # def minimal_column_covers2(matrix,smallest = false)
355
+ # # print "matrix=#{matrix}\n"
356
+ # # Generate a variables nodes for each column: their name is
357
+ # # directly the column number.
358
+ # variables = matrix[0].size.times.map {|i| Variable.get("#{i}") }
359
+
360
+ # # Step 1: reduce the matrix for faster processing.
361
+ # # First put appart the essential columns.
362
+ # essentials = []
363
+ # matrix.each do |row|
364
+ # col = nil
365
+ # row.each_char.with_index do |c,i|
366
+ # if c == "1" then
367
+ # if col then
368
+ # # The row has several "1", no essential column there.
369
+ # col = nil
370
+ # break
371
+ # end
372
+ # col = i
373
+ # end
374
+ # end
375
+ # # An essential column is found.
376
+ # essentials << col if col
377
+ # end
378
+ # essentials.uniq!
379
+ # # print "essentials = #{essentials}\n"
380
+ # # The remove the rows covered by essential columns.
381
+ # keep = [ true ] * matrix.size
382
+ # essentials.each do |col|
383
+ # matrix.each.with_index do |row,i|
384
+ # keep[i] = false if row[col] == "1"
385
+ # end
386
+ # end
387
+ # # print "keep = #{keep}\n"
388
+ # reduced = matrix.select.with_index {|row,i| keep[i] }
389
+ # # print "matrix = #{matrix}\n"
390
+ # # print "reduced = #{reduced}\n"
391
+ # if reduced.empty? then
392
+ # # Essentials columns are enough for the cover, end here.
393
+ # if smallest then
394
+ # return essentials
395
+ # else
396
+ # return [ essentials ]
397
+ # end
398
+ # end
399
+ # # Then remove the dominating rows
400
+ # # For that purpose, sort them lexicographically.
401
+ # # print "reduced=#{reduced}\n"
402
+ # reduced.sort!.reverse!
403
+ # reduced = reduced.select.with_index do |row,i|
404
+ # i == reduced.size-1 or row.each_char.with_index.find do |c,j|
405
+ # ( c == "0" ) and ( (i+1)..(reduced.size-1) ).each.find do |k|
406
+ # reduced[k][j] == "1"
407
+ # end
408
+ # end
409
+ # end
410
+ # # print "now reduced=#{reduced}\n"
411
+
412
+ # # Step 2: Generate the Petrick's product.
413
+ # product = []
414
+ # reduced.each do |row|
415
+ # term = []
416
+ # # Get the columns covering the row.
417
+ # row.each_char.with_index do |bit,i|
418
+ # term << NodeVar.new(variables[i]) if bit == "1"
419
+ # end
420
+ # if term.size == 1 then
421
+ # product << term[0]
422
+ # elsif term.size > 1 then
423
+ # product << NodeOr.new(*term)
424
+ # end
425
+ # end
426
+ # # print "product=#{product}\n"
427
+ # if (product.empty?) then
428
+ # sum = nil
429
+ # elsif (product.size == 1)
430
+ # sum = product[0]
431
+ # else
432
+ # product = NodeAnd.new(*product)
433
+ # sum = product.sort.reduce.to_sum_product(true).sort.reduce
434
+ # end
435
+
436
+ # # Step 3: Convert the product to sum of product.
437
+ # # print "sum=#{sum}\n"
438
+ # unless sum
439
+ # # No minimal cover
440
+ # if smallest then
441
+ # return nil
442
+ # else
443
+ # return []
444
+ # end
445
+ # end
446
+ # sum = [ sum ] unless sum.is_a?(NodeOr) # In case sum is not a sum
447
+
448
+ # # Step4: Each term of the sum is a minimal cover.
449
+ # result = []
450
+ # smallest_cover = nil
451
+ # sum.each do |term|
452
+ # # Maybe the term is a litteral, if yes, make it an array
453
+ # term = [ term ] unless term.is_a?(NodeNary)
454
+ # # The name of a variable is directly the column number!
455
+ # cover = term.each.map do |lit|
456
+ # lit.variable.to_s.to_i
457
+ # end
458
+ # result << cover
459
+ # # In case we look for the smallest
460
+ # if smallest then
461
+ # if smallest_cover == nil then
462
+ # smallest_cover = cover
463
+ # elsif cover.size < smallest_cover.size
464
+ # smallest_cover = cover
465
+ # end
466
+ # end
467
+ # end
468
+ # # Add the essentials to the result and return it.
469
+ # if smallest then
470
+ # # print "smallest_cover=#{smallest_cover}, essentials=#{essentials}\n"
471
+ # return essentials unless smallest_cover
472
+ # return smallest_cover unless essentials
473
+ # return smallest_cover + essentials
474
+ # else
475
+ # result.map! { |cover| cover + essentials }
476
+ # return result
477
+ # end
478
+ # end
479
+
480
+
481
+ end