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 }