logic_tools 0.2.1

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,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