001    package aima.logic.fol;
002    
003    import java.util.ArrayList;
004    import java.util.LinkedHashMap;
005    import java.util.LinkedHashSet;
006    import java.util.List;
007    import java.util.Map;
008    import java.util.Set;
009    
010    import aima.logic.fol.kb.data.CNF;
011    import aima.logic.fol.kb.data.Clause;
012    import aima.logic.fol.parsing.FOLParser;
013    import aima.logic.fol.parsing.FOLVisitor;
014    import aima.logic.fol.parsing.ast.ConnectedSentence;
015    import aima.logic.fol.parsing.ast.Constant;
016    import aima.logic.fol.parsing.ast.Function;
017    import aima.logic.fol.parsing.ast.NotSentence;
018    import aima.logic.fol.parsing.ast.Predicate;
019    import aima.logic.fol.parsing.ast.QuantifiedSentence;
020    import aima.logic.fol.parsing.ast.Sentence;
021    import aima.logic.fol.parsing.ast.Term;
022    import aima.logic.fol.parsing.ast.TermEquality;
023    import aima.logic.fol.parsing.ast.Variable;
024    
025    /**
026     * Artificial Intelligence A Modern Approach (2nd Edition): page 296.
027     * Every sentence of first-order logic can be converted into an inferentially
028     * equivalent CNF sentence.
029     * 
030     * Note: Transformation rules extracted from pg 215, 296, and 297, which
031     * is essentially the INSEADO method outlined in:
032     * http://logic.stanford.edu/classes/cs157/2008/lectures/lecture09.pdf
033     */
034    
035    /**
036     * @author Ciaran O'Reilly
037     * 
038     */
039    public class CNFConverter {
040    
041            private FOLParser parser = null;
042            private SubstVisitor substVisitor;
043    
044            public CNFConverter(FOLParser parser) {
045                    this.parser = parser;
046    
047                    this.substVisitor = new SubstVisitor();
048            }
049    
050            public CNF convertToCNF(Sentence aSentence) {           
051                    // I)mplications Out:
052                    Sentence implicationsOut = (Sentence) aSentence.accept(
053                                    new ImplicationsOut(), null);
054                    
055                    // N)egations In:
056                    Sentence negationsIn = (Sentence) implicationsOut.accept(
057                                    new NegationsIn(), null);
058    
059                    // S)tandardize variables: 
060                    // For sentences like:
061                    // (FORALL x P(x)) V (EXISTS x Q(x)),
062                    // which use the same variable name twice, change the name of one of the
063                    // variables.
064                    Sentence saQuantifiers = (Sentence) negationsIn.accept(
065                                    new StandardizeQuantiferVariables(substVisitor),
066                                    new LinkedHashSet<Variable>());
067    
068                    // Remove explicit quantifiers, by skolemizing existentials 
069                    // and dropping universals:
070                    // E)xistentials Out
071                    // A)lls Out:
072                    Sentence andsAndOrs = (Sentence) saQuantifiers.accept(
073                                    new RemoveQuantifiers(parser), new LinkedHashSet<Variable>());
074    
075                    // D)istribution
076                    // V over ^:
077                    Sentence orDistributedOverAnd = (Sentence) andsAndOrs.accept(
078                                    new DistributeOrOverAnd(), null);
079    
080                    // O)perators Out 
081                    return (new CNFConstructor()).construct(orDistributedOverAnd);
082            }
083    }
084    
085    class ImplicationsOut implements FOLVisitor {
086            public ImplicationsOut() {
087    
088            }
089    
090            public Object visitPredicate(Predicate p, Object arg) {
091                    return p;
092            }
093    
094            public Object visitTermEquality(TermEquality equality, Object arg) {
095                    return equality;
096            }
097    
098            public Object visitVariable(Variable variable, Object arg) {
099                    return variable;
100            }
101    
102            public Object visitConstant(Constant constant, Object arg) {
103                    return constant;
104            }
105    
106            public Object visitFunction(Function function, Object arg) {
107                    return function;
108            }
109    
110            public Object visitNotSentence(NotSentence notSentence, Object arg) {
111                    Sentence negated = notSentence.getNegated();
112    
113                    return new NotSentence((Sentence) negated.accept(this, arg));
114            }
115    
116            public Object visitConnectedSentence(ConnectedSentence sentence, Object arg) {
117                    Sentence alpha = (Sentence) sentence.getFirst().accept(this, arg);
118                    Sentence beta = (Sentence) sentence.getSecond().accept(this, arg);
119    
120                    // Eliminate <=>, bi-conditional elimination,
121                    // replace (alpha <=> beta) with (~alpha V beta) ^ (alpha V ~beta).
122                    if (Connectors.isBICOND(sentence.getConnector())) {                     
123                            Sentence first = new ConnectedSentence(
124                                            Connectors.OR, new NotSentence(alpha), beta);
125                            Sentence second = new ConnectedSentence(
126                                            Connectors.OR, alpha, new NotSentence(beta));
127    
128                            return new ConnectedSentence(Connectors.AND, first, second);
129                    }
130    
131                    // Eliminate =>, implication elimination,
132                    // replacing (alpha => beta) with (~alpha V beta)
133                    if (Connectors.isIMPLIES(sentence.getConnector())) {
134                            return new ConnectedSentence(Connectors.OR, new NotSentence(alpha), beta);
135                    }
136    
137                    return new ConnectedSentence(sentence.getConnector(), alpha, beta);
138            }
139    
140            public Object visitQuantifiedSentence(QuantifiedSentence sentence,
141                            Object arg) {
142    
143                    return new QuantifiedSentence(sentence.getQuantifier(), sentence
144                                    .getVariables(), (Sentence) sentence.getQuantified().accept(
145                                    this, arg));
146            }
147    }
148    
149    class NegationsIn implements FOLVisitor {
150            public NegationsIn() {
151    
152            }
153    
154            public Object visitPredicate(Predicate p, Object arg) {
155                    return p;
156            }
157    
158            public Object visitTermEquality(TermEquality equality, Object arg) {
159                    return equality;
160            }
161    
162            public Object visitVariable(Variable variable, Object arg) {
163                    return variable;
164            }
165    
166            public Object visitConstant(Constant constant, Object arg) {
167                    return constant;
168            }
169    
170            public Object visitFunction(Function function, Object arg) {
171                    return function;
172            }
173    
174            public Object visitNotSentence(NotSentence notSentence, Object arg) {
175                    // CNF requires NOT (~) to appear only in literals, so we 'move ~
176                    // inwards' by repeated application of the following equivalences:
177                    Sentence negated = notSentence.getNegated();
178    
179                    // ~(~alpha) equivalent to alpha (double negation elimination)
180                    if (negated instanceof NotSentence) {
181                            return ((NotSentence) negated).getNegated().accept(this, arg);
182                    }
183    
184                    if (negated instanceof ConnectedSentence) {
185                            ConnectedSentence negConnected = (ConnectedSentence) negated;
186                            Sentence alpha = negConnected.getFirst();
187                            Sentence beta = negConnected.getSecond();
188                            // ~(alpha ^ beta) equivalent to (~alpha V ~beta) (De Morgan)
189                            if (Connectors.isAND(negConnected.getConnector())) {
190                                    // I need to ensure the ~s are moved in deeper
191                                    Sentence notAlpha = (Sentence) (new NotSentence(
192                                                    (Sentence) alpha)).accept(this, arg);
193                                    Sentence notBeta = (Sentence) (new NotSentence((Sentence) beta))
194                                                    .accept(this, arg);
195                                    return new ConnectedSentence(Connectors.OR, notAlpha, notBeta);
196                            }
197    
198                            // ~(alpha V beta) equivalent to (~alpha ^ ~beta) (De Morgan)
199                            if (Connectors.isOR(negConnected.getConnector())) {
200                                    // I need to ensure the ~s are moved in deeper
201                                    Sentence notAlpha = (Sentence) (new NotSentence(
202                                                    (Sentence) alpha)).accept(this, arg);
203                                    Sentence notBeta = (Sentence) (new NotSentence((Sentence) beta))
204                                                    .accept(this, arg);
205                                    return new ConnectedSentence(Connectors.AND, notAlpha, notBeta);
206                            }
207                    }
208    
209                    // in addition, rules for negated quantifiers:
210                    if (negated instanceof QuantifiedSentence) {
211                            QuantifiedSentence negQuantified = (QuantifiedSentence) negated;
212                            // I need to ensure the ~ is moved in deeper
213                            Sentence notP = (Sentence) (new NotSentence(negQuantified
214                                            .getQuantified())).accept(this, arg);
215    
216                            // ~FORALL x p becomes EXISTS x ~p
217                            if (Quantifiers.isFORALL(negQuantified.getQuantifier())) {
218                                    return new QuantifiedSentence(Quantifiers.EXISTS, negQuantified
219                                                    .getVariables(), notP);
220                            }
221    
222                            // ~EXISTS x p becomes FORALL x ~p
223                            if (Quantifiers.isEXISTS(negQuantified.getQuantifier())) {
224                                    return new QuantifiedSentence(Quantifiers.FORALL, negQuantified
225                                                    .getVariables(), notP);
226                            }
227                    }
228    
229                    return new NotSentence((Sentence) negated.accept(this, arg));
230            }
231    
232            public Object visitConnectedSentence(ConnectedSentence sentence, Object arg) {
233                    return new ConnectedSentence(sentence.getConnector(), (Sentence) sentence.getFirst()
234                                    .accept(this, arg), (Sentence) sentence.getSecond().accept(this, arg));
235            }
236    
237            public Object visitQuantifiedSentence(QuantifiedSentence sentence,
238                            Object arg) {
239    
240                    return new QuantifiedSentence(sentence.getQuantifier(), sentence
241                                    .getVariables(), (Sentence) sentence.getQuantified().accept(
242                                    this, arg));
243            }
244    }
245    
246    class StandardizeQuantiferVariables implements FOLVisitor {
247            // Just use a localized indexical here.
248            private StandardizeApartIndexical quantifiedIndexical = new StandardizeApartIndexical() {
249                    private int index = 0;
250    
251                    public String getPrefix() {
252                            return "q";
253                    }
254    
255                    public int getNextIndex() {
256                            return index++;
257                    }
258            };
259            
260            private SubstVisitor substVisitor = null;
261    
262            public StandardizeQuantiferVariables(SubstVisitor substVisitor) {
263                    this.substVisitor = substVisitor;
264            }
265    
266            public Object visitPredicate(Predicate p, Object arg) {
267                    return p;
268            }
269    
270            public Object visitTermEquality(TermEquality equality, Object arg) {
271                    return equality;
272            }
273    
274            public Object visitVariable(Variable variable, Object arg) {
275                    return variable;
276            }
277    
278            public Object visitConstant(Constant constant, Object arg) {
279                    return constant;
280            }
281    
282            public Object visitFunction(Function function, Object arg) {
283                    return function;
284            }
285    
286            public Object visitNotSentence(NotSentence sentence, Object arg) {
287                    return new NotSentence((Sentence) sentence.getNegated().accept(this,
288                                    arg));
289            }
290    
291            public Object visitConnectedSentence(ConnectedSentence sentence, Object arg) {
292                    return new ConnectedSentence(sentence.getConnector(),
293                                    (Sentence) sentence.getFirst().accept(this, arg),
294                                    (Sentence) sentence.getSecond().accept(this, arg));
295            }
296    
297            @SuppressWarnings("unchecked")
298            public Object visitQuantifiedSentence(QuantifiedSentence sentence,
299                            Object arg) {
300                    Set<Variable> seenSoFar = (Set<Variable>) arg;
301    
302                    // Keep track of what I have to subst locally and
303                    // what my renamed variables will be.
304                    Map<Variable, Term> localSubst = new LinkedHashMap<Variable, Term>();
305                    List<Variable> replVariables = new ArrayList<Variable>();
306                    for (Variable v : sentence.getVariables()) {
307                            // If local variable has be renamed already
308                            // then I need to come up with own name
309                            if (seenSoFar.contains(v)) {
310                                    Variable sV = new Variable(quantifiedIndexical.getPrefix()
311                                                    + quantifiedIndexical.getNextIndex());
312                                    localSubst.put(v, sV);
313                                    // Replacement variables should contain new name for variable
314                                    replVariables.add(sV);
315                            } else {
316                                    // Not already replaced, this name is good
317                                    replVariables.add(v);
318                            }
319                    }
320    
321                    // Apply the local subst
322                    Sentence subst = substVisitor.subst(localSubst, sentence.getQuantified());
323                    
324                    // Ensure all my existing and replaced variable
325                    // names are tracked
326                    seenSoFar.addAll(replVariables);
327                    
328                    Sentence sQuantified = (Sentence) subst.accept(this, arg);
329    
330                    return new QuantifiedSentence(sentence.getQuantifier(), replVariables,
331                                    sQuantified);
332            }
333    }
334    
335    class RemoveQuantifiers implements FOLVisitor {
336    
337            private FOLParser parser = null;
338            private SubstVisitor substVisitor = null;
339    
340            public RemoveQuantifiers(FOLParser parser) {
341                    this.parser = parser;
342    
343                    substVisitor = new SubstVisitor();
344            }
345    
346            public Object visitPredicate(Predicate p, Object arg) {
347                    return p;
348            }
349    
350            public Object visitTermEquality(TermEquality equality, Object arg) {
351                    return equality;
352            }
353    
354            public Object visitVariable(Variable variable, Object arg) {
355                    return variable;
356            }
357    
358            public Object visitConstant(Constant constant, Object arg) {
359                    return constant;
360            }
361    
362            public Object visitFunction(Function function, Object arg) {
363                    return function;
364            }
365    
366            public Object visitNotSentence(NotSentence sentence, Object arg) {
367                    return new NotSentence((Sentence) sentence.getNegated().accept(this,
368                                    arg));
369            }
370    
371            public Object visitConnectedSentence(ConnectedSentence sentence, Object arg) {
372                    return new ConnectedSentence(sentence.getConnector(),
373                                    (Sentence) sentence.getFirst().accept(this, arg),
374                                    (Sentence) sentence.getSecond().accept(this, arg));
375            }
376    
377            @SuppressWarnings("unchecked")
378            public Object visitQuantifiedSentence(QuantifiedSentence sentence,
379                            Object arg) {
380                    Sentence quantified = sentence.getQuantified();
381                    Set<Variable> universalScope = (Set<Variable>) arg;
382    
383                    // Skolemize: Skolemization is the process of removing existential
384                    // quantifiers by elimination. This is done by introducing Skolem
385                    // functions. The general rule is that the arguments of the Skolem
386                    // function are all the universally quantified variables in whose
387                    // scope the existential quantifier appears.
388                    if (Quantifiers.isEXISTS(sentence.getQuantifier())) {
389                            Map<Variable, Term> skolemSubst = new LinkedHashMap<Variable, Term>();
390                            for (Variable eVar : sentence.getVariables()) {
391                                    if (universalScope.size() > 0) {
392                                            // Replace with a Skolem Function
393                                            String skolemFunctionName = parser.getFOLDomain()
394                                                            .addSkolemFunction();
395                                            skolemSubst.put(eVar, new Function(skolemFunctionName,
396                                                            new ArrayList<Term>(universalScope)));
397                                    } else {
398                                            // Replace with a Skolem Constant
399                                            String skolemConstantName = parser.getFOLDomain()
400                                                            .addSkolemConstant();
401                                            skolemSubst.put(eVar, new Constant(skolemConstantName));
402                                    }
403                            }
404    
405                            Sentence skolemized = substVisitor.subst(skolemSubst, quantified);
406                            return (Sentence) skolemized.accept(this, arg);
407                    }
408    
409                    // Drop universal quantifiers.
410                    if (Quantifiers.isFORALL(sentence.getQuantifier())) {
411                            // Add to the universal scope so that
412                            // existential skolemization may be done correctly
413                            universalScope.addAll(sentence.getVariables());
414    
415                            Sentence droppedUniversal = (Sentence) quantified.accept(this, arg);
416    
417                            // Enusre my scope is removed before moving back up
418                            // the call stack when returning
419                            universalScope.removeAll(sentence.getVariables());
420    
421                            return droppedUniversal;
422                    }
423    
424                    // Should not reach here as have already
425                    // handled the two quantifiers.
426                    throw new IllegalStateException("Unhandled Quantifier:"
427                                    + sentence.getQuantifier());
428            }
429    }
430    
431    class DistributeOrOverAnd implements FOLVisitor {
432    
433            public DistributeOrOverAnd() {
434    
435            }
436    
437            public Object visitPredicate(Predicate p, Object arg) {
438                    return p;
439            }
440    
441            public Object visitTermEquality(TermEquality equality, Object arg) {
442                    return equality;
443            }
444    
445            public Object visitVariable(Variable variable, Object arg) {
446                    return variable;
447            }
448    
449            public Object visitConstant(Constant constant, Object arg) {
450                    return constant;
451            }
452    
453            public Object visitFunction(Function function, Object arg) {
454                    return function;
455            }
456    
457            public Object visitNotSentence(NotSentence sentence, Object arg) {
458                    return new NotSentence((Sentence) sentence.getNegated().accept(this,
459                                    arg));
460            }
461    
462            public Object visitConnectedSentence(ConnectedSentence sentence, Object arg) {
463                    // Distribute V over ^:
464    
465                    // This will cause flattening out of nested ^s and Vs
466                    Sentence alpha = (Sentence) sentence.getFirst().accept(this, arg);
467                    Sentence beta = (Sentence) sentence.getSecond().accept(this, arg);
468    
469                    // (alpha V (beta ^ gamma)) equivalent to
470                    // ((alpha V beta) ^ (alpha V gamma))
471                    if (Connectors.isOR(sentence.getConnector())
472                                    && ConnectedSentence.class.isInstance(beta)) {
473                            ConnectedSentence betaAndGamma = (ConnectedSentence) beta;
474                            if (Connectors.isAND(betaAndGamma.getConnector())) {
475                                    beta = betaAndGamma.getFirst();
476                                    Sentence gamma = betaAndGamma.getSecond();
477                                    return new ConnectedSentence(Connectors.AND,
478                                                    (Sentence) (new ConnectedSentence(Connectors.OR, alpha,
479                                                                    beta)).accept(this, arg),
480                                                    (Sentence) (new ConnectedSentence(Connectors.OR, alpha,
481                                                                    gamma)).accept(this, arg));
482                            }
483                    }
484    
485                    // ((alpha ^ gamma) V beta) equivalent to
486                    // ((alpha V beta) ^ (gamma V beta))
487                    if (Connectors.isOR(sentence.getConnector())
488                                    && ConnectedSentence.class.isInstance(alpha)) {
489                            ConnectedSentence alphaAndGamma = (ConnectedSentence) alpha;
490                            if (Connectors.isAND(alphaAndGamma.getConnector())) {
491                                    alpha = alphaAndGamma.getFirst();
492                                    Sentence gamma = alphaAndGamma.getSecond();
493                                    return new ConnectedSentence(Connectors.AND,
494                                                    (Sentence) (new ConnectedSentence(Connectors.OR, alpha,
495                                                                    beta)).accept(this, arg),
496                                                    (Sentence) (new ConnectedSentence(Connectors.OR, gamma,
497                                                                    beta)).accept(this, arg));
498                            }
499                    }
500    
501                    return new ConnectedSentence(sentence.getConnector(), alpha, beta);
502            }
503    
504            public Object visitQuantifiedSentence(QuantifiedSentence sentence,
505                            Object arg) {
506                    // This should not be called as should have already
507                    // removed all of the quantifiers.
508                    throw new IllegalStateException(
509                                    "All quantified sentences should have already been removed.");
510            }
511    }
512    
513    class CNFConstructor implements FOLVisitor {    
514            public CNFConstructor() {
515    
516            }
517    
518            public CNF construct(Sentence orDistributedOverAnd) {
519                    ArgData ad = new ArgData();
520    
521                    orDistributedOverAnd.accept(this, ad);
522    
523                    return new CNF(ad.clauses);
524            }
525    
526            public Object visitPredicate(Predicate p, Object arg) {
527                    ArgData ad = (ArgData) arg;
528                    if (ad.negated) {
529                            ad.clauses.get(ad.clauses.size() - 1).addNegativeLiteral(p);
530                    } else {
531                            ad.clauses.get(ad.clauses.size() - 1).addPositiveLiteral(p);
532                    }
533                    return p;
534            }
535    
536            public Object visitTermEquality(TermEquality equality, Object arg) {
537                    ArgData ad = (ArgData) arg;
538                    if (ad.negated) {
539                            ad.clauses.get(ad.clauses.size() - 1).addNegativeLiteral(equality);
540                    } else {
541                            ad.clauses.get(ad.clauses.size() - 1).addPositiveLiteral(equality);
542                    }
543                    return equality;
544            }
545    
546            public Object visitVariable(Variable variable, Object arg) {
547                    // This should not be called
548                    throw new IllegalStateException("visitVariable() should not be called.");
549            }
550    
551            public Object visitConstant(Constant constant, Object arg) {
552                    // This should not be called
553                    throw new IllegalStateException("visitConstant() should not be called.");
554            }
555    
556            public Object visitFunction(Function function, Object arg) {
557                    // This should not be called
558                    throw new IllegalStateException("visitFunction() should not be called.");
559            }
560    
561            public Object visitNotSentence(NotSentence sentence, Object arg) {
562                    ArgData ad = (ArgData) arg;
563                    // Indicate that the enclosed predicate is negated
564                    ad.negated = true;
565                    sentence.getNegated().accept(this, arg);
566                    ad.negated = false;
567    
568                    return sentence;
569            }
570    
571            public Object visitConnectedSentence(ConnectedSentence sentence, Object arg) {
572                    ArgData ad = (ArgData) arg;
573                    Sentence first = sentence.getFirst();
574                    Sentence second = sentence.getSecond();
575    
576                    first.accept(this, arg);
577                    if (Connectors.isAND(sentence.getConnector())) {
578                            ad.clauses.add(new Clause());
579                    }
580                    second.accept(this, arg);
581    
582                    return sentence;
583            }
584    
585            public Object visitQuantifiedSentence(QuantifiedSentence sentence,
586                            Object arg) {
587                    // This should not be called as should have already
588                    // removed all of the quantifiers.
589                    throw new IllegalStateException(
590                                    "All quantified sentences should have already been removed.");
591            }
592    
593            class ArgData {
594                    public List<Clause> clauses = new ArrayList<Clause>();
595                    public boolean negated = false;
596    
597                    public ArgData() {
598                            clauses.add(new Clause());
599                    }
600            }
601    }