Term.to_cnf()   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 1
dl 0
loc 3
rs 10
1
require 'ref'
2
require 'prop_logic/functions'
3
4
module PropLogic
5
  #
6
  # Abstract base class for terms of PropLogic.
7
  # Actual terms are subclasses of Term.
8
  #
9
  class Term
10
    include Functions
11
12
    # @raise NotImplementedError Term is abstract class.
13
    def initialize
14
      raise NotImplementedError, 'Term cannot be initialized'
15
    end
16
17
    # disallow duplication
18
    def initialize_copy(*)
19
      raise TypeError, 'Term cannot be duplicated (immutable, not necessary)'
20
    end
21
22
    class << self
23
      protected :new
24
    end
25
26
    attr_reader :terms
27
28
    def and(*others)
29
      others.unshift self
30
      Term.get AndTerm, *others
31
    end
32
33
    alias_method :&, :and
34
35
    def or(*others)
36
      others.unshift self
37
      Term.get OrTerm, *others
38
    end
39
40
    alias_method :|, :or
41
42
    def not
43
      Term.get NotTerm, self
44
    end
45
46
    alias_method :~, :not
47
    alias_method :-@, :not
48
49
    def then(other)
50
      Term.get ThenTerm, self, other
51
    end
52
53
    alias_method :>>, :then
54
55
    def to_s_in_term
56
      to_s true
57
    end
58
59
    def to_nnf
60
      if nnf?
61
        self
62
      else
63
        Term.get self.class, *@terms.map(&:to_nnf)
64
      end
65
    end
66
67
    def nnf?
68
      false
69
    end
70
71
    def reduce
72
      if reduced?
73
        self
74
      else
75
        Term.get self.class, *@terms.map(&:reduce)
76
      end
77
    end
78
79
    def reduced?
80
      false
81
    end
82
83
    def to_cnf
84
      reduce.to_cnf
85
    end
86
87
    def self.validate_terms(*terms)
88
      raise ArgumentError, 'no terms given' if terms.empty?
89
      terms.map do |term|
90
        case term
91
        when TrueClass
92
          True
93
        when FalseClass
94
          False
95
        when Term
96
          term
97
        else
98
          raise TypeError, "#{term.class} cannot be treated as term"
99
        end
100
      end
101
    end
102
103
    private_class_method :validate_terms
104
105
    def self.generate_cache
106
      Ref::WeakValueMap.new
107
    end
108
109
    private_class_method :generate_cache
110
111
    def self.cached(klass, *terms)
112
      @table ||= generate_cache
113
      key = klass.name + terms.map(&:object_id).join(',')
114
      return @table[key] if @table[key]
115
      ret = klass.__send__ :new, *terms
116
      @table[key] = ret
117
      # kick caching mechanism
118
      ret.variables
119
      ret.freeze
120
    end
121
122
    private_class_method :cached
123
124
    def self.get(klass, *terms)
125
      terms = validate_terms(*terms)
126
      if klass == AndTerm || klass == OrTerm
127
        terms = terms.map { |t| t.is_a?(klass) ? t.terms : t }.flatten
128
        return terms[0] if terms.length == 1
129
      end
130
      cached klass, *terms
131
    end
132
133
    # check if this term is a cnf term.
134
    # @return [Boolean] false unless overridden.
135
    def cnf?
136
      false
137
    end
138
139
    # @return [Array] variables used by this term.
140
    def variables
141
      @variables ||= @terms.map(&:variables).flatten.uniq
142
    end
143
144
    def assign(trues, falses, variables = nil)
145
      # contradicted assignment
146
      raise ArgumentError, 'Contradicted assignment' unless (trues & falses).empty?
147
      variables ||= trues | falses
148
      assigned_terms = terms.map do |term|
149
        if (term.variables & variables).empty?
150
          term
151
        else
152
          term.assign(trues, falses, variables)
153
        end
154
      end
155
      Term.get self.class, *assigned_terms
156
    end
157
158
    def assign_true(*variables)
159
      assign variables, []
160
    end
161
162
    def assign_false(*variables)
163
      assign [], variables
164
    end
165
166
    def sat?
167
      PropLogic.sat_solver.call(self)
168
    end
169
170
    # loop with each satisfied terms.
171
    # @return [Enumerator] if block is not given.
172
    # @return [nil] if block is given.
173
    def each_sat
174
      return to_enum(:each_sat) unless block_given?
175
      sat_loop(self) do |sat, solver|
176
        yield sat
177
        negated_vars = sat.terms.map do |t|
178
          t.is_a?(NotTerm) ? t.terms[0] : ~t
179
        end
180
        solver << PropLogic.all_or(*negated_vars)
181
      end
182
    end
183
184
    def unsat?
185
      sat? == false
186
    end
187
188
    def equiv?(other)
189
      ((self | other) & (~self | ~other)).unsat?
190
    end
191
192
    private
193
194
    # checking methods
195
196
    def check_term_uniqueness
197
      @is_reduced &&= (@terms.length == @terms.uniq.length)
198
    end
199
200
    def check_ambivalent_vars
201
      return unless @is_reduced
202
      term_by_class = @terms.group_by(&:class)
203
      return if term_by_class[NotTerm].nil? || term_by_class[Variable].nil?
204
      negated_variales = term_by_class[NotTerm].map { |t| t.terms[0] }
205
      @is_reduced = false unless (negated_variales & term_by_class[Variable]).empty?
206
    end
207
208
    def check_nnf_reduced
209
      @is_reduced = true
210
      @is_nnf = @terms.all? do |term|
211
        @is_reduced &&= !term.is_a?(Constant) && term.reduced?
212
        @is_reduced &&= !term.is_a?(NotTerm) || term.terms[0].is_a?(Variable)
213
        next true if @is_reduced
214
        term.nnf?
215
      end
216
      return unless @is_reduced
217
      check_term_uniqueness
218
      check_ambivalent_vars
219
    end
220
221
  end
222
end
223