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.
- checksums.yaml +4 -4
- data/.gitignore +3 -0
- data/exe/is_tautology +12 -0
- data/exe/simplify_es +12 -0
- data/lib/logic_tools/is_tautology.rb +46 -0
- data/lib/logic_tools/logicconvert.rb +142 -0
- data/lib/logic_tools/logiccover.rb +828 -0
- data/lib/logic_tools/logicgenerator.rb +222 -0
- data/lib/logic_tools/logicparse.rb +9 -2
- data/lib/logic_tools/logicsimplify_es.rb +734 -0
- data/lib/logic_tools/{logicsimplify.rb → logicsimplify_qm.rb} +10 -3
- data/lib/logic_tools/logictree.rb +200 -36
- data/lib/logic_tools/minimal_column_covers.rb +481 -0
- data/lib/logic_tools/simplify_es.rb +39 -0
- data/lib/logic_tools/simplify_qm.rb +2 -18
- data/lib/logic_tools/test_logic_tools.rb +161 -0
- data/lib/logic_tools/traces.rb +116 -0
- data/lib/logic_tools/truth_tbl.rb +2 -2
- data/lib/logic_tools/version.rb +1 -1
- metadata +16 -3
@@ -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
|