Package spade :: Module logic
[hide private]
[frames] | no frames]

Source Code for Module spade.logic

   1  """Representations and Inference for Logic (Chapters 7-10) 
   2   
   3  Covers both Propositional and First-Order Logic. First we have four 
   4  important data types: 
   5   
   6      KB            Abstract class holds a knowledge base of logical expressions 
   7      KB_Agent      Abstract class subclasses agents.Agent 
   8      Expr          A logical expression 
   9      substitution  Implemented as a dictionary of var:value pairs, {x:1, y:x} 
  10   
  11  Be careful: some functions take an Expr as argument, and some take a KB. 
  12  Then we implement various functions for doing logical inference: 
  13   
  14      pl_true          Evaluate a propositional logical sentence in a model 
  15      tt_entails       Say if a statement is entailed by a KB 
  16      pl_resolution    Do resolution on propositional sentences 
  17      dpll_satisfiable See if a propositional sentence is satisfiable 
  18      WalkSAT          (not yet implemented) 
  19   
  20  And a few other functions: 
  21   
  22      to_cnf           Convert to conjunctive normal form 
  23      unify            Do unification of two FOL sentences 
  24      diff, simp       Symbolic differentiation and simplification              
  25  """ 
  26   
  27  from __future__ import generators 
  28  import re 
  29   
30 -def every(predicate, seq):
31 """True if every element of seq satisfies predicate. 32 >>> every(callable, [min, max]) 33 1 34 >>> every(callable, [min, 3]) 35 0 36 """ 37 for x in seq: 38 if not predicate(x): return False 39 return True
40
41 -def isnumber(x):
42 "Is x a number? We say it is if it has a __int__ method." 43 return hasattr(x, '__int__')
44
45 -def issequence(x):
46 "Is x a sequence? We say it is if it has a __getitem__ method." 47 return hasattr(x, '__getitem__')
48
49 -def num_or_str(x):
50 """The argument is a string; convert to a number if possible, or strip it. 51 >>> num_or_str('42') 52 42 53 >>> num_or_str(' 42x ') 54 '42x' 55 """ 56 if isnumber(x): return x 57 try: 58 return int(x) 59 except ValueError: 60 try: 61 return float(x) 62 except ValueError: 63 return str(x).strip()
64 65 #______________________________________________________________________________ 66
67 -class KB:
68 """A Knowledge base to which you can tell and ask sentences. 69 To create a KB, first subclass this class and implement 70 tell, ask_generator, and retract. Why ask_generator instead of ask? 71 The book is a bit vague on what ask means -- 72 For a Propositional Logic KB, ask(P & Q) returns True or False, but for an 73 FOL KB, something like ask(Brother(x, y)) might return many substitutions 74 such as {x: Cain, y: Abel}, {x: Abel, y: Cain}, {x: George, y: Jeb}, etc. 75 So ask_generator generates these one at a time, and ask either returns the 76 first one or returns False.""" 77
78 - def __init__(self, sentence=None):
79 abstract
80
81 - def tell(self, sentence):
82 "Add the sentence to the KB" 83 abstract
84
85 - def ask(self, query):
86 """Ask returns a substitution that makes the query true, or 87 it returns False. It is implemented in terms of ask_generator.""" 88 if issubclass(query.__class__,str): 89 query = expr(query) 90 try: 91 return self.ask_generator(query).next() 92 except StopIteration: 93 return False
94
95 - def ask_generator(self, query):
96 "Yield all the substitutions that make query true." 97 abstract 98
99 - def retract(self, sentence):
100 "Remove the sentence from the KB" 101 abstract
102 103
104 -class PropKB(KB):
105 "A KB for Propositional Logic. Inefficient, with no indexing." 106
107 - def __init__(self, sentence=None):
108 self.clauses = [] 109 if sentence: 110 self.tell(sentence)
111
112 - def tell(self, sentence):
113 "Add the sentence's clauses to the KB" 114 self.clauses.extend(conjuncts(to_cnf(sentence)))
115
116 - def ask_generator(self, query):
117 "Yield the empty substitution if KB implies query; else False" 118 if not tt_entails(Expr('&', *self.clauses), query): 119 return 120 yield {} 121
122 - def retract(self, sentence):
123 "Remove the sentence's clauses from the KB" 124 for c in conjuncts(to_cnf(sentence)): 125 if c in self.clauses: 126 self.clauses.remove(c)
127 128 #______________________________________________________________________________ 129 130 #class KB_Agent(agents.Agent): 131 """A generic logical knowledge-based agent. [Fig. 7.1]""" 132 """def __init__(self, KB): 133 t = 0 134 def program(percept): 135 KB.tell(self.make_percept_sentence(percept, t)) 136 action = KB.ask(self.make_action_query(t)) 137 KB.tell(self.make_action_sentence(action, t)) 138 t = t + 1 139 return action 140 self.program = program 141 142 def make_percept_sentence(self, percept, t): 143 return(Expr("Percept")(percept, t)) 144 145 def make_action_query(self, t): 146 return(expr("ShouldDo(action, %d)" % t)) 147 148 def make_action_sentence(self, action, t): 149 return(Expr("Did")(action, t))""" 150 151 #______________________________________________________________________________ 152
153 -class Expr:
154 """A symbolic mathematical expression. We use this class for logical 155 expressions, and for terms within logical expressions. In general, an 156 Expr has an op (operator) and a list of args. The op can be: 157 Null-ary (no args) op: 158 A number, representing the number itself. (e.g. Expr(42) => 42) 159 A symbol, representing a variable or constant (e.g. Expr('F') => F) 160 Unary (1 arg) op: 161 '~', '-', representing NOT, negation (e.g. Expr('~', Expr('P')) => ~P) 162 Binary (2 arg) op: 163 '>>', '<<', representing forward and backward implication 164 '+', '-', '*', '/', '**', representing arithmetic operators 165 '<', '>', '>=', '<=', representing comparison operators 166 '<=>', '^', representing logical equality and XOR 167 N-ary (0 or more args) op: 168 '&', '|', representing conjunction and disjunction 169 A symbol, representing a function term or FOL proposition 170 171 Exprs can be constructed with operator overloading: if x and y are Exprs, 172 then so are x + y and x & y, etc. Also, if F and x are Exprs, then so is 173 F(x); it works by overloading the __call__ method of the Expr F. Note 174 that in the Expr that is created by F(x), the op is the str 'F', not the 175 Expr F. See http://www.python.org/doc/current/ref/specialnames.html 176 to learn more about operator overloading in Python. 177 178 WARNING: x == y and x != y are NOT Exprs. The reason is that we want 179 to write code that tests 'if x == y:' and if x == y were the same 180 as Expr('==', x, y), then the result would always be true; not what a 181 programmer would expect. But we still need to form Exprs representing 182 equalities and disequalities. We concentrate on logical equality (or 183 equivalence) and logical disequality (or XOR). You have 3 choices: 184 (1) Expr('<=>', x, y) and Expr('^', x, y) 185 Note that ^ is bitwose XOR in Python (and Java and C++) 186 (2) expr('x <=> y') and expr('x =/= y'). 187 See the doc string for the function expr. 188 (3) (x % y) and (x ^ y). 189 It is very ugly to have (x % y) mean (x <=> y), but we need 190 SOME operator to make (2) work, and this seems the best choice. 191 192 WARNING: if x is an Expr, then so is x + 1, because the int 1 gets 193 coerced to an Expr by the constructor. But 1 + x is an error, because 194 1 doesn't know how to add an Expr. (Adding an __radd__ method to Expr 195 wouldn't help, because int.__add__ is still called first.) Therefore, 196 you should use Expr(1) + x instead, or ONE + x, or expr('1 + x'). 197 """ 198
199 - def __init__(self, op, *args):
200 "Op is a string or number; args are Exprs (or are coerced to Exprs)." 201 assert isinstance(op, str) or (isnumber(op) and not args) 202 self.op = num_or_str(op) 203 self.args = map(expr, args) ## Coerce args to Exprs
204
205 - def __call__(self, *args):
206 """Self must be a symbol with no args, such as Expr('F'). Create a new 207 Expr with 'F' as op and the args as arguments.""" 208 assert is_symbol(self.op) and not self.args 209 return Expr(self.op, *args)
210
211 - def __repr__(self):
212 "Show something like 'P' or 'P(x, y)', or '~P' or '(P | Q | R)'" 213 if len(self.args) == 0: # Constant or proposition with arity 0 214 return str(self.op) 215 elif is_symbol(self.op): # Functional or Propositional operator 216 return '%s(%s)' % (self.op, ', '.join(map(repr, self.args))) 217 elif len(self.args) == 1: # Prefix operator 218 return self.op + repr(self.args[0]) 219 else: # Infix operator 220 return '(%s)' % (' '+self.op+' ').join(map(repr, self.args))
221
222 - def __eq__(self, other):
223 """x and y are equal iff their ops and args are equal.""" 224 return (other is self) or (isinstance(other, Expr) 225 and self.op == other.op and self.args == other.args)
226
227 - def __hash__(self):
228 "Need a hash method so Exprs can live in dicts." 229 return hash(self.op) ^ hash(tuple(self.args))
230 231 # See http://www.python.org/doc/current/lib/module-operator.html 232 # Not implemented: not, abs, pos, concat, contains, *item, *slice
233 - def __lt__(self, other): return Expr('<', self, other)
234 - def __le__(self, other): return Expr('<=', self, other)
235 - def __ge__(self, other): return Expr('>=', self, other)
236 - def __gt__(self, other): return Expr('>', self, other)
237 - def __add__(self, other): return Expr('+', self, other)
238 - def __sub__(self, other): return Expr('-', self, other)
239 - def __and__(self, other): return Expr('&', self, other)
240 - def __div__(self, other): return Expr('/', self, other)
241 - def __truediv__(self, other):return Expr('/', self, other)
242 - def __invert__(self): return Expr('~', self)
243 - def __lshift__(self, other): return Expr('<<', self, other)
244 - def __rshift__(self, other): return Expr('>>', self, other)
245 - def __mul__(self, other): return Expr('*', self, other)
246 - def __neg__(self): return Expr('-', self)
247 - def __or__(self, other): return Expr('|', self, other)
248 - def __pow__(self, other): return Expr('**', self, other)
249 - def __xor__(self, other): return Expr('^', self, other)
250 - def __mod__(self, other): return Expr('<=>', self, other) ## (x % y)
251 252 253
254 -def expr(s):
255 """Create an Expr representing a logic expression by parsing the input 256 string. Symbols and numbers are automatically converted to Exprs. 257 In addition you can use alternative spellings of these operators: 258 'x ==> y' parses as (x >> y) # Implication 259 'x <== y' parses as (x << y) # Reverse implication 260 'x <=> y' parses as (x % y) # Logical equivalence 261 'x =/= y' parses as (x ^ y) # Logical disequality (xor) 262 But BE CAREFUL; precedence of implication is wrong. expr('P & Q ==> R & S') 263 is ((P & (Q >> R)) & S); so you must use expr('(P & Q) ==> (R & S)'). 264 >>> expr('P <=> Q(1)') 265 (P <=> Q(1)) 266 >>> expr('P & Q | ~R(x, F(x))') 267 ((P & Q) | ~R(x, F(x))) 268 """ 269 if isinstance(s, Expr): return s 270 if isnumber(s): return Expr(s) 271 ## Replace the alternative spellings of operators with canonical spellings 272 s = s.replace('==>', '>>').replace('<==', '<<') 273 s = s.replace('<=>', '%').replace('=/=', '^') 274 ## Replace a symbol or number, such as 'P' with 'Expr("P")' 275 s = re.sub(r'([a-zA-Z0-9_.]+)', r'Expr("\1")', s) 276 ## Now eval the string. (A security hole; do not use with an adversary.) 277 return eval(s, {'Expr':Expr})
278
279 -def is_symbol(s):
280 "A string s is a symbol if it starts with an alphabetic char." 281 return isinstance(s, str) and s[0].isalpha()
282
283 -def is_var_symbol(s):
284 "A logic variable symbol is an initial-lowercase string." 285 return is_symbol(s) and s[0].islower()
286
287 -def is_prop_symbol(s):
288 """A proposition logic symbol is an initial-uppercase string other than 289 TRUE or FALSE.""" 290 return is_symbol(s) and s[0].isupper() and s != 'TRUE' and s != 'FALSE'
291
292 -def is_positive(s):
293 """s is an unnegated logical expression 294 >>> is_positive(expr('F(A, B)')) 295 True 296 >>> is_positive(expr('~F(A, B)')) 297 False 298 """ 299 return s.op != '~'
300
301 -def is_negative(s):
302 """s is a negated logical expression 303 >>> is_negative(expr('F(A, B)')) 304 False 305 >>> is_negative(expr('~F(A, B)')) 306 True 307 """ 308 return s.op == '~'
309
310 -def is_literal(s):
311 """s is a FOL literal 312 >>> is_literal(expr('~F(A, B)')) 313 True 314 >>> is_literal(expr('F(A, B)')) 315 True 316 >>> is_literal(expr('F(A, B) & G(B, C)')) 317 False 318 """ 319 return is_symbol(s.op) or (s.op == '~' and is_literal(s.args[0]))
320
321 -def literals(s):
322 """returns the list of literals of logical expression s. 323 >>> literals(expr('F(A, B)')) 324 [F(A, B)] 325 >>> literals(expr('~F(A, B)')) 326 [~F(A, B)] 327 >>> literals(expr('(F(A, B) & G(B, C)) ==> R(A, C)')) 328 [F(A, B), G(B, C), R(A, C)] 329 """ 330 op = s.op 331 if op in set(['&', '|', '<<', '>>', '%', '^']): 332 result = [] 333 for arg in s.args: 334 result.extend(literals(arg)) 335 return result 336 elif is_literal(s): 337 return [s] 338 else: 339 return []
340
341 -def variables(s):
342 """returns the set of variables in logical expression s. 343 >>> ppset(variables(F(x, A, y))) 344 set([x, y]) 345 >>> ppset(variables(expr('F(x, x) & G(x, y) & H(y, z) & R(A, z, z)'))) 346 set([x, y, z]) 347 """ 348 if is_literal(s): 349 return set([v for v in s.args if is_variable(v)]) 350 else: 351 vars = set([]) 352 for lit in literals(s): 353 vars = vars.union(variables(lit)) 354 return vars
355
356 -def is_definite_clause(s):
357 """returns True for exprs s of the form A & B & ... & C ==> D, 358 where all literals are positive. In clause form, this is 359 ~A | ~B | ... | ~C | D, where exactly one clause is positive. 360 >>> is_definite_clause(expr('Farmer(Mac)')) 361 True 362 >>> is_definite_clause(expr('~Farmer(Mac)')) 363 False 364 >>> is_definite_clause(expr('(Farmer(f) & Rabbit(r)) ==> Hates(f, r)')) 365 True 366 >>> is_definite_clause(expr('(Farmer(f) & ~Rabbit(r)) ==> Hates(f, r)')) 367 False 368 """ 369 op = s.op 370 return (is_symbol(op) or 371 (op == '>>' and every(is_positive, literals(s))))
372 373 ## Useful constant Exprs used in examples and code: 374 TRUE, FALSE, ZERO, ONE, TWO = map(Expr, ['TRUE', 'FALSE', 0, 1, 2]) 375 A, B, C, F, G, P, Q, x, y, z = map(Expr, 'ABCFGPQxyz') 376 377 #______________________________________________________________________________ 378
379 -def tt_entails(kb, alpha):
380 """Use truth tables to determine if KB entails sentence alpha. [Fig. 7.10] 381 >>> tt_entails(expr('P & Q'), expr('Q')) 382 True 383 """ 384 return tt_check_all(kb, alpha, prop_symbols(kb & alpha), {})
385
386 -def tt_check_all(kb, alpha, symbols, model):
387 "Auxiliary routine to implement tt_entails." 388 if not symbols: 389 if pl_true(kb, model): return pl_true(alpha, model) 390 else: return True 391 assert result != None 392 else: 393 P, rest = symbols[0], symbols[1:] 394 return (tt_check_all(kb, alpha, rest, extend(model, P, True)) and 395 tt_check_all(kb, alpha, rest, extend(model, P, False)))
396
397 -def prop_symbols(x):
398 "Return a list of all propositional symbols in x." 399 if not isinstance(x, Expr): 400 return [] 401 elif is_prop_symbol(x.op): 402 return [x] 403 else: 404 s = set(()) 405 for arg in x.args: 406 for symbol in prop_symbols(arg): 407 s.add(symbol) 408 return list(s)
409
410 -def tt_true(alpha):
411 """Is the sentence alpha a tautology? (alpha will be coerced to an expr.) 412 >>> tt_true(expr("(P >> Q) <=> (~P | Q)")) 413 True 414 """ 415 return tt_entails(TRUE, expr(alpha))
416
417 -def pl_true(exp, model={}):
418 """Return True if the propositional logic expression is true in the model, 419 and False if it is false. If the model does not specify the value for 420 every proposition, this may return None to indicate 'not obvious'; 421 this may happen even when the expression is tautological.""" 422 op, args = exp.op, exp.args 423 if exp == TRUE: 424 return True 425 elif exp == FALSE: 426 return False 427 elif is_prop_symbol(op): 428 return model.get(exp) 429 elif op == '~': 430 p = pl_true(args[0], model) 431 if p == None: return None 432 else: return not p 433 elif op == '|': 434 result = False 435 for arg in args: 436 p = pl_true(arg, model) 437 if p == True: return True 438 if p == None: result = None 439 return result 440 elif op == '&': 441 result = True 442 for arg in args: 443 p = pl_true(arg, model) 444 if p == False: return False 445 if p == None: result = None 446 return result 447 p, q = args 448 if op == '>>': 449 return pl_true(~p | q, model) 450 elif op == '<<': 451 return pl_true(p | ~q, model) 452 pt = pl_true(p, model) 453 if pt == None: return None 454 qt = pl_true(q, model) 455 if qt == None: return None 456 if op == '<=>': 457 return pt == qt 458 elif op == '^': 459 return pt != qt 460 else: 461 raise ValueError, "illegal operator in logic expression" + str(exp)
462 463 #______________________________________________________________________________ 464 465 ## Convert to Conjunctive Normal Form (CNF) 466
467 -def to_cnf(s):
468 """Convert a propositional logical sentence s to conjunctive normal form. 469 That is, of the form ((A | ~B | ...) & (B | C | ...) & ...) [p. 215] 470 >>> to_cnf("~(B|C)") 471 (~B & ~C) 472 >>> to_cnf("B <=> (P1|P2)") 473 ((~P1 | B) & (~P2 | B) & (P1 | P2 | ~B)) 474 >>> to_cnf("a | (b & c) | d") 475 ((b | a | d) & (c | a | d)) 476 >>> to_cnf("A & (B | (D & E))") 477 (A & (D | B) & (E | B)) 478 """ 479 if isinstance(s, str): s = expr(s) 480 s = eliminate_implications(s) # Steps 1, 2 from p. 215 481 s = move_not_inwards(s) # Step 3 482 return distribute_and_over_or(s) # Step 4
483
484 -def eliminate_implications(s):
485 """Change >>, <<, and <=> into &, |, and ~. That is, return an Expr 486 that is equivalent to s, but has only &, |, and ~ as logical operators. 487 >>> eliminate_implications(A >> (~B << C)) 488 ((~B | ~C) | ~A) 489 """ 490 if not s.args or is_symbol(s.op): return s ## (Atoms are unchanged.) 491 args = map(eliminate_implications, s.args) 492 a, b = args[0], args[-1] 493 if s.op == '>>': 494 return (b | ~a) 495 elif s.op == '<<': 496 return (a | ~b) 497 elif s.op == '<=>': 498 return (a | ~b) & (b | ~a) 499 else: 500 return Expr(s.op, *args)
501
502 -def move_not_inwards(s):
503 """Rewrite sentence s by moving negation sign inward. 504 >>> move_not_inwards(~(A | B)) 505 (~A & ~B) 506 >>> move_not_inwards(~(A & B)) 507 (~A | ~B) 508 >>> move_not_inwards(~(~(A | ~B) | ~~C)) 509 ((A | ~B) & ~C) 510 """ 511 if s.op == '~': 512 NOT = lambda b: move_not_inwards(~b) 513 a = s.args[0] 514 if a.op == '~': return move_not_inwards(a.args[0]) # ~~A ==> A 515 if a.op =='&': return NaryExpr('|', *map(NOT, a.args)) 516 if a.op =='|': return NaryExpr('&', *map(NOT, a.args)) 517 return s 518 elif is_symbol(s.op) or not s.args: 519 return s 520 else: 521 return Expr(s.op, *map(move_not_inwards, s.args))
522
523 -def distribute_and_over_or(s):
524 """Given a sentence s consisting of conjunctions and disjunctions 525 of literals, return an equivalent sentence in CNF. 526 >>> distribute_and_over_or((A & B) | C) 527 ((A | C) & (B | C)) 528 """ 529 if s.op == '|': 530 s = NaryExpr('|', *s.args) 531 if len(s.args) == 0: 532 return FALSE 533 if len(s.args) == 1: 534 return distribute_and_over_or(s.args[0]) 535 conj = find_if((lambda d: d.op == '&'), s.args) 536 if not conj: 537 return NaryExpr(s.op, *s.args) 538 others = [a for a in s.args if a is not conj] 539 if len(others) == 1: 540 rest = others[0] 541 else: 542 rest = NaryExpr('|', *others) 543 return NaryExpr('&', *map(distribute_and_over_or, 544 [(c|rest) for c in conj.args])) 545 elif s.op == '&': 546 return NaryExpr('&', *map(distribute_and_over_or, s.args)) 547 else: 548 return s
549 550 _NaryExprTable = {'&':TRUE, '|':FALSE, '+':ZERO, '*':ONE} 551
552 -def NaryExpr(op, *args):
553 """Create an Expr, but with an nary, associative op, so we can promote 554 nested instances of the same op up to the top level. 555 >>> NaryExpr('&', (A&B),(B|C),(B&C)) 556 (A & B & (B | C) & B & C) 557 """ 558 arglist = [] 559 for arg in args: 560 if arg.op == op: arglist.extend(arg.args) 561 else: arglist.append(arg) 562 if len(args) == 1: 563 return args[0] 564 elif len(args) == 0: 565 return _NaryExprTable[op] 566 else: 567 return Expr(op, *arglist)
568
569 -def conjuncts(s):
570 """Return a list of the conjuncts in the sentence s. 571 >>> conjuncts(A & B) 572 [A, B] 573 >>> conjuncts(A | B) 574 [(A | B)] 575 """ 576 if isinstance(s, Expr) and s.op == '&': 577 return s.args 578 else: 579 return [s]
580
581 -def disjuncts(s):
582 """Return a list of the disjuncts in the sentence s. 583 >>> disjuncts(A | B) 584 [A, B] 585 >>> disjuncts(A & B) 586 [(A & B)] 587 """ 588 if isinstance(s, Expr) and s.op == '|': 589 return s.args 590 else: 591 return [s]
592 593 #______________________________________________________________________________ 594
595 -def pl_resolution(KB, alpha):
596 "Propositional Logic Resolution: say if alpha follows from KB. [Fig. 7.12]" 597 clauses = KB.clauses + conjuncts(to_cnf(~alpha)) 598 new = set() 599 while True: 600 n = len(clauses) 601 pairs = [(clauses[i], clauses[j]) 602 for i in range(n) for j in range(i+1, n)] 603 for (ci, cj) in pairs: 604 resolvents = pl_resolve(ci, cj) 605 if FALSE in resolvents: return True 606 new = new.union(set(resolvents)) 607 if new.issubset(set(clauses)): return False 608 for c in new: 609 if c not in clauses: clauses.append(c)
610
611 -def pl_resolve(ci, cj):
612 """Return all clauses that can be obtained by resolving clauses ci and cj. 613 >>> for res in pl_resolve(to_cnf(A|B|C), to_cnf(~B|~C|F)): 614 ... ppset(disjuncts(res)) 615 set([A, C, F, ~C]) 616 set([A, B, F, ~B]) 617 """ 618 clauses = [] 619 for di in disjuncts(ci): 620 for dj in disjuncts(cj): 621 if di == ~dj or ~di == dj: 622 dnew = unique(removeall(di, disjuncts(ci)) + 623 removeall(dj, disjuncts(cj))) 624 clauses.append(NaryExpr('|', *dnew)) 625 return clauses
626 627 #______________________________________________________________________________ 628
629 -class PropHornKB(PropKB):
630 "A KB of Propositional Horn clauses." 631
632 - def tell(self, sentence):
633 "Add a Horn Clauses to this KB." 634 op = sentence.op 635 assert op == '>>' or is_prop_symbol(op), "Must be Horn Clause" 636 self.clauses.append(sentence)
637
638 - def ask_generator(self, query):
639 "Yield the empty substitution if KB implies query; else False" 640 if not pl_fc_entails(self.clauses, query): 641 return 642 yield {}
643
644 - def retract(self, sentence):
645 "Remove the sentence's clauses from the KB" 646 for c in conjuncts(to_cnf(sentence)): 647 if c in self.clauses: 648 self.clauses.remove(c)
649
650 - def clauses_with_premise(self, p):
651 """The list of clauses in KB that have p in the premise. 652 This could be cached away for O(1) speed, but we'll recompute it.""" 653 return [c for c in self.clauses 654 if c.op == '>>' and p in conjuncts(c.args[0])]
655
656 -def pl_fc_entails(KB, q):
657 """Use forward chaining to see if a HornKB entails symbol q. [Fig. 7.14] 658 >>> pl_fc_entails(Fig[7,15], expr('Q')) 659 True 660 """ 661 count = dict([(c, len(conjuncts(c.args[0]))) for c in KB.clauses 662 if c.op == '>>']) 663 inferred = DefaultDict(False) 664 agenda = [s for s in KB.clauses if is_prop_symbol(s.op)] 665 if q in agenda: return True 666 while agenda: 667 p = agenda.pop() 668 if not inferred[p]: 669 inferred[p] = True 670 for c in KB.clauses_with_premise(p): 671 count[c] -= 1 672 if count[c] == 0: 673 if c.args[1] == q: return True 674 agenda.append(c.args[1]) 675 return False
676 677 ## Wumpus World example [Fig. 7.13] 678 #Fig[7,13] = expr("(B11 <=> (P12 | P21)) & ~B11") 679 680 ## Propositional Logic Forward Chaining example [Fig. 7.15] 681 #Fig[7,15] = PropHornKB() 682 #for s in "P>>Q (L&M)>>P (B&L)>>M (A&P)>>L (A&B)>>L A B".split(): 683 # Fig[7,15].tell(expr(s)) 684 685 #______________________________________________________________________________ 686 687 # DPLL-Satisfiable [Fig. 7.16] 688
689 -def dpll_satisfiable(s):
690 """Check satisfiability of a propositional sentence. 691 This differs from the book code in two ways: (1) it returns a model 692 rather than True when it succeeds; this is more useful. (2) The 693 function find_pure_symbol is passed a list of unknown clauses, rather 694 than a list of all clauses and the model; this is more efficient. 695 >>> ppsubst(dpll_satisfiable(A&~B)) 696 {A: True, B: False} 697 >>> dpll_satisfiable(P&~P) 698 False 699 """ 700 clauses = conjuncts(to_cnf(s)) 701 symbols = prop_symbols(s) 702 return dpll(clauses, symbols, {})
703
704 -def dpll(clauses, symbols, model):
705 "See if the clauses are true in a partial model." 706 unknown_clauses = [] ## clauses with an unknown truth value 707 for c in clauses: 708 val = pl_true(c, model) 709 if val == False: 710 return False 711 if val != True: 712 unknown_clauses.append(c) 713 if not unknown_clauses: 714 return model 715 P, value = find_pure_symbol(symbols, unknown_clauses) 716 if P: 717 return dpll(clauses, removeall(P, symbols), extend(model, P, value)) 718 P, value = find_unit_clause(clauses, model) 719 if P: 720 return dpll(clauses, removeall(P, symbols), extend(model, P, value)) 721 P = symbols.pop() 722 return (dpll(clauses, symbols, extend(model, P, True)) or 723 dpll(clauses, symbols, extend(model, P, False)))
724
725 -def find_pure_symbol(symbols, unknown_clauses):
726 """Find a symbol and its value if it appears only as a positive literal 727 (or only as a negative) in clauses. 728 >>> find_pure_symbol([A, B, C], [A|~B,~B|~C,C|A]) 729 (A, True) 730 """ 731 for s in symbols: 732 found_pos, found_neg = False, False 733 for c in unknown_clauses: 734 if not found_pos and s in disjuncts(c): found_pos = True 735 if not found_neg and ~s in disjuncts(c): found_neg = True 736 if found_pos != found_neg: return s, found_pos 737 return None, None
738
739 -def find_unit_clause(clauses, model):
740 """A unit clause has only 1 variable that is not bound in the model. 741 >>> find_unit_clause([A|B|C, B|~C, A|~B], {A:True}) 742 (B, False) 743 """ 744 for clause in clauses: 745 num_not_in_model = 0 746 for literal in disjuncts(clause): 747 sym = literal_symbol(literal) 748 if sym not in model: 749 num_not_in_model += 1 750 P, value = sym, (literal.op != '~') 751 if num_not_in_model == 1: 752 return P, value 753 return None, None
754 755
756 -def literal_symbol(literal):
757 """The symbol in this literal (without the negation). 758 >>> literal_symbol(P) 759 P 760 >>> literal_symbol(~P) 761 P 762 """ 763 if literal.op == '~': 764 return literal.args[0] 765 else: 766 return literal
767 768 769 #______________________________________________________________________________ 770 # Walk-SAT [Fig. 7.17] 771
772 -def WalkSAT(clauses, p=0.5, max_flips=10000):
773 ## model is a random assignment of true/false to the symbols in clauses 774 ## See ~/aima1e/print1/manual/knowledge+logic-answers.tex ??? 775 model = dict([(s, random.choice([True, False])) 776 for s in prop_symbols(clauses)]) 777 for i in range(max_flips): 778 satisfied, unsatisfied = [], [] 779 for clause in clauses: 780 if_(pl_true(clause, model), satisfied, unsatisfied).append(clause) 781 if not unsatisfied: ## if model satisfies all the clauses 782 return model 783 clause = random.choice(unsatisfied) 784 if probability(p): 785 sym = random.choice(prop_symbols(clause)) 786 else: 787 ## Flip the symbol in clause that miximizes number of sat. clauses 788 raise NotImplementedError 789 model[sym] = not model[sym] 790 791 792 # PL-Wumpus-Agent [Fig. 7.19] 793 #class PLWumpusAgent(agents.Agent): 794 """An agent for the wumpus world that does logical inference. [Fig. 7.19]""" 795 """def __init__(self): 796 KB = FOLKB() ## shouldn't this be a propositional KB? *** 797 x, y, orientation = 1, 1, (1, 0) 798 visited = set() ## squares already visited 799 action = None 800 plan = [] 801 802 def program(percept): 803 stench, breeze, glitter = percept 804 x, y, orientation = update_position(x, y, orientation, action) 805 KB.tell('%sS_%d,%d' % (if_(stench, '', '~'), x, y)) 806 KB.tell('%sB_%d,%d' % (if_(breeze, '', '~'), x, y)) 807 if glitter: action = 'Grab' 808 elif plan: action = plan.pop() 809 else: 810 for [i, j] in fringe(visited): 811 if KB.ask('~P_%d,%d & ~W_%d,%d' % (i, j, i, j)) != False: 812 raise NotImplementedError 813 KB.ask('~P_%d,%d | ~W_%d,%d' % (i, j, i, j)) != False 814 if action == None: 815 action = random.choice(['Forward', 'Right', 'Left']) 816 return action 817 818 self.program = program"""
819
820 -def update_position(x, y, orientation, action):
821 if action == 'TurnRight': 822 orientation = turn_right(orientation) 823 elif action == 'TurnLeft': 824 orientation = turn_left(orientation) 825 elif action == 'Forward': 826 x, y = x + vector_add((x, y), orientation) 827 return x, y, orientation
828 829 #______________________________________________________________________________ 830
831 -def unify(x, y, s):
832 """Unify expressions x,y with substitution s; return a substitution that 833 would make x,y equal, or None if x,y can not unify. x and y can be 834 variables (e.g. Expr('x')), constants, lists, or Exprs. [Fig. 9.1] 835 >>> ppsubst(unify(x + y, y + C, {})) 836 {x: y, y: C} 837 """ 838 if s == None: 839 return None 840 elif x == y: 841 return s 842 elif is_variable(x): 843 return unify_var(x, y, s) 844 elif is_variable(y): 845 return unify_var(y, x, s) 846 elif isinstance(x, Expr) and isinstance(y, Expr): 847 return unify(x.args, y.args, unify(x.op, y.op, s)) 848 elif isinstance(x, str) or isinstance(y, str) or not x or not y: 849 # orig. return if_(x == y, s, None) but we already know x != y 850 return None 851 elif issequence(x) and issequence(y) and len(x) == len(y): 852 # Assert neither x nor y is [] 853 return unify(x[1:], y[1:], unify(x[0], y[0], s)) 854 else: 855 return None
856
857 -def is_variable(x):
858 "A variable is an Expr with no args and a lowercase symbol as the op." 859 return isinstance(x, Expr) and not x.args and is_var_symbol(x.op)
860
861 -def unify_var(var, x, s):
862 if var in s: 863 return unify(s[var], x, s) 864 elif occur_check(var, x, s): 865 return None 866 else: 867 return extend(s, var, x)
868
869 -def occur_check(var, x, s):
870 """Return true if variable var occurs anywhere in x 871 (or in subst(s, x), if s has a binding for x).""" 872 873 if var == x: 874 return True 875 elif is_variable(x) and s.has_key(x): 876 return occur_check(var, s[x], s) # fixed 877 # What else might x be? an Expr, a list, a string? 878 elif isinstance(x, Expr): 879 # Compare operator and arguments 880 return (occur_check(var, x.op, s) or 881 occur_check(var, x.args, s)) 882 elif isinstance(x, list) and x != []: 883 # Compare first and rest 884 return (occur_check(var, x[0], s) or 885 occur_check(var, x[1:], s)) 886 else: 887 # A variable cannot occur in a string 888 return False
889 890 #elif isinstance(x, Expr): 891 # return var.op == x.op or occur_check(var, x.args) 892 #elif not isinstance(x, str) and issequence(x): 893 # for xi in x: 894 # if occur_check(var, xi): return True 895 #return False 896
897 -def extend(s, var, val):
898 """Copy the substitution s and extend it by setting var to val; 899 return copy. 900 901 >>> ppsubst(extend({x: 1}, y, 2)) 902 {x: 1, y: 2} 903 """ 904 s2 = s.copy() 905 s2[var] = val 906 return s2
907
908 -def subst(s, x):
909 """Substitute the substitution s into the expression x. 910 >>> subst({x: 42, y:0}, F(x) + y) 911 (F(42) + 0) 912 """ 913 if isinstance(x, list): 914 return [subst(s, xi) for xi in x] 915 elif isinstance(x, tuple): 916 return tuple([subst(s, xi) for xi in x]) 917 elif not isinstance(x, Expr): 918 return x 919 elif is_var_symbol(x.op): 920 return s.get(x, x) 921 else: 922 return Expr(x.op, *[subst(s, arg) for arg in x.args])
923
924 -def fol_fc_ask(KB, alpha):
925 """Inefficient forward chaining for first-order logic. [Fig. 9.3] 926 KB is an FOLHornKB and alpha must be an atomic sentence.""" 927 while True: 928 new = {} 929 for r in KB.clauses: 930 r1 = standardize_apart(r) 931 ps, q = conjuncts(r.args[0]), r.args[1] 932 raise NotImplementedError
933
934 -def standardize_apart(sentence, dic={}):
935 """Replace all the variables in sentence with new variables. 936 >>> e = expr('F(a, b, c) & G(c, A, 23)') 937 >>> len(variables(standardize_apart(e))) 938 3 939 >>> variables(e).intersection(variables(standardize_apart(e))) 940 set([]) 941 >>> is_variable(standardize_apart(expr('x'))) 942 True 943 """ 944 if not isinstance(sentence, Expr): 945 return sentence 946 elif is_var_symbol(sentence.op): 947 if sentence in dic: 948 return dic[sentence] 949 else: 950 standardize_apart.counter += 1 951 v = Expr('v_%d' % standardize_apart.counter) 952 dic[sentence] = v 953 return v 954 else: 955 return Expr(sentence.op, 956 *[standardize_apart(a, dic) for a in sentence.args])
957 958 standardize_apart.counter = 0 959 960 #______________________________________________________________________________ 961 962
963 -class FolKB (KB):
964 """A knowledge base consisting of first-order definite clauses 965 >>> kb0 = FolKB([expr('Farmer(Mac)'), expr('Rabbit(Pete)'), 966 ... expr('(Rabbit(r) & Farmer(f)) ==> Hates(f, r)')]) 967 >>> kb0.tell(expr('Rabbit(Flopsie)')) 968 >>> kb0.retract(expr('Rabbit(Pete)')) 969 >>> kb0.ask(expr('Hates(Mac, x)'))[x] 970 Flopsie 971 >>> kb0.ask(expr('Wife(Pete, x)')) 972 False 973 """ 974
975 - def __init__ (self, initial_clauses=[]):
976 self.clauses = [] # inefficient: no indexing 977 for clause in initial_clauses: 978 self.tell(clause)
979
980 - def tell(self, sentence):
981 if is_definite_clause(sentence): 982 self.clauses.append(sentence) 983 else: 984 raise Exception("Not a definite clause: %s" % sentence)
985
986 - def ask_generator(self, query):
987 return fol_bc_ask(self, [query])
988
989 - def retract(self, sentence):
990 self.clauses.remove(sentence)
991
992 -def test_ask(q):
993 e = expr(q) 994 vars = variables(e) 995 ans = fol_bc_ask(test_kb, [e]) 996 res = [] 997 for a in ans: 998 res.append(pretty(dict([(x, v) for (x, v) in a.items() if x in vars]))) 999 res.sort(key=str) 1000 return res
1001 1002 test_kb = FolKB( 1003 map(expr, ['Farmer(Mac)', 1004 'Rabbit(Pete)', 1005 'Mother(MrsMac, Mac)', 1006 'Mother(MrsRabbit, Pete)', 1007 '(Rabbit(r) & Farmer(f)) ==> Hates(f, r)', 1008 '(Mother(m, c)) ==> Loves(m, c)', 1009 '(Mother(m, r) & Rabbit(r)) ==> Rabbit(m)', 1010 '(Farmer(f)) ==> Human(f)', 1011 # Note that this order of conjuncts 1012 # would result in infinite recursion: 1013 #'(Human(h) & Mother(m, h)) ==> Human(m)' 1014 '(Mother(m, h) & Human(h)) ==> Human(m)' 1015 ]) 1016 ) 1017 1018
1019 -def fol_bc_ask(KB, goals, theta={}):
1020 """A simple backward-chaining algorithm for first-order logic. [Fig. 9.6] 1021 KB should be an instance of FolKB, and goals a list of literals. 1022 1023 >>> test_ask('Farmer(x)') 1024 ['{x: Mac}'] 1025 >>> test_ask('Human(x)') 1026 ['{x: Mac}', '{x: MrsMac}'] 1027 >>> test_ask('Hates(x, y)') 1028 ['{x: Mac, y: Pete}'] 1029 >>> test_ask('Loves(x, y)') 1030 ['{x: MrsMac, y: Mac}', '{x: MrsRabbit, y: Pete}'] 1031 >>> test_ask('Rabbit(x)') 1032 ['{x: MrsRabbit}', '{x: Pete}'] 1033 """ 1034 1035 if goals == []: 1036 yield theta 1037 raise StopIteration() 1038 1039 q1 = subst(theta, goals[0]) 1040 1041 for r in KB.clauses: 1042 sar = standardize_apart(r) 1043 1044 # Split into head and body 1045 if is_symbol(sar.op): 1046 head = sar 1047 body = [] 1048 elif sar.op == '>>': # sar = (Body1 & Body2 & ...) >> Head 1049 head = sar.args[1] 1050 body = sar.args[0] # as conjunction 1051 else: 1052 raise Exception("Invalid clause in FolKB: %s" % r) 1053 1054 theta1 = unify(head, q1, {}) 1055 1056 if theta1 is not None: 1057 if body == []: 1058 new_goals = goals[1:] 1059 else: 1060 new_goals = conjuncts(body) + goals[1:] 1061 1062 for ans in fol_bc_ask(KB, new_goals, subst_compose(theta1, theta)): 1063 yield ans 1064 1065 raise StopIteration()
1066
1067 -def subst_compose (s1, s2):
1068 """Return the substitution which is equivalent to applying s2 to 1069 the result of applying s1 to an expression. 1070 1071 >>> s1 = {x: A, y: B} 1072 >>> s2 = {z: x, x: C} 1073 >>> p = F(x) & G(y) & expr('H(z)') 1074 >>> subst(s1, p) 1075 ((F(A) & G(B)) & H(z)) 1076 >>> subst(s2, p) 1077 ((F(C) & G(y)) & H(x)) 1078 1079 >>> subst(s2, subst(s1, p)) 1080 ((F(A) & G(B)) & H(x)) 1081 >>> subst(subst_compose(s1, s2), p) 1082 ((F(A) & G(B)) & H(x)) 1083 1084 >>> subst(s1, subst(s2, p)) 1085 ((F(C) & G(B)) & H(A)) 1086 >>> subst(subst_compose(s2, s1), p) 1087 ((F(C) & G(B)) & H(A)) 1088 >>> ppsubst(subst_compose(s1, s2)) 1089 {x: A, y: B, z: x} 1090 >>> ppsubst(subst_compose(s2, s1)) 1091 {x: C, y: B, z: A} 1092 >>> subst(subst_compose(s1, s2), p) == subst(s2, subst(s1, p)) 1093 True 1094 >>> subst(subst_compose(s2, s1), p) == subst(s1, subst(s2, p)) 1095 True 1096 """ 1097 sc = {} 1098 for x, v in s1.items(): 1099 if s2.has_key(v): 1100 w = s2[v] 1101 sc[x] = w # x -> v -> w 1102 else: 1103 sc[x] = v 1104 for x, v in s2.items(): 1105 if not (s1.has_key(x)): 1106 sc[x] = v 1107 # otherwise s1[x] preemptys s2[x] 1108 return sc
1109 1110 #______________________________________________________________________________ 1111 1112 # Example application (not in the book). 1113 # You can use the Expr class to do symbolic differentiation. This used to be 1114 # a part of AI; now it is considered a separate field, Symbolic Algebra. 1115
1116 -def diff(y, x):
1117 """Return the symbolic derivative, dy/dx, as an Expr. 1118 However, you probably want to simplify the results with simp. 1119 >>> diff(x * x, x) 1120 ((x * 1) + (x * 1)) 1121 >>> simp(diff(x * x, x)) 1122 (2 * x) 1123 """ 1124 if y == x: return ONE 1125 elif not y.args: return ZERO 1126 else: 1127 u, op, v = y.args[0], y.op, y.args[-1] 1128 if op == '+': return diff(u, x) + diff(v, x) 1129 elif op == '-' and len(args) == 1: return -diff(u, x) 1130 elif op == '-': return diff(u, x) - diff(v, x) 1131 elif op == '*': return u * diff(v, x) + v * diff(u, x) 1132 elif op == '/': return (v*diff(u, x) - u*diff(v, x)) / (v * v) 1133 elif op == '**' and isnumber(x.op): 1134 return (v * u ** (v - 1) * diff(u, x)) 1135 elif op == '**': return (v * u ** (v - 1) * diff(u, x) 1136 + u ** v * Expr('log')(u) * diff(v, x)) 1137 elif op == 'log': return diff(u, x) / u 1138 else: raise ValueError("Unknown op: %s in diff(%s, %s)" % (op, y, x))
1139
1140 -def simp(x):
1141 if not x.args: return x 1142 args = map(simp, x.args) 1143 u, op, v = args[0], x.op, args[-1] 1144 if op == '+': 1145 if v == ZERO: return u 1146 if u == ZERO: return v 1147 if u == v: return TWO * u 1148 if u == -v or v == -u: return ZERO 1149 elif op == '-' and len(args) == 1: 1150 if u.op == '-' and len(u.args) == 1: return u.args[0] ## --y ==> y 1151 elif op == '-': 1152 if v == ZERO: return u 1153 if u == ZERO: return -v 1154 if u == v: return ZERO 1155 if u == -v or v == -u: return ZERO 1156 elif op == '*': 1157 if u == ZERO or v == ZERO: return ZERO 1158 if u == ONE: return v 1159 if v == ONE: return u 1160 if u == v: return u ** 2 1161 elif op == '/': 1162 if u == ZERO: return ZERO 1163 if v == ZERO: return Expr('Undefined') 1164 if u == v: return ONE 1165 if u == -v or v == -u: return ZERO 1166 elif op == '**': 1167 if u == ZERO: return ZERO 1168 if v == ZERO: return ONE 1169 if u == ONE: return ONE 1170 if v == ONE: return u 1171 elif op == 'log': 1172 if u == ONE: return ZERO 1173 else: raise ValueError("Unknown op: " + op) 1174 ## If we fall through to here, we can not simplify further 1175 return Expr(op, *args)
1176
1177 -def d(y, x):
1178 "Differentiate and then simplify." 1179 return simp(diff(y, x))
1180 1181 #_______________________________________________________________________________ 1182 1183 # Utilities for doctest cases 1184 # These functions print their arguments in a standard order 1185 # to compensate for the random order in the standard representation 1186
1187 -def pretty(x):
1188 t = type(x) 1189 if t == dict: 1190 return pretty_dict(x) 1191 elif t == set: 1192 return pretty_set(x)
1193
1194 -def pretty_dict(d):
1195 """Print the dictionary d. 1196 1197 Prints a string representation of the dictionary 1198 with keys in sorted order according to their string 1199 representation: {a: A, d: D, ...}. 1200 >>> pretty_dict({'m': 'M', 'a': 'A', 'r': 'R', 'k': 'K'}) 1201 "{'a': 'A', 'k': 'K', 'm': 'M', 'r': 'R'}" 1202 >>> pretty_dict({z: C, y: B, x: A}) 1203 '{x: A, y: B, z: C}' 1204 """ 1205 1206 def format(k, v): 1207 return "%s: %s" % (repr(k), repr(v))
1208 1209 ditems = d.items() 1210 ditems.sort(key=str) 1211 k, v = ditems[0] 1212 dpairs = format(k, v) 1213 for (k, v) in ditems[1:]: 1214 dpairs += (', ' + format(k, v)) 1215 return '{%s}' % dpairs 1216
1217 -def pretty_set(s):
1218 """Print the set s. 1219 1220 >>> pretty_set(set(['A', 'Q', 'F', 'K', 'Y', 'B'])) 1221 "set(['A', 'B', 'F', 'K', 'Q', 'Y'])" 1222 >>> pretty_set(set([z, y, x])) 1223 'set([x, y, z])' 1224 """ 1225 1226 slist = list(s) 1227 slist.sort(key=str) 1228 return 'set(%s)' % slist
1229
1230 -def pp(x):
1231 print pretty(x)
1232
1233 -def ppsubst(s):
1234 """Pretty-print substitution s""" 1235 ppdict(s)
1236
1237 -def ppdict(d):
1238 print pretty_dict(d)
1239
1240 -def ppset(s):
1241 print pretty_set(s)
1242 1243 #________________________________________________________________________ 1244
1245 -class logicTest: """ 1246 ### PropKB 1247 >>> kb = PropKB() 1248 >>> kb.tell(A & B) 1249 >>> kb.tell(B >> C) 1250 >>> kb.ask(C) ## The result {} means true, with no substitutions 1251 {} 1252 >>> kb.ask(P) 1253 False 1254 >>> kb.retract(B) 1255 >>> kb.ask(C) 1256 False 1257 1258 >>> pl_true(P, {}) 1259 >>> pl_true(P | Q, {P: True}) 1260 True 1261 1262 # Notice that the function pl_true cannot reason by cases: 1263 >>> pl_true(P | ~P) 1264 1265 # However, tt_true can: 1266 >>> tt_true(P | ~P) 1267 True 1268 1269 # The following are tautologies from [Fig. 7.11]: 1270 >>> tt_true("(A & B) <=> (B & A)") 1271 True 1272 >>> tt_true("(A | B) <=> (B | A)") 1273 True 1274 >>> tt_true("((A & B) & C) <=> (A & (B & C))") 1275 True 1276 >>> tt_true("((A | B) | C) <=> (A | (B | C))") 1277 True 1278 >>> tt_true("~~A <=> A") 1279 True 1280 >>> tt_true("(A >> B) <=> (~B >> ~A)") 1281 True 1282 >>> tt_true("(A >> B) <=> (~A | B)") 1283 True 1284 >>> tt_true("(A <=> B) <=> ((A >> B) & (B >> A))") 1285 True 1286 >>> tt_true("~(A & B) <=> (~A | ~B)") 1287 True 1288 >>> tt_true("~(A | B) <=> (~A & ~B)") 1289 True 1290 >>> tt_true("(A & (B | C)) <=> ((A & B) | (A & C))") 1291 True 1292 >>> tt_true("(A | (B & C)) <=> ((A | B) & (A | C))") 1293 True 1294 1295 # The following are not tautologies: 1296 >>> tt_true(A & ~A) 1297 False 1298 >>> tt_true(A & B) 1299 False 1300 1301 ### [Fig. 7.13] 1302 >>> alpha = expr("~P12") 1303 >>> to_cnf(Fig[7,13] & ~alpha) 1304 ((~P12 | B11) & (~P21 | B11) & (P12 | P21 | ~B11) & ~B11 & P12) 1305 >>> tt_entails(Fig[7,13], alpha) 1306 True 1307 >>> pl_resolution(PropKB(Fig[7,13]), alpha) 1308 True 1309 1310 ### [Fig. 7.15] 1311 >>> pl_fc_entails(Fig[7,15], expr('SomethingSilly')) 1312 False 1313 1314 ### Unification: 1315 >>> unify(x, x, {}) 1316 {} 1317 >>> unify(x, 3, {}) 1318 {x: 3} 1319 1320 1321 >>> to_cnf((P&Q) | (~P & ~Q)) 1322 ((~P | P) & (~Q | P) & (~P | Q) & (~Q | Q)) 1323 """
1324