logic_tools 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,804 @@
1
+ #############################################################
2
+ # Logic tree classes: used for describing logic expressions #
3
+ #############################################################
4
+
5
+ require 'forwardable'
6
+
7
+ module LogicTools
8
+
9
+ ## A logical variable
10
+ class Variable
11
+
12
+ include Comparable
13
+
14
+ ## The pool of variables
15
+ @@variables = {}
16
+
17
+ attr_reader :value
18
+
19
+ ## Initialize with a name (the value is set to false)
20
+ # Params:
21
+ # +name+:: the name of the variable
22
+ def initialize(name)
23
+ if @@variables.key?(name.to_s)
24
+ raise "Variable already present."
25
+ end
26
+ # print "New variable with name=#{name}\n"
27
+ @name = name.to_s
28
+ @value = false
29
+ # Add the variable to the pool
30
+ @@variables[name.to_s] = self
31
+ end
32
+
33
+ ## Set the value
34
+ # Params:
35
+ # +value+:: the value to set
36
+ def value=(value)
37
+ if (value.respond_to?(:to_i))
38
+ @value = value.to_i == 0 ? false : true
39
+ else
40
+ @value = value ? true : false
41
+ end
42
+ end
43
+
44
+ ## Checks if a variables exists
45
+ # Params:
46
+ # +name+:: the name of the variable to check
47
+ def Variable.exists?(name)
48
+ return @@variables.has_key?(name.to_s)
49
+ end
50
+
51
+ ## Get a variable by name, if not existing creates it.
52
+ # Params:
53
+ # +name+:: the name of the variable to get
54
+ def Variable.get(name)
55
+ var = @@variables[name.to_s]
56
+ # print "Got var=#{var.to_s} with value=#{var.value}\n" if var
57
+ var = Variable.new(name) unless var
58
+ return var
59
+ end
60
+
61
+ ## Convert to a string
62
+ def to_s
63
+ @name.dup
64
+ end
65
+
66
+ ## For print
67
+ def inspect
68
+ to_s
69
+ end
70
+
71
+ ## For comparison
72
+ def <=>(b)
73
+ self.to_s <=> b.to_s
74
+ end
75
+ end
76
+
77
+ ## A logical tree node
78
+ class Node
79
+ include Enumerable
80
+
81
+ ## Get the variables of the tree
82
+ # Return: the variables into an array, sorted by name
83
+ def getVariables()
84
+ result = self.getVariablesRecurse
85
+ return result.flatten.uniq.sort
86
+ end
87
+
88
+ ## Get the operator if any
89
+ def op
90
+ nil
91
+ end
92
+
93
+ ## Get the size (number of sons)
94
+ # Default: 0
95
+ def size
96
+ 0
97
+ end
98
+
99
+ ## Iterate on each truth table line.
100
+ # TODO: generate an Enumerator.
101
+ # Iteration parameters:
102
+ # vars: the variables of the expression
103
+ # val: the value
104
+ def each_line
105
+ # Get the variables
106
+ vars = self.getVariables
107
+ # Compute the number of iterations
108
+ nlines = 2**vars.size
109
+ # Generates each bit value for the variables and the
110
+ # correspong node value for iterating on it
111
+ nlines.times do |line|
112
+ vars.each_with_index do |var,i|
113
+ val = line[vars.size-i-1]
114
+ var.value = val
115
+ end
116
+ # Apply the block
117
+ yield(vars,self.eval)
118
+ end
119
+ end
120
+
121
+ ## Iterate over each minterm.
122
+ # TODO: generate an Enumerator.
123
+ # Iteration parameters:
124
+ # vars: the variables of the expression
125
+ def each_minterm
126
+ each_line { |vars,val| yield(vars) if val }
127
+ end
128
+
129
+ ## Iterate over each maxterm.
130
+ # TODO: generate an Enumerator.
131
+ # Iteration parameters:
132
+ # vars: the variables of the expression
133
+ def each_maxterm
134
+ each_line do |vars,val|
135
+ unless val then
136
+ vars.each { |var| var.value = !var.value }
137
+ yield(vars)
138
+ end
139
+ end
140
+ end
141
+
142
+ ## Iterate on the sons (if any, by default: none)
143
+ # TODO: generate an Enumerator.
144
+ def each
145
+ end
146
+
147
+ ## Generates the equivalent standard conjunctive form
148
+ def to_std_conjunctive
149
+ # Generate each minterm tree
150
+ minterms = []
151
+ each_minterm do |vars|
152
+ vars = vars.map do |var|
153
+ var = NodeVar.new(var)
154
+ var = NodeNot.new(var) unless var.eval
155
+ var
156
+ end
157
+ # Create the term
158
+ term = vars.size == 1 ? vars[0] : NodeAnd.new(*vars)
159
+ # Add the term
160
+ minterms << term
161
+ end
162
+ # Conjunct them
163
+ return minterms.size == 1 ? minterms[0] : NodeOr.new(*minterms)
164
+ end
165
+
166
+ ## Generates the equivalent standard disjonctive form
167
+ def to_std_disjonctive
168
+ # Generate each maxterm tree
169
+ maxterms = []
170
+ each_maxterm do |vars|
171
+ vars = vars.map do |var|
172
+ var = NodeVar.new(var)
173
+ var = NodeNot.new(var) unless var.eval
174
+ var
175
+ end
176
+ # Create the term
177
+ term = vars.size == 1 ? vars[0] : NodeOr.new(*vars)
178
+ # Add the term
179
+ maxterms << term
180
+ end
181
+ # Disjunct them
182
+ return maxterms.size == 1 ? maxterms[0] : NodeAnd.new(*maxterms)
183
+ end
184
+
185
+ ## Flatten ands, ors and nots
186
+ # Default: simply duplicate
187
+ def flatten
188
+ return self.dup
189
+ end
190
+
191
+ ## Flatten hierachical ands, ors and nots.
192
+ # Default: simply duplicate.
193
+ # Return: the new tree
194
+ def flatten_deep
195
+ return self.dup
196
+ end
197
+
198
+ ## Distribute over a given operator.
199
+ # Default distribution: self is unary.
200
+ # Params:
201
+ # +dop+:: the operator to distribute over
202
+ # +node+:: the node to distribute with
203
+ def distribute(dop,node)
204
+ fop = dop == :and ? :or : :and
205
+ # print "dop=#{dop}, fop=#{fop}, node.op=#{node.op}\n"
206
+ if (node.op == dop) then
207
+ # Same operator: merge self in node
208
+ return NodeNary.make(dop, self, *node)
209
+ elsif (node.op == fop) then
210
+ # Opposite operator: can distribute
211
+ nsons = node.map do |son|
212
+ NodeNary.make(dop,son,self).flatten
213
+ end
214
+ return NodeNary.make(fop,*nsons).flatten
215
+ else
216
+ # Unary operator: simple product
217
+ return NodeNary.make(dop, self, node)
218
+ end
219
+ end
220
+
221
+ ## Convert to a sum of product
222
+ # Params:
223
+ # +flattened+:: tell if the tree is already flattend
224
+ # Return: the conversion result
225
+ def to_sum_product(flattened = false)
226
+ return self.dup
227
+ end
228
+
229
+ ## For print
230
+ def inspect
231
+ to_s
232
+ end
233
+
234
+ ## Convert to a symbol:
235
+ # Default: to_s.to_sym
236
+ def to_sym
237
+ to_s.to_sym
238
+ end
239
+
240
+ ## For hash
241
+ def hash
242
+ to_sym.hash
243
+ end
244
+ def eql?(val)
245
+ self == val
246
+ end
247
+ end
248
+
249
+ ## A Value node
250
+ class NodeValue < Node
251
+
252
+ protected
253
+ ## Build with a value.
254
+ # Params:
255
+ # +value+:: the value
256
+ def initialize(value)
257
+ @value = value
258
+ @sym = @value.to_s.to_sym
259
+ end
260
+ public
261
+
262
+ ## Get the variables, recursively, without postprocessing.
263
+ # Return: the variables into sets of arrays with possible doublon
264
+ def getVariablesRecurse()
265
+ return [ ]
266
+ end
267
+
268
+ ## Compare with another node.
269
+ # Params:
270
+ # +n+:: the node to compare with
271
+ def ==(n)
272
+ return false unless n.is_a?(NodeValue)
273
+ return self.eval() == n.eval()
274
+ end
275
+
276
+ ## Evaluate.
277
+ def eval
278
+ return @value
279
+ end
280
+
281
+ ## Convert to a symbol.
282
+ def to_sym
283
+ return @sym
284
+ end
285
+
286
+ ## Converts to a string.
287
+ def to_s
288
+ return @value.to_s
289
+ end
290
+ end
291
+
292
+ ## A true node
293
+ class NodeTrue < NodeValue
294
+ ## Intialize as a NodeValue whose value is true.
295
+ def initialize
296
+ super(true)
297
+ end
298
+ end
299
+
300
+ ## A false node
301
+ class NodeFalse < NodeValue
302
+ ## Intialize as a NodeValue whose value is false.
303
+ def initialize
304
+ super(false)
305
+ end
306
+ end
307
+
308
+ ## A variable node
309
+ class NodeVar < Node
310
+ attr_reader :variable
311
+
312
+ ## Initialize with the variable name.
313
+ # Params:
314
+ # +name+:: the name of the variable
315
+ def initialize(name)
316
+ @variable = Variable.get(name)
317
+ @sym = @variable.to_s.to_sym
318
+ end
319
+
320
+ ## Evaluates the node
321
+ def eval()
322
+ return @variable.value
323
+ end
324
+
325
+ ## Convert to a symbol
326
+ def to_sym
327
+ return @sym
328
+ end
329
+
330
+ ## Get the variables, recursively, without postprocessing
331
+ # Return: the variables into sets of arrays with possible doublon
332
+ def getVariablesRecurse()
333
+ return [ @variable ]
334
+ end
335
+
336
+ ## Compare with another node
337
+ # Params:
338
+ # +n+:: the node to compare with
339
+ def ==(n)
340
+ return false unless n.is_a?(NodeVar)
341
+ return self.variable == n.variable
342
+ end
343
+
344
+ ## Converts to a string
345
+ def to_s
346
+ return variable.to_s
347
+ end
348
+ end
349
+
350
+ ## A binary node
351
+ class NodeNary < Node
352
+ extend Forwardable
353
+ include Enumerable
354
+
355
+ attr_reader :op
356
+
357
+ protected
358
+ ## Initialize with the operator
359
+ # Params:
360
+ # +op+:: the operator name
361
+ # +sons+:: the sons
362
+ def initialize(op,*sons)
363
+ # Check the sons
364
+ sons.each do |son|
365
+ unless son.is_a?(Node) then
366
+ raise ArgumentError.new("Not a valid class for a son: "+
367
+ "#{son.class}")
368
+ end
369
+ end
370
+ # Sons are ok
371
+ @op = op.to_sym
372
+ @sons = sons
373
+ @sym = self.to_s.to_sym
374
+ end
375
+
376
+ public
377
+ ## Create a new nary node
378
+ # Params:
379
+ # +op+:: the operator name
380
+ # +sons+:: the sons
381
+ def NodeNary.make(op,*sons)
382
+ case op
383
+ when :or
384
+ return NodeOr.new(*sons)
385
+ when :and
386
+ return NodeAnd.new(*sons)
387
+ else
388
+ raise ArgumentError.new("Not a valid operator: #{op}")
389
+ end
390
+ end
391
+
392
+
393
+ # Also acts as an array of nodes
394
+ def_delegators :@sons, :[], :empty?, :size
395
+ def each(&blk)
396
+ @sons.each(&blk)
397
+ return self
398
+ end
399
+ # def sort_by!(&blk)
400
+ # @sons.sort_by!(&blk)
401
+ # return self
402
+ # end
403
+ # def sort!
404
+ # @sons.sort_by! {|son| son.sym }
405
+ # return self
406
+ # end
407
+ def sort
408
+ return NodeNary.make(@op,*@sons.sort_by {|son| son.to_sym })
409
+ end
410
+
411
+ ## Create a new node without doublons.
412
+ def uniq(&blk)
413
+ if blk then
414
+ nsons = @sons.uniq(&blk)
415
+ else
416
+ nsons = @sons.uniq { |son| son.to_sym }
417
+ end
418
+ if nsons.size == 1 then
419
+ return nsons[0]
420
+ else
421
+ return NodeNary.make(@op,*nsons)
422
+ end
423
+ end
424
+
425
+ ## Convert to a symbol.
426
+ def to_sym
427
+ return @sym
428
+ end
429
+
430
+ ## Get the variables, recursively, without postprocessing.
431
+ # Return: the variables into sets of arrays with possible doublon
432
+ def getVariablesRecurse()
433
+ return @sons.reduce([]) do |res,son|
434
+ res.concat(son.getVariablesRecurse)
435
+ end
436
+ end
437
+
438
+ ## Compare with another node
439
+ # Params:
440
+ # +n+:: the node to compare with
441
+ def ==(n)
442
+ return false unless n.is_a?(Node)
443
+ return false unless self.op == n.op
444
+ # There is no find_with_index!
445
+ # return ! @sons.find_with_index {|son,i| son != n[i] }
446
+ @sons.each_with_index do |son,i|
447
+ return false if son != n[i]
448
+ end
449
+ return true
450
+ end
451
+
452
+ # WRONG
453
+ ## Reduce a node: remove its redudancies using absbortion rules
454
+ # NOTE: NEED to CONSIDER X~X and X+~X CASES
455
+ # def reduce
456
+ # # The operator used for the factors
457
+ # fop = @op == :and ? :or : :and
458
+ # # Gather the terms by factor
459
+ # terms = Hash.new {|h,k| h[k] = [] }
460
+ # @sons.each do |term|
461
+ # if (term.op == fop) then
462
+ # # There are factors
463
+ # term.each { |fact| terms[fact] << term }
464
+ # else
465
+ # # There term is singleton
466
+ # terms[term] << term
467
+ # end
468
+ # end
469
+ # # Keep only the shortest term per factor
470
+ # terms.each_key {|k| terms[k] = terms[k].min_by {|term| term.size} }
471
+ # nsons = terms.values
472
+ # # Avoid doublons
473
+ # nsons.uniq!
474
+ # # Generate the result
475
+ # if (nsons.size == 1)
476
+ # return nsons[0]
477
+ # else
478
+ # return NodeNary.make(@op,*nsons)
479
+ # end
480
+ # end
481
+
482
+ ## Reduce a node: remove its redudancies using absbortion rules.
483
+ # NOTE: NEED to CONSIDER X~X and X+~X CASES
484
+ def reduce
485
+ # The operator used for the factors
486
+ fop = @op == :and ? :or : :and
487
+ # Gather the terms converted to a sorted string for fast
488
+ # comparison
489
+ terms = @sons.map do |son|
490
+ if (son.op == fop) then
491
+ [ son, son.sort.to_s ]
492
+ else
493
+ [ son, son.to_s ]
494
+ end
495
+ end
496
+ nsons = []
497
+ # Keep only the terms that do not contain another one
498
+ terms.each_with_index do |term0,i|
499
+ skipped = false
500
+ terms.each_with_index do |term1,j|
501
+ next if (i==j) # Same term
502
+ if (term0[1].include?(term1[1])) and term0[1]!=term1[1] then
503
+ # term0 contains term1 but is different, skip it
504
+ skipped = true
505
+ break
506
+ end
507
+ end
508
+ nsons << term0[0] unless skipped # Term has not been skipped
509
+ end
510
+ # Avoid doublons
511
+ nsons.uniq!
512
+ # Generate the result
513
+ if (nsons.size == 1)
514
+ return nsons[0]
515
+ else
516
+ return NodeNary.make(@op,*nsons)
517
+ end
518
+ end
519
+
520
+ ## Flatten ands, ors and nots.
521
+ # Default: simply duplicate
522
+ def flatten
523
+ return NodeNary.make(@op,*(@sons.reduce([]) do |nsons,son|
524
+ if (son.op == self.op) then
525
+ nsons.push(*son)
526
+ else
527
+ nsons << son
528
+ end
529
+ end)).reduce
530
+ end
531
+
532
+ ## Flatten hierachical ands, ors and nots, removing redudancies.
533
+ # Return: the new tree
534
+ def flatten_deep
535
+ return NodeNary.make(@op,*(@sons.reduce([]) do |nsons,son|
536
+ son = son.flatten_deep
537
+ if (son.op == self.op) then
538
+ nsons.push(*son)
539
+ else
540
+ nsons << son
541
+ end
542
+ end)).reduce
543
+ end
544
+
545
+ ## Distribute over a given operator.
546
+ # Params:
547
+ # +dop+:: the operator to distribute over
548
+ # +node+:: the node to distribute with
549
+ def distribute(dop,node)
550
+ fop = dop == :and ? :or : :and
551
+ # print "dop=#{dop} fop=#{fop} self.op=#{@op}\n"
552
+ if (@op == dop) then
553
+ # Self operator is dop: merge node in self
554
+ return NodeNary.make(dop,*self,node).flatten
555
+ else
556
+ # self operator if fop
557
+ if (node.op == fop) then
558
+ # node operator is also fop: (a+b)(c+d) or ab+cd case
559
+ nsons = []
560
+ self.each do |son0|
561
+ node.each do |son1|
562
+ # print "son0=#{son0}, son1=#{son1}\n"
563
+ nsons << NodeNary.make(dop, son0, son1).flatten
564
+ # print "nsons=#{nsons}\n"
565
+ end
566
+ end
567
+ return NodeNary.make(fop,*nsons).flatten
568
+ else
569
+ # node operator is not fop: (a+b)c or ab+c case
570
+ nsons = self.map do |son|
571
+ NodeNary.make(dop,son,node).flatten
572
+ end
573
+ return NodeNary.make(fop,*nsons).flatten
574
+ end
575
+ end
576
+ end
577
+ end
578
+
579
+ ## An AND node
580
+ class NodeAnd < NodeNary
581
+ ## Initialize by building a new nary node whose operator is and.
582
+ # Params:
583
+ # +sons+:: the sons
584
+ def initialize(*sons)
585
+ super(:and,*sons)
586
+ end
587
+
588
+ ## Duplicates the node.
589
+ def dup
590
+ return NodeAnd.new(@sons.map(&:dup))
591
+ end
592
+
593
+ ## Evaluate the node.
594
+ def eval()
595
+ return !@sons.any? {|son| son.eval() == false }
596
+ end
597
+
598
+ ## Convert to a sum of product.
599
+ # Params:
600
+ # +flattened+:: tell of the tree is already flatttend
601
+ # Return: the conversion result
602
+ def to_sum_product(flattened = false)
603
+ # Flatten if required
604
+ node = flattened ? self : self.flatten_deep
605
+ # print "node = #{node}\n"
606
+ # Convert each son to sum of product
607
+ nsons = node.map {|son| son.to_sum_product(true) }
608
+ # print "nsons = #{nsons}\n"
609
+ # Distribute
610
+ while(nsons.size>1)
611
+ dist = []
612
+ nsons.each_slice(2) do |left,right|
613
+ # print "left=#{left}, right=#{right}\n"
614
+ dist << (right ? left.distribute(:and,right) : left)
615
+ end
616
+ # print "dist=#{dist}\n"
617
+ nsons = dist
618
+ end
619
+ # print "Distributed nsons=#{nsons}\n"
620
+ # Generate the or
621
+ if (nsons.size > 1)
622
+ return NodeOr.new(*nsons)
623
+ else
624
+ return nsons[0]
625
+ end
626
+ end
627
+
628
+ ## Convert to a string.
629
+ def to_s
630
+ return @str if @str
631
+ @str = ""
632
+ # Convert the sons to a string
633
+ @sons.each do |son|
634
+ if (son.op == :or) then
635
+ # Yes, need parenthesis
636
+ @str << ( "(" + son.to_s + ")" )
637
+ else
638
+ @str << son.to_s
639
+ end
640
+ end
641
+ return @str
642
+ end
643
+ end
644
+
645
+
646
+ ## An OR node
647
+ class NodeOr < NodeNary
648
+ ## Initialize by building a new nary node whose operator is or
649
+ # @param sons the sons
650
+ def initialize(*sons)
651
+ super(:or,*sons)
652
+ end
653
+
654
+ ## Duplicates the node
655
+ def dup
656
+ return NodeOr.new(@sons.map(&:dup))
657
+ end
658
+
659
+ ## Evaluate the node
660
+ def eval
661
+ return @sons.any? {|son| son.eval() == true }
662
+ end
663
+
664
+ ## Convert to a sum of product
665
+ # @param flattened tell of the tree is already flatttend
666
+ # @return the conversion result
667
+ def to_sum_product(flattened = false)
668
+ return NodeOr.new(*@sons.map {|son| son.to_sum_product(flatten) })
669
+ end
670
+
671
+ ## Converts to a string
672
+ def to_s
673
+ return @str if @str
674
+ # Convert the sons to string a insert "+" between them
675
+ @str = @sons.join("+")
676
+ return @str
677
+ end
678
+ end
679
+
680
+
681
+
682
+ # An unary node
683
+ class NodeUnary < Node
684
+ attr_reader :op, :son
685
+
686
+ ## Initialize with the operator.
687
+ # Params:
688
+ # +op+:: the operator name
689
+ # +son+:: the son node
690
+ def initialize(op,son)
691
+ if !son.is_a?(Node) then
692
+ raise ArgumentError.new("Not a valid object for son.")
693
+ end
694
+ @op = op.to_sym
695
+ @son = son
696
+ @sym = self.to_s.to_sym
697
+ end
698
+
699
+ ## Get the size (number of sons).
700
+ def size
701
+ 1
702
+ end
703
+
704
+ # ## Set the son node
705
+ # # @param son the node to set
706
+ # def son=(son)
707
+ # # Checks it is a valid object
708
+ # if !son.is_a?(Node) then
709
+ # raise ArgumentError.new("Not a valid object for son.")
710
+ # else
711
+ # @son = son
712
+ # end
713
+ # end
714
+
715
+ ## Get the variables in an array recursively.
716
+ # Return: the variables into an array with possible doublon
717
+ def getVariablesRecurse()
718
+ return @son.getVariablesRecurse
719
+ end
720
+
721
+ ## Iterate on the sons.
722
+ def each
723
+ yield(@son)
724
+ end
725
+
726
+ ## Compare with another node.
727
+ # Params:
728
+ # +n+:: the node to compare with
729
+ def ==(n)
730
+ return false unless n.is_a?(Node)
731
+ return false unless self.op == n.op
732
+ return self.son == n.son
733
+ end
734
+
735
+ ## Convert to a symbol.
736
+ def to_sym
737
+ return @sym
738
+ end
739
+ end
740
+
741
+ ## A NOT node
742
+ class NodeNot < NodeUnary
743
+ ## Initialize by building a new unary node whose operator is not.
744
+ # Params:
745
+ # +son+:: the son
746
+ def initialize(son)
747
+ super(:not,son)
748
+ end
749
+
750
+ ## Duplicates the node.
751
+ def dup
752
+ return NodeNot.new(@son.dup)
753
+ end
754
+
755
+ ## Evaluate the node.
756
+ def eval
757
+ return !son.eval
758
+ end
759
+
760
+ ## Flatten ands, ors and nots.
761
+ # Default: simply duplicate
762
+ def flatten
763
+ if nson.op == :not then
764
+ return nson
765
+ else
766
+ return NodeNot.new(nson)
767
+ end
768
+ end
769
+
770
+ ## Flatten hierachical ands, ors and nots.
771
+ # Return: the new tree
772
+ def flatten_deep
773
+ nson = @son.flatten_deep
774
+ if nson.op == :not then
775
+ return nson
776
+ else
777
+ return NodeNot.new(nson)
778
+ end
779
+ end
780
+
781
+ ## Convert to a sum of product.
782
+ # Params:
783
+ # +flattened+:: tell of the tree is already flatttend
784
+ # Return: the conversion result
785
+ def to_sum_product(flattened = false)
786
+ return NodeNot.new(@son.to_sum_product(flatten))
787
+ end
788
+
789
+ ## Converts to a string.
790
+ def to_s
791
+ return @str if @str
792
+ # Is the son a binary node?
793
+ if son.op == :or || son.op == :and then
794
+ # Yes must put parenthesis
795
+ @str = "~(" + son.to_s + ")"
796
+ else
797
+ # No
798
+ @str = "~" + son.to_s
799
+ end
800
+ return @str
801
+ end
802
+ end
803
+
804
+ end