Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
aimacode
GitHub Repository: aimacode/aima-python
Path: blob/master/logic4e.py
615 views
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, imported from utils.py
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
13
Logical expressions can be created with Expr or expr, imported from utils, TODO
14
or with expr, which adds the capability to write a string that uses
15
the connectives ==>, <==, <=>, or <=/=>. But be careful: these have the
16
operator precedence of commas; you may need to add parents to make precedence work.
17
See logic.ipynb for examples.
18
19
Then we implement various functions for doing logical inference:
20
21
pl_true Evaluate a propositional logical sentence in a model
22
tt_entails Say if a statement is entailed by a KB
23
pl_resolution Do resolution on propositional sentences
24
dpll_satisfiable See if a propositional sentence is satisfiable
25
WalkSAT Try to find a solution for a set of clauses
26
27
And a few other functions:
28
29
to_cnf Convert to conjunctive normal form
30
unify Do unification of two FOL sentences
31
diff, simp Symbolic differentiation and simplification
32
"""
33
import itertools
34
import random
35
from collections import defaultdict
36
37
from agents import Agent, Glitter, Bump, Stench, Breeze, Scream
38
from search import astar_search, PlanRoute
39
from utils4e import remove_all, unique, first, probability, isnumber, issequence, Expr, expr, subexpressions
40
41
42
# ______________________________________________________________________________
43
# Chapter 7 Logical Agents
44
# 7.1 Knowledge Based Agents
45
46
47
class KB:
48
"""
49
A knowledge base to which you can tell and ask sentences.
50
To create a KB, subclass this class and implement tell, ask_generator, and retract.
51
Ask_generator:
52
For a Propositional Logic KB, ask(P & Q) returns True or False, but for an
53
FOL KB, something like ask(Brother(x, y)) might return many substitutions
54
such as {x: Cain, y: Abel}, {x: Abel, y: Cain}, {x: George, y: Jeb}, etc.
55
So ask_generator generates these one at a time, and ask either returns the
56
first one or returns False.
57
"""
58
59
def __init__(self, sentence=None):
60
raise NotImplementedError
61
62
def tell(self, sentence):
63
"""Add the sentence to the KB."""
64
raise NotImplementedError
65
66
def ask(self, query):
67
"""Return a substitution that makes the query true, or, failing that, return False."""
68
return first(self.ask_generator(query), default=False)
69
70
def ask_generator(self, query):
71
"""Yield all the substitutions that make query true."""
72
raise NotImplementedError
73
74
def retract(self, sentence):
75
"""Remove sentence from the KB."""
76
raise NotImplementedError
77
78
79
class PropKB(KB):
80
"""A KB for propositional logic. Inefficient, with no indexing."""
81
82
def __init__(self, sentence=None):
83
self.clauses = []
84
if sentence:
85
self.tell(sentence)
86
87
def tell(self, sentence):
88
"""Add the sentence's clauses to the KB."""
89
self.clauses.extend(conjuncts(to_cnf(sentence)))
90
91
def ask_generator(self, query):
92
"""Yield the empty substitution {} if KB entails query; else no results."""
93
if tt_entails(Expr('&', *self.clauses), query):
94
yield {}
95
96
def ask_if_true(self, query):
97
"""Return True if the KB entails query, else return False."""
98
for _ in self.ask_generator(query):
99
return True
100
return False
101
102
def retract(self, sentence):
103
"""Remove the sentence's clauses from the KB."""
104
for c in conjuncts(to_cnf(sentence)):
105
if c in self.clauses:
106
self.clauses.remove(c)
107
108
109
def KB_AgentProgram(KB):
110
"""A generic logical knowledge-based agent program. [Figure 7.1]"""
111
steps = itertools.count()
112
113
def program(percept):
114
t = next(steps)
115
KB.tell(make_percept_sentence(percept, t))
116
action = KB.ask(make_action_query(t))
117
KB.tell(make_action_sentence(action, t))
118
return action
119
120
def make_percept_sentence(percept, t):
121
return Expr("Percept")(percept, t)
122
123
def make_action_query(t):
124
return expr("ShouldDo(action, {})".format(t))
125
126
def make_action_sentence(action, t):
127
return Expr("Did")(action[expr('action')], t)
128
129
return program
130
131
132
# _____________________________________________________________________________
133
# 7.2 The Wumpus World
134
135
136
# Expr functions for WumpusKB and HybridWumpusAgent
137
138
139
def facing_east(time):
140
return Expr('FacingEast', time)
141
142
143
def facing_west(time):
144
return Expr('FacingWest', time)
145
146
147
def facing_north(time):
148
return Expr('FacingNorth', time)
149
150
151
def facing_south(time):
152
return Expr('FacingSouth', time)
153
154
155
def wumpus(x, y):
156
return Expr('W', x, y)
157
158
159
def pit(x, y):
160
return Expr('P', x, y)
161
162
163
def breeze(x, y):
164
return Expr('B', x, y)
165
166
167
def stench(x, y):
168
return Expr('S', x, y)
169
170
171
def wumpus_alive(time):
172
return Expr('WumpusAlive', time)
173
174
175
def have_arrow(time):
176
return Expr('HaveArrow', time)
177
178
179
def percept_stench(time):
180
return Expr('Stench', time)
181
182
183
def percept_breeze(time):
184
return Expr('Breeze', time)
185
186
187
def percept_glitter(time):
188
return Expr('Glitter', time)
189
190
191
def percept_bump(time):
192
return Expr('Bump', time)
193
194
195
def percept_scream(time):
196
return Expr('Scream', time)
197
198
199
def move_forward(time):
200
return Expr('Forward', time)
201
202
203
def shoot(time):
204
return Expr('Shoot', time)
205
206
207
def turn_left(time):
208
return Expr('TurnLeft', time)
209
210
211
def turn_right(time):
212
return Expr('TurnRight', time)
213
214
215
def ok_to_move(x, y, time):
216
return Expr('OK', x, y, time)
217
218
219
def location(x, y, time=None):
220
if time is None:
221
return Expr('L', x, y)
222
else:
223
return Expr('L', x, y, time)
224
225
226
# Symbols
227
228
229
def implies(lhs, rhs):
230
return Expr('==>', lhs, rhs)
231
232
233
def equiv(lhs, rhs):
234
return Expr('<=>', lhs, rhs)
235
236
237
# Helper Function
238
239
240
def new_disjunction(sentences):
241
t = sentences[0]
242
for i in range(1, len(sentences)):
243
t |= sentences[i]
244
return t
245
246
247
# ______________________________________________________________________________
248
# 7.4 Propositional Logic
249
250
251
def is_symbol(s):
252
"""A string s is a symbol if it starts with an alphabetic char.
253
>>> is_symbol('R2D2')
254
True
255
"""
256
return isinstance(s, str) and s[:1].isalpha()
257
258
259
def is_var_symbol(s):
260
"""A logic variable symbol is an initial-lowercase string.
261
>>> is_var_symbol('EXE')
262
False
263
"""
264
return is_symbol(s) and s[0].islower()
265
266
267
def is_prop_symbol(s):
268
"""A proposition logic symbol is an initial-uppercase string.
269
>>> is_prop_symbol('exe')
270
False
271
"""
272
return is_symbol(s) and s[0].isupper()
273
274
275
def variables(s):
276
"""Return a set of the variables in expression s.
277
>>> variables(expr('F(x, x) & G(x, y) & H(y, z) & R(A, z, 2)')) == {x, y, z}
278
True
279
"""
280
return {x for x in subexpressions(s) if is_variable(x)}
281
282
283
def is_definite_clause(s):
284
"""
285
Returns True for exprs s of the form A & B & ... & C ==> D,
286
where all literals are positive. In clause form, this is
287
~A | ~B | ... | ~C | D, where exactly one clause is positive.
288
>>> is_definite_clause(expr('Farmer(Mac)'))
289
True
290
"""
291
if is_symbol(s.op):
292
return True
293
elif s.op == '==>':
294
antecedent, consequent = s.args
295
return (is_symbol(consequent.op) and
296
all(is_symbol(arg.op) for arg in conjuncts(antecedent)))
297
else:
298
return False
299
300
301
def parse_definite_clause(s):
302
"""Return the antecedents and the consequent of a definite clause."""
303
assert is_definite_clause(s)
304
if is_symbol(s.op):
305
return [], s
306
else:
307
antecedent, consequent = s.args
308
return conjuncts(antecedent), consequent
309
310
311
# Useful constant Exprs used in examples and code:
312
A, B, C, D, E, F, G, P, Q, x, y, z = map(Expr, 'ABCDEFGPQxyz')
313
314
315
# ______________________________________________________________________________
316
# 7.4.4 A simple inference procedure
317
318
319
def tt_entails(kb, alpha):
320
"""
321
Does kb entail the sentence alpha? Use truth tables. For propositional
322
kb's and sentences. [Figure 7.10]. Note that the 'kb' should be an
323
Expr which is a conjunction of clauses.
324
>>> tt_entails(expr('P & Q'), expr('Q'))
325
True
326
"""
327
assert not variables(alpha)
328
symbols = list(prop_symbols(kb & alpha))
329
return tt_check_all(kb, alpha, symbols, {})
330
331
332
def tt_check_all(kb, alpha, symbols, model):
333
"""Auxiliary routine to implement tt_entails."""
334
if not symbols:
335
if pl_true(kb, model):
336
result = pl_true(alpha, model)
337
assert result in (True, False)
338
return result
339
else:
340
return True
341
else:
342
P, rest = symbols[0], symbols[1:]
343
return (tt_check_all(kb, alpha, rest, extend(model, P, True)) and
344
tt_check_all(kb, alpha, rest, extend(model, P, False)))
345
346
347
def prop_symbols(x):
348
"""Return the set of all propositional symbols in x."""
349
if not isinstance(x, Expr):
350
return set()
351
elif is_prop_symbol(x.op):
352
return {x}
353
else:
354
return {symbol for arg in x.args for symbol in prop_symbols(arg)}
355
356
357
def constant_symbols(x):
358
"""Return the set of all constant symbols in x."""
359
if not isinstance(x, Expr):
360
return set()
361
elif is_prop_symbol(x.op) and not x.args:
362
return {x}
363
else:
364
return {symbol for arg in x.args for symbol in constant_symbols(arg)}
365
366
367
def predicate_symbols(x):
368
"""
369
Return a set of (symbol_name, arity) in x.
370
All symbols (even functional) with arity > 0 are considered.
371
"""
372
if not isinstance(x, Expr) or not x.args:
373
return set()
374
pred_set = {(x.op, len(x.args))} if is_prop_symbol(x.op) else set()
375
pred_set.update({symbol for arg in x.args for symbol in predicate_symbols(arg)})
376
return pred_set
377
378
379
def tt_true(s):
380
"""Is a propositional sentence a tautology?
381
>>> tt_true('P | ~P')
382
True
383
"""
384
s = expr(s)
385
return tt_entails(True, s)
386
387
388
def pl_true(exp, model={}):
389
"""
390
Return True if the propositional logic expression is true in the model,
391
and False if it is false. If the model does not specify the value for
392
every proposition, this may return None to indicate 'not obvious';
393
this may happen even when the expression is tautological.
394
>>> pl_true(P, {}) is None
395
True
396
"""
397
if exp in (True, False):
398
return exp
399
op, args = exp.op, exp.args
400
if is_prop_symbol(op):
401
return model.get(exp)
402
elif op == '~':
403
p = pl_true(args[0], model)
404
if p is None:
405
return None
406
else:
407
return not p
408
elif op == '|':
409
result = False
410
for arg in args:
411
p = pl_true(arg, model)
412
if p is True:
413
return True
414
if p is None:
415
result = None
416
return result
417
elif op == '&':
418
result = True
419
for arg in args:
420
p = pl_true(arg, model)
421
if p is False:
422
return False
423
if p is None:
424
result = None
425
return result
426
p, q = args
427
if op == '==>':
428
return pl_true(~p | q, model)
429
elif op == '<==':
430
return pl_true(p | ~q, model)
431
pt = pl_true(p, model)
432
if pt is None:
433
return None
434
qt = pl_true(q, model)
435
if qt is None:
436
return None
437
if op == '<=>':
438
return pt == qt
439
elif op == '^': # xor or 'not equivalent'
440
return pt != qt
441
else:
442
raise ValueError("illegal operator in logic expression" + str(exp))
443
444
445
# ______________________________________________________________________________
446
# 7.5 Propositional Theorem Proving
447
448
449
def to_cnf(s):
450
"""Convert a propositional logical sentence to conjunctive normal form.
451
That is, to the form ((A | ~B | ...) & (B | C | ...) & ...) [p. 253]
452
>>> to_cnf('~(B | C)')
453
(~B & ~C)
454
"""
455
s = expr(s)
456
if isinstance(s, str):
457
s = expr(s)
458
s = eliminate_implications(s) # Steps 1, 2 from p. 253
459
s = move_not_inwards(s) # Step 3
460
return distribute_and_over_or(s) # Step 4
461
462
463
def eliminate_implications(s):
464
"""Change implications into equivalent form with only &, |, and ~ as logical operators."""
465
s = expr(s)
466
if not s.args or is_symbol(s.op):
467
return s # Atoms are unchanged.
468
args = list(map(eliminate_implications, s.args))
469
a, b = args[0], args[-1]
470
if s.op == '==>':
471
return b | ~a
472
elif s.op == '<==':
473
return a | ~b
474
elif s.op == '<=>':
475
return (a | ~b) & (b | ~a)
476
elif s.op == '^':
477
assert len(args) == 2 # TODO: relax this restriction
478
return (a & ~b) | (~a & b)
479
else:
480
assert s.op in ('&', '|', '~')
481
return Expr(s.op, *args)
482
483
484
def move_not_inwards(s):
485
"""Rewrite sentence s by moving negation sign inward.
486
>>> move_not_inwards(~(A | B))
487
(~A & ~B)
488
"""
489
s = expr(s)
490
if s.op == '~':
491
def NOT(b):
492
return move_not_inwards(~b)
493
494
a = s.args[0]
495
if a.op == '~':
496
return move_not_inwards(a.args[0]) # ~~A ==> A
497
if a.op == '&':
498
return associate('|', list(map(NOT, a.args)))
499
if a.op == '|':
500
return associate('&', list(map(NOT, a.args)))
501
return s
502
elif is_symbol(s.op) or not s.args:
503
return s
504
else:
505
return Expr(s.op, *list(map(move_not_inwards, s.args)))
506
507
508
def distribute_and_over_or(s):
509
"""Given a sentence s consisting of conjunctions and disjunctions
510
of literals, return an equivalent sentence in CNF.
511
>>> distribute_and_over_or((A & B) | C)
512
((A | C) & (B | C))
513
"""
514
s = expr(s)
515
if s.op == '|':
516
s = associate('|', s.args)
517
if s.op != '|':
518
return distribute_and_over_or(s)
519
if len(s.args) == 0:
520
return False
521
if len(s.args) == 1:
522
return distribute_and_over_or(s.args[0])
523
conj = first(arg for arg in s.args if arg.op == '&')
524
if not conj:
525
return s
526
others = [a for a in s.args if a is not conj]
527
rest = associate('|', others)
528
return associate('&', [distribute_and_over_or(c | rest)
529
for c in conj.args])
530
elif s.op == '&':
531
return associate('&', list(map(distribute_and_over_or, s.args)))
532
else:
533
return s
534
535
536
def associate(op, args):
537
"""Given an associative op, return an expression with the same
538
meaning as Expr(op, *args), but flattened -- that is, with nested
539
instances of the same op promoted to the top level.
540
>>> associate('&', [(A&B),(B|C),(B&C)])
541
(A & B & (B | C) & B & C)
542
>>> associate('|', [A|(B|(C|(A&B)))])
543
(A | B | C | (A & B))
544
"""
545
args = dissociate(op, args)
546
if len(args) == 0:
547
return _op_identity[op]
548
elif len(args) == 1:
549
return args[0]
550
else:
551
return Expr(op, *args)
552
553
554
_op_identity = {'&': True, '|': False, '+': 0, '*': 1}
555
556
557
def dissociate(op, args):
558
"""Given an associative op, return a flattened list result such
559
that Expr(op, *result) means the same as Expr(op, *args).
560
>>> dissociate('&', [A & B])
561
[A, B]
562
"""
563
result = []
564
565
def collect(subargs):
566
for arg in subargs:
567
if arg.op == op:
568
collect(arg.args)
569
else:
570
result.append(arg)
571
572
collect(args)
573
return result
574
575
576
def conjuncts(s):
577
"""Return a list of the conjuncts in the sentence s.
578
>>> conjuncts(A & B)
579
[A, B]
580
>>> conjuncts(A | B)
581
[(A | B)]
582
"""
583
return dissociate('&', [s])
584
585
586
def disjuncts(s):
587
"""Return a list of the disjuncts in the sentence s.
588
>>> disjuncts(A | B)
589
[A, B]
590
>>> disjuncts(A & B)
591
[(A & B)]
592
"""
593
return dissociate('|', [s])
594
595
596
# ______________________________________________________________________________
597
598
599
def pl_resolution(KB, alpha):
600
"""
601
Propositional-logic resolution: say if alpha follows from KB. [Figure 7.12]
602
>>> pl_resolution(horn_clauses_KB, A)
603
True
604
"""
605
clauses = KB.clauses + conjuncts(to_cnf(~alpha))
606
new = set()
607
while True:
608
n = len(clauses)
609
pairs = [(clauses[i], clauses[j])
610
for i in range(n) for j in range(i + 1, n)]
611
for (ci, cj) in pairs:
612
resolvents = pl_resolve(ci, cj)
613
if False in resolvents:
614
return True
615
new = new.union(set(resolvents))
616
if new.issubset(set(clauses)):
617
return False
618
for c in new:
619
if c not in clauses:
620
clauses.append(c)
621
622
623
def pl_resolve(ci, cj):
624
"""Return all clauses that can be obtained by resolving clauses ci and cj."""
625
clauses = []
626
for di in disjuncts(ci):
627
for dj in disjuncts(cj):
628
if di == ~dj or ~di == dj:
629
dnew = unique(remove_all(di, disjuncts(ci)) +
630
remove_all(dj, disjuncts(cj)))
631
clauses.append(associate('|', dnew))
632
return clauses
633
634
635
# ______________________________________________________________________________
636
# 7.5.4 Forward and backward chaining
637
638
639
class PropDefiniteKB(PropKB):
640
"""A KB of propositional definite clauses."""
641
642
def tell(self, sentence):
643
"""Add a definite clause to this KB."""
644
assert is_definite_clause(sentence), "Must be definite clause"
645
self.clauses.append(sentence)
646
647
def ask_generator(self, query):
648
"""Yield the empty substitution if KB implies query; else nothing."""
649
if pl_fc_entails(self.clauses, query):
650
yield {}
651
652
def retract(self, sentence):
653
self.clauses.remove(sentence)
654
655
def clauses_with_premise(self, p):
656
"""Return a list of the clauses in KB that have p in their premise.
657
This could be cached away for O(1) speed, but we'll recompute it."""
658
return [c for c in self.clauses
659
if c.op == '==>' and p in conjuncts(c.args[0])]
660
661
662
def pl_fc_entails(KB, q):
663
"""Use forward chaining to see if a PropDefiniteKB entails symbol q.
664
[Figure 7.15]
665
>>> pl_fc_entails(horn_clauses_KB, expr('Q'))
666
True
667
"""
668
count = {c: len(conjuncts(c.args[0]))
669
for c in KB.clauses
670
if c.op == '==>'}
671
inferred = defaultdict(bool)
672
agenda = [s for s in KB.clauses if is_prop_symbol(s.op)]
673
while agenda:
674
p = agenda.pop()
675
if p == q:
676
return True
677
if not inferred[p]:
678
inferred[p] = True
679
for c in KB.clauses_with_premise(p):
680
count[c] -= 1
681
if count[c] == 0:
682
agenda.append(c.args[1])
683
return False
684
685
686
""" [Figure 7.13]
687
Simple inference in a wumpus world example
688
"""
689
wumpus_world_inference = expr("(B11 <=> (P12 | P21)) & ~B11")
690
691
""" [Figure 7.16]
692
Propositional Logic Forward Chaining example
693
"""
694
horn_clauses_KB = PropDefiniteKB()
695
for s in "P==>Q; (L&M)==>P; (B&L)==>M; (A&P)==>L; (A&B)==>L; A;B".split(';'):
696
horn_clauses_KB.tell(expr(s))
697
698
"""
699
Definite clauses KB example
700
"""
701
definite_clauses_KB = PropDefiniteKB()
702
for clause in ['(B & F)==>E', '(A & E & F)==>G', '(B & C)==>F', '(A & B)==>D', '(E & F)==>H', '(H & I)==>J', 'A', 'B',
703
'C']:
704
definite_clauses_KB.tell(expr(clause))
705
706
707
# ______________________________________________________________________________
708
# 7.6 Effective Propositional Model Checking
709
# DPLL-Satisfiable [Figure 7.17]
710
711
712
def dpll_satisfiable(s):
713
"""Check satisfiability of a propositional sentence.
714
This differs from the book code in two ways: (1) it returns a model
715
rather than True when it succeeds; this is more useful. (2) The
716
function find_pure_symbol is passed a list of unknown clauses, rather
717
than a list of all clauses and the model; this is more efficient.
718
>>> dpll_satisfiable(A |'<=>'| B) == {A: True, B: True}
719
True
720
"""
721
clauses = conjuncts(to_cnf(s))
722
symbols = list(prop_symbols(s))
723
return dpll(clauses, symbols, {})
724
725
726
def dpll(clauses, symbols, model):
727
"""See if the clauses are true in a partial model."""
728
unknown_clauses = [] # clauses with an unknown truth value
729
for c in clauses:
730
val = pl_true(c, model)
731
if val is False:
732
return False
733
if val is not True:
734
unknown_clauses.append(c)
735
if not unknown_clauses:
736
return model
737
P, value = find_pure_symbol(symbols, unknown_clauses)
738
if P:
739
return dpll(clauses, remove_all(P, symbols), extend(model, P, value))
740
P, value = find_unit_clause(clauses, model)
741
if P:
742
return dpll(clauses, remove_all(P, symbols), extend(model, P, value))
743
if not symbols:
744
raise TypeError("Argument should be of the type Expr.")
745
P, symbols = symbols[0], symbols[1:]
746
return (dpll(clauses, symbols, extend(model, P, True)) or
747
dpll(clauses, symbols, extend(model, P, False)))
748
749
750
def find_pure_symbol(symbols, clauses):
751
"""
752
Find a symbol and its value if it appears only as a positive literal
753
(or only as a negative) in clauses.
754
>>> find_pure_symbol([A, B, C], [A|~B,~B|~C,C|A])
755
(A, True)
756
"""
757
for s in symbols:
758
found_pos, found_neg = False, False
759
for c in clauses:
760
if not found_pos and s in disjuncts(c):
761
found_pos = True
762
if not found_neg and ~s in disjuncts(c):
763
found_neg = True
764
if found_pos != found_neg:
765
return s, found_pos
766
return None, None
767
768
769
def find_unit_clause(clauses, model):
770
"""
771
Find a forced assignment if possible from a clause with only 1
772
variable not bound in the model.
773
>>> find_unit_clause([A|B|C, B|~C, ~A|~B], {A:True})
774
(B, False)
775
"""
776
for clause in clauses:
777
P, value = unit_clause_assign(clause, model)
778
if P:
779
return P, value
780
return None, None
781
782
783
def unit_clause_assign(clause, model):
784
"""Return a single variable/value pair that makes clause true in
785
the model, if possible.
786
>>> unit_clause_assign(A|B|C, {A:True})
787
(None, None)
788
>>> unit_clause_assign(B|~C, {A:True})
789
(None, None)
790
>>> unit_clause_assign(~A|~B, {A:True})
791
(B, False)
792
"""
793
P, value = None, None
794
for literal in disjuncts(clause):
795
sym, positive = inspect_literal(literal)
796
if sym in model:
797
if model[sym] == positive:
798
return None, None # clause already True
799
elif P:
800
return None, None # more than 1 unbound variable
801
else:
802
P, value = sym, positive
803
return P, value
804
805
806
def inspect_literal(literal):
807
"""The symbol in this literal, and the value it should take to
808
make the literal true.
809
>>> inspect_literal(P)
810
(P, True)
811
>>> inspect_literal(~P)
812
(P, False)
813
"""
814
if literal.op == '~':
815
return literal.args[0], False
816
else:
817
return literal, True
818
819
820
# ______________________________________________________________________________
821
# 7.6.2 Local search algorithms
822
# Walk-SAT [Figure 7.18]
823
824
825
def WalkSAT(clauses, p=0.5, max_flips=10000):
826
"""
827
Checks for satisfiability of all clauses by randomly flipping values of variables
828
>>> WalkSAT([A & ~A], 0.5, 100) is None
829
True
830
"""
831
# Set of all symbols in all clauses
832
symbols = {sym for clause in clauses for sym in prop_symbols(clause)}
833
# model is a random assignment of true/false to the symbols in clauses
834
model = {s: random.choice([True, False]) for s in symbols}
835
for i in range(max_flips):
836
satisfied, unsatisfied = [], []
837
for clause in clauses:
838
(satisfied if pl_true(clause, model) else unsatisfied).append(clause)
839
if not unsatisfied: # if model satisfies all the clauses
840
return model
841
clause = random.choice(unsatisfied)
842
if probability(p):
843
sym = random.choice(list(prop_symbols(clause)))
844
else:
845
# Flip the symbol in clause that maximizes number of sat. clauses
846
def sat_count(sym):
847
# Return the the number of clauses satisfied after flipping the symbol.
848
model[sym] = not model[sym]
849
count = len([clause for clause in clauses if pl_true(clause, model)])
850
model[sym] = not model[sym]
851
return count
852
853
sym = max(prop_symbols(clause), key=sat_count)
854
model[sym] = not model[sym]
855
# If no solution is found within the flip limit, we return failure
856
return None
857
858
859
# ______________________________________________________________________________
860
# 7.7 Agents Based on Propositional Logic
861
# 7.7.1 The current state of the world
862
863
864
class WumpusKB(PropKB):
865
"""
866
Create a Knowledge Base that contains the atemporal "Wumpus physics" and temporal rules with time zero.
867
"""
868
869
def __init__(self, dimrow):
870
super().__init__()
871
self.dimrow = dimrow
872
self.tell(~wumpus(1, 1))
873
self.tell(~pit(1, 1))
874
875
for y in range(1, dimrow + 1):
876
for x in range(1, dimrow + 1):
877
878
pits_in = list()
879
wumpus_in = list()
880
881
if x > 1: # West room exists
882
pits_in.append(pit(x - 1, y))
883
wumpus_in.append(wumpus(x - 1, y))
884
885
if y < dimrow: # North room exists
886
pits_in.append(pit(x, y + 1))
887
wumpus_in.append(wumpus(x, y + 1))
888
889
if x < dimrow: # East room exists
890
pits_in.append(pit(x + 1, y))
891
wumpus_in.append(wumpus(x + 1, y))
892
893
if y > 1: # South room exists
894
pits_in.append(pit(x, y - 1))
895
wumpus_in.append(wumpus(x, y - 1))
896
897
self.tell(equiv(breeze(x, y), new_disjunction(pits_in)))
898
self.tell(equiv(stench(x, y), new_disjunction(wumpus_in)))
899
900
# Rule that describes existence of at least one Wumpus
901
wumpus_at_least = list()
902
for x in range(1, dimrow + 1):
903
for y in range(1, dimrow + 1):
904
wumpus_at_least.append(wumpus(x, y))
905
906
self.tell(new_disjunction(wumpus_at_least))
907
908
# Rule that describes existence of at most one Wumpus
909
for i in range(1, dimrow + 1):
910
for j in range(1, dimrow + 1):
911
for u in range(1, dimrow + 1):
912
for v in range(1, dimrow + 1):
913
if i != u or j != v:
914
self.tell(~wumpus(i, j) | ~wumpus(u, v))
915
916
# Temporal rules at time zero
917
self.tell(location(1, 1, 0))
918
for i in range(1, dimrow + 1):
919
for j in range(1, dimrow + 1):
920
self.tell(implies(location(i, j, 0), equiv(percept_breeze(0), breeze(i, j))))
921
self.tell(implies(location(i, j, 0), equiv(percept_stench(0), stench(i, j))))
922
if i != 1 or j != 1:
923
self.tell(~location(i, j, 0))
924
925
self.tell(wumpus_alive(0))
926
self.tell(have_arrow(0))
927
self.tell(facing_east(0))
928
self.tell(~facing_north(0))
929
self.tell(~facing_south(0))
930
self.tell(~facing_west(0))
931
932
def make_action_sentence(self, action, time):
933
actions = [move_forward(time), shoot(time), turn_left(time), turn_right(time)]
934
935
for a in actions:
936
if action is a:
937
self.tell(action)
938
else:
939
self.tell(~a)
940
941
def make_percept_sentence(self, percept, time):
942
# Glitter, Bump, Stench, Breeze, Scream
943
flags = [0, 0, 0, 0, 0]
944
945
# Things perceived
946
if isinstance(percept, Glitter):
947
flags[0] = 1
948
self.tell(percept_glitter(time))
949
elif isinstance(percept, Bump):
950
flags[1] = 1
951
self.tell(percept_bump(time))
952
elif isinstance(percept, Stench):
953
flags[2] = 1
954
self.tell(percept_stench(time))
955
elif isinstance(percept, Breeze):
956
flags[3] = 1
957
self.tell(percept_breeze(time))
958
elif isinstance(percept, Scream):
959
flags[4] = 1
960
self.tell(percept_scream(time))
961
962
# Things not perceived
963
for i in range(len(flags)):
964
if flags[i] == 0:
965
if i == 0:
966
self.tell(~percept_glitter(time))
967
elif i == 1:
968
self.tell(~percept_bump(time))
969
elif i == 2:
970
self.tell(~percept_stench(time))
971
elif i == 3:
972
self.tell(~percept_breeze(time))
973
elif i == 4:
974
self.tell(~percept_scream(time))
975
976
def add_temporal_sentences(self, time):
977
if time == 0:
978
return
979
t = time - 1
980
981
# current location rules
982
for i in range(1, self.dimrow + 1):
983
for j in range(1, self.dimrow + 1):
984
self.tell(implies(location(i, j, time), equiv(percept_breeze(time), breeze(i, j))))
985
self.tell(implies(location(i, j, time), equiv(percept_stench(time), stench(i, j))))
986
987
s = list()
988
989
s.append(
990
equiv(
991
location(i, j, time), location(i, j, time) & ~move_forward(time) | percept_bump(time)))
992
993
if i != 1:
994
s.append(location(i - 1, j, t) & facing_east(t) & move_forward(t))
995
996
if i != self.dimrow:
997
s.append(location(i + 1, j, t) & facing_west(t) & move_forward(t))
998
999
if j != 1:
1000
s.append(location(i, j - 1, t) & facing_north(t) & move_forward(t))
1001
1002
if j != self.dimrow:
1003
s.append(location(i, j + 1, t) & facing_south(t) & move_forward(t))
1004
1005
# add sentence about location i,j
1006
self.tell(new_disjunction(s))
1007
1008
# add sentence about safety of location i,j
1009
self.tell(
1010
equiv(ok_to_move(i, j, time), ~pit(i, j) & ~wumpus(i, j) & wumpus_alive(time))
1011
)
1012
1013
# Rules about current orientation
1014
1015
a = facing_north(t) & turn_right(t)
1016
b = facing_south(t) & turn_left(t)
1017
c = facing_east(t) & ~turn_left(t) & ~turn_right(t)
1018
s = equiv(facing_east(time), a | b | c)
1019
self.tell(s)
1020
1021
a = facing_north(t) & turn_left(t)
1022
b = facing_south(t) & turn_right(t)
1023
c = facing_west(t) & ~turn_left(t) & ~turn_right(t)
1024
s = equiv(facing_west(time), a | b | c)
1025
self.tell(s)
1026
1027
a = facing_east(t) & turn_left(t)
1028
b = facing_west(t) & turn_right(t)
1029
c = facing_north(t) & ~turn_left(t) & ~turn_right(t)
1030
s = equiv(facing_north(time), a | b | c)
1031
self.tell(s)
1032
1033
a = facing_west(t) & turn_left(t)
1034
b = facing_east(t) & turn_right(t)
1035
c = facing_south(t) & ~turn_left(t) & ~turn_right(t)
1036
s = equiv(facing_south(time), a | b | c)
1037
self.tell(s)
1038
1039
# Rules about last action
1040
self.tell(equiv(move_forward(t), ~turn_right(t) & ~turn_left(t)))
1041
1042
# Rule about the arrow
1043
self.tell(equiv(have_arrow(time), have_arrow(t) & ~shoot(t)))
1044
1045
# Rule about Wumpus (dead or alive)
1046
self.tell(equiv(wumpus_alive(time), wumpus_alive(t) & ~percept_scream(time)))
1047
1048
def ask_if_true(self, query):
1049
return pl_resolution(self, query)
1050
1051
1052
# ______________________________________________________________________________
1053
1054
1055
class WumpusPosition:
1056
def __init__(self, x, y, orientation):
1057
self.X = x
1058
self.Y = y
1059
self.orientation = orientation
1060
1061
def get_location(self):
1062
return self.X, self.Y
1063
1064
def set_location(self, x, y):
1065
self.X = x
1066
self.Y = y
1067
1068
def get_orientation(self):
1069
return self.orientation
1070
1071
def set_orientation(self, orientation):
1072
self.orientation = orientation
1073
1074
def __eq__(self, other):
1075
if (other.get_location() == self.get_location() and
1076
other.get_orientation() == self.get_orientation()):
1077
return True
1078
else:
1079
return False
1080
1081
1082
# ______________________________________________________________________________
1083
# 7.7.2 A hybrid agent
1084
1085
1086
class HybridWumpusAgent(Agent):
1087
"""An agent for the wumpus world that does logical inference. [Figure 7.20]"""
1088
1089
def __init__(self, dimentions):
1090
self.dimrow = dimentions
1091
self.kb = WumpusKB(self.dimrow)
1092
self.t = 0
1093
self.plan = list()
1094
self.current_position = WumpusPosition(1, 1, 'UP')
1095
super().__init__(self.execute)
1096
1097
def execute(self, percept):
1098
self.kb.make_percept_sentence(percept, self.t)
1099
self.kb.add_temporal_sentences(self.t)
1100
1101
temp = list()
1102
1103
for i in range(1, self.dimrow + 1):
1104
for j in range(1, self.dimrow + 1):
1105
if self.kb.ask_if_true(location(i, j, self.t)):
1106
temp.append(i)
1107
temp.append(j)
1108
1109
if self.kb.ask_if_true(facing_north(self.t)):
1110
self.current_position = WumpusPosition(temp[0], temp[1], 'UP')
1111
elif self.kb.ask_if_true(facing_south(self.t)):
1112
self.current_position = WumpusPosition(temp[0], temp[1], 'DOWN')
1113
elif self.kb.ask_if_true(facing_west(self.t)):
1114
self.current_position = WumpusPosition(temp[0], temp[1], 'LEFT')
1115
elif self.kb.ask_if_true(facing_east(self.t)):
1116
self.current_position = WumpusPosition(temp[0], temp[1], 'RIGHT')
1117
1118
safe_points = list()
1119
for i in range(1, self.dimrow + 1):
1120
for j in range(1, self.dimrow + 1):
1121
if self.kb.ask_if_true(ok_to_move(i, j, self.t)):
1122
safe_points.append([i, j])
1123
1124
if self.kb.ask_if_true(percept_glitter(self.t)):
1125
goals = list()
1126
goals.append([1, 1])
1127
self.plan.append('Grab')
1128
actions = self.plan_route(self.current_position, goals, safe_points)
1129
self.plan.extend(actions)
1130
self.plan.append('Climb')
1131
1132
if len(self.plan) == 0:
1133
unvisited = list()
1134
for i in range(1, self.dimrow + 1):
1135
for j in range(1, self.dimrow + 1):
1136
for k in range(self.t):
1137
if self.kb.ask_if_true(location(i, j, k)):
1138
unvisited.append([i, j])
1139
unvisited_and_safe = list()
1140
for u in unvisited:
1141
for s in safe_points:
1142
if u not in unvisited_and_safe and s == u:
1143
unvisited_and_safe.append(u)
1144
1145
temp = self.plan_route(self.current_position, unvisited_and_safe, safe_points)
1146
self.plan.extend(temp)
1147
1148
if len(self.plan) == 0 and self.kb.ask_if_true(have_arrow(self.t)):
1149
possible_wumpus = list()
1150
for i in range(1, self.dimrow + 1):
1151
for j in range(1, self.dimrow + 1):
1152
if not self.kb.ask_if_true(wumpus(i, j)):
1153
possible_wumpus.append([i, j])
1154
1155
temp = self.plan_shot(self.current_position, possible_wumpus, safe_points)
1156
self.plan.extend(temp)
1157
1158
if len(self.plan) == 0:
1159
not_unsafe = list()
1160
for i in range(1, self.dimrow + 1):
1161
for j in range(1, self.dimrow + 1):
1162
if not self.kb.ask_if_true(ok_to_move(i, j, self.t)):
1163
not_unsafe.append([i, j])
1164
temp = self.plan_route(self.current_position, not_unsafe, safe_points)
1165
self.plan.extend(temp)
1166
1167
if len(self.plan) == 0:
1168
start = list()
1169
start.append([1, 1])
1170
temp = self.plan_route(self.current_position, start, safe_points)
1171
self.plan.extend(temp)
1172
self.plan.append('Climb')
1173
1174
action = self.plan[0]
1175
self.plan = self.plan[1:]
1176
self.kb.make_action_sentence(action, self.t)
1177
self.t += 1
1178
1179
return action
1180
1181
def plan_route(self, current, goals, allowed):
1182
problem = PlanRoute(current, goals, allowed, self.dimrow)
1183
return astar_search(problem).solution()
1184
1185
def plan_shot(self, current, goals, allowed):
1186
shooting_positions = set()
1187
1188
for loc in goals:
1189
x = loc[0]
1190
y = loc[1]
1191
for i in range(1, self.dimrow + 1):
1192
if i < x:
1193
shooting_positions.add(WumpusPosition(i, y, 'EAST'))
1194
if i > x:
1195
shooting_positions.add(WumpusPosition(i, y, 'WEST'))
1196
if i < y:
1197
shooting_positions.add(WumpusPosition(x, i, 'NORTH'))
1198
if i > y:
1199
shooting_positions.add(WumpusPosition(x, i, 'SOUTH'))
1200
1201
# Can't have a shooting position from any of the rooms the Wumpus could reside
1202
orientations = ['EAST', 'WEST', 'NORTH', 'SOUTH']
1203
for loc in goals:
1204
for orientation in orientations:
1205
shooting_positions.remove(WumpusPosition(loc[0], loc[1], orientation))
1206
1207
actions = list()
1208
actions.extend(self.plan_route(current, shooting_positions, allowed))
1209
actions.append('Shoot')
1210
return actions
1211
1212
1213
# ______________________________________________________________________________
1214
# 7.7.4 Making plans by propositional inference
1215
1216
1217
def SAT_plan(init, transition, goal, t_max, SAT_solver=dpll_satisfiable):
1218
"""Converts a planning problem to Satisfaction problem by translating it to a cnf sentence.
1219
[Figure 7.22]
1220
>>> transition = {'A': {'Left': 'A', 'Right': 'B'}, 'B': {'Left': 'A', 'Right': 'C'}, 'C': {'Left': 'B', 'Right': 'C'}}
1221
>>> SAT_plan('A', transition, 'C', 2) is None
1222
True
1223
"""
1224
1225
# Functions used by SAT_plan
1226
def translate_to_SAT(init, transition, goal, time):
1227
clauses = []
1228
states = [state for state in transition]
1229
1230
# Symbol claiming state s at time t
1231
state_counter = itertools.count()
1232
for s in states:
1233
for t in range(time + 1):
1234
state_sym[s, t] = Expr("State_{}".format(next(state_counter)))
1235
1236
# Add initial state axiom
1237
clauses.append(state_sym[init, 0])
1238
1239
# Add goal state axiom
1240
clauses.append(state_sym[goal, time])
1241
1242
# All possible transitions
1243
transition_counter = itertools.count()
1244
for s in states:
1245
for action in transition[s]:
1246
s_ = transition[s][action]
1247
for t in range(time):
1248
# Action 'action' taken from state 's' at time 't' to reach 's_'
1249
action_sym[s, action, t] = Expr(
1250
"Transition_{}".format(next(transition_counter)))
1251
1252
# Change the state from s to s_
1253
clauses.append(action_sym[s, action, t] | '==>' | state_sym[s, t])
1254
clauses.append(action_sym[s, action, t] | '==>' | state_sym[s_, t + 1])
1255
1256
# Allow only one state at any time
1257
for t in range(time + 1):
1258
# must be a state at any time
1259
clauses.append(associate('|', [state_sym[s, t] for s in states]))
1260
1261
for s in states:
1262
for s_ in states[states.index(s) + 1:]:
1263
# for each pair of states s, s_ only one is possible at time t
1264
clauses.append((~state_sym[s, t]) | (~state_sym[s_, t]))
1265
1266
# Restrict to one transition per timestep
1267
for t in range(time):
1268
# list of possible transitions at time t
1269
transitions_t = [tr for tr in action_sym if tr[2] == t]
1270
1271
# make sure at least one of the transitions happens
1272
clauses.append(associate('|', [action_sym[tr] for tr in transitions_t]))
1273
1274
for tr in transitions_t:
1275
for tr_ in transitions_t[transitions_t.index(tr) + 1:]:
1276
# there cannot be two transitions tr and tr_ at time t
1277
clauses.append(~action_sym[tr] | ~action_sym[tr_])
1278
1279
# Combine the clauses to form the cnf
1280
return associate('&', clauses)
1281
1282
def extract_solution(model):
1283
true_transitions = [t for t in action_sym if model[action_sym[t]]]
1284
# Sort transitions based on time, which is the 3rd element of the tuple
1285
true_transitions.sort(key=lambda x: x[2])
1286
return [action for s, action, time in true_transitions]
1287
1288
# Body of SAT_plan algorithm
1289
for t in range(t_max):
1290
# dictionaries to help extract the solution from model
1291
state_sym = {}
1292
action_sym = {}
1293
1294
cnf = translate_to_SAT(init, transition, goal, t)
1295
model = SAT_solver(cnf)
1296
if model is not False:
1297
return extract_solution(model)
1298
return None
1299
1300
1301
# ______________________________________________________________________________
1302
# Chapter 9 Inference in First Order Logic
1303
# 9.2 Unification and First Order Inference
1304
# 9.2.1 Unification
1305
1306
1307
def unify(x, y, s={}):
1308
"""Unify expressions x,y with substitution s; return a substitution that
1309
would make x,y equal, or None if x,y can not unify. x and y can be
1310
variables (e.g. Expr('x')), constants, lists, or Exprs. [Figure 9.1]
1311
>>> unify(x, 3, {})
1312
{x: 3}
1313
"""
1314
if s is None:
1315
return None
1316
elif x == y:
1317
return s
1318
elif is_variable(x):
1319
return unify_var(x, y, s)
1320
elif is_variable(y):
1321
return unify_var(y, x, s)
1322
elif isinstance(x, Expr) and isinstance(y, Expr):
1323
return unify(x.args, y.args, unify(x.op, y.op, s))
1324
elif isinstance(x, str) or isinstance(y, str):
1325
return None
1326
elif issequence(x) and issequence(y) and len(x) == len(y):
1327
if not x:
1328
return s
1329
return unify(x[1:], y[1:], unify(x[0], y[0], s))
1330
else:
1331
return None
1332
1333
1334
def is_variable(x):
1335
"""A variable is an Expr with no args and a lowercase symbol as the op."""
1336
return isinstance(x, Expr) and not x.args and x.op[0].islower()
1337
1338
1339
def unify_var(var, x, s):
1340
if var in s:
1341
return unify(s[var], x, s)
1342
elif x in s:
1343
return unify(var, s[x], s)
1344
elif occur_check(var, x, s):
1345
return None
1346
else:
1347
return extend(s, var, x)
1348
1349
1350
def occur_check(var, x, s):
1351
"""Return true if variable var occurs anywhere in x
1352
(or in subst(s, x), if s has a binding for x)."""
1353
if var == x:
1354
return True
1355
elif is_variable(x) and x in s:
1356
return occur_check(var, s[x], s)
1357
elif isinstance(x, Expr):
1358
return (occur_check(var, x.op, s) or
1359
occur_check(var, x.args, s))
1360
elif isinstance(x, (list, tuple)):
1361
return first(e for e in x if occur_check(var, e, s))
1362
else:
1363
return False
1364
1365
1366
def extend(s, var, val):
1367
"""Copy the substitution s and extend it by setting var to val; return copy.
1368
>>> extend({x: 1}, y, 2) == {x: 1, y: 2}
1369
True
1370
"""
1371
s2 = s.copy()
1372
s2[var] = val
1373
return s2
1374
1375
1376
# 9.2.2 Storage and retrieval
1377
1378
1379
class FolKB(KB):
1380
"""A knowledge base consisting of first-order definite clauses.
1381
>>> kb0 = FolKB([expr('Farmer(Mac)'), expr('Rabbit(Pete)'),
1382
... expr('(Rabbit(r) & Farmer(f)) ==> Hates(f, r)')])
1383
>>> kb0.tell(expr('Rabbit(Flopsie)'))
1384
>>> kb0.retract(expr('Rabbit(Pete)'))
1385
>>> kb0.ask(expr('Hates(Mac, x)'))[x]
1386
Flopsie
1387
>>> kb0.ask(expr('Wife(Pete, x)'))
1388
False
1389
"""
1390
1391
def __init__(self, initial_clauses=None):
1392
self.clauses = [] # inefficient: no indexing
1393
if initial_clauses:
1394
for clause in initial_clauses:
1395
self.tell(clause)
1396
1397
def tell(self, sentence):
1398
if is_definite_clause(sentence):
1399
self.clauses.append(sentence)
1400
else:
1401
raise Exception("Not a definite clause: {}".format(sentence))
1402
1403
def ask_generator(self, query):
1404
return fol_bc_ask(self, query)
1405
1406
def retract(self, sentence):
1407
self.clauses.remove(sentence)
1408
1409
def fetch_rules_for_goal(self, goal):
1410
return self.clauses
1411
1412
1413
# ______________________________________________________________________________
1414
# 9.3 Forward Chaining
1415
# 9.3.2 A simple forward-chaining algorithm
1416
1417
1418
def fol_fc_ask(KB, alpha):
1419
"""A simple forward-chaining algorithm. [Figure 9.3]"""
1420
kb_consts = list({c for clause in KB.clauses for c in constant_symbols(clause)})
1421
1422
def enum_subst(p):
1423
query_vars = list({v for clause in p for v in variables(clause)})
1424
for assignment_list in itertools.product(kb_consts, repeat=len(query_vars)):
1425
theta = {x: y for x, y in zip(query_vars, assignment_list)}
1426
yield theta
1427
1428
# check if we can answer without new inferences
1429
for q in KB.clauses:
1430
phi = unify(q, alpha, {})
1431
if phi is not None:
1432
yield phi
1433
1434
while True:
1435
new = []
1436
for rule in KB.clauses:
1437
p, q = parse_definite_clause(rule)
1438
for theta in enum_subst(p):
1439
if set(subst(theta, p)).issubset(set(KB.clauses)):
1440
q_ = subst(theta, q)
1441
if all([unify(x, q_, {}) is None for x in KB.clauses + new]):
1442
new.append(q_)
1443
phi = unify(q_, alpha, {})
1444
if phi is not None:
1445
yield phi
1446
if not new:
1447
break
1448
for clause in new:
1449
KB.tell(clause)
1450
return None
1451
1452
1453
def subst(s, x):
1454
"""Substitute the substitution s into the expression x.
1455
>>> subst({x: 42, y:0}, F(x) + y)
1456
(F(42) + 0)
1457
"""
1458
if isinstance(x, list):
1459
return [subst(s, xi) for xi in x]
1460
elif isinstance(x, tuple):
1461
return tuple([subst(s, xi) for xi in x])
1462
elif not isinstance(x, Expr):
1463
return x
1464
elif is_var_symbol(x.op):
1465
return s.get(x, x)
1466
else:
1467
return Expr(x.op, *[subst(s, arg) for arg in x.args])
1468
1469
1470
def standardize_variables(sentence, dic=None):
1471
"""Replace all the variables in sentence with new variables."""
1472
if dic is None:
1473
dic = {}
1474
if not isinstance(sentence, Expr):
1475
return sentence
1476
elif is_var_symbol(sentence.op):
1477
if sentence in dic:
1478
return dic[sentence]
1479
else:
1480
v = Expr('v_{}'.format(next(standardize_variables.counter)))
1481
dic[sentence] = v
1482
return v
1483
else:
1484
return Expr(sentence.op,
1485
*[standardize_variables(a, dic) for a in sentence.args])
1486
1487
1488
standardize_variables.counter = itertools.count()
1489
1490
1491
# __________________________________________________________________
1492
# 9.4 Backward Chaining
1493
1494
1495
def fol_bc_ask(KB, query):
1496
"""A simple backward-chaining algorithm for first-order logic. [Figure 9.6]
1497
KB should be an instance of FolKB, and query an atomic sentence."""
1498
return fol_bc_or(KB, query, {})
1499
1500
1501
def fol_bc_or(KB, goal, theta):
1502
for rule in KB.fetch_rules_for_goal(goal):
1503
lhs, rhs = parse_definite_clause(standardize_variables(rule))
1504
for theta1 in fol_bc_and(KB, lhs, unify(rhs, goal, theta)):
1505
yield theta1
1506
1507
1508
def fol_bc_and(KB, goals, theta):
1509
if theta is None:
1510
pass
1511
elif not goals:
1512
yield theta
1513
else:
1514
first, rest = goals[0], goals[1:]
1515
for theta1 in fol_bc_or(KB, subst(theta, first), theta):
1516
for theta2 in fol_bc_and(KB, rest, theta1):
1517
yield theta2
1518
1519
1520
# ______________________________________________________________________________
1521
# A simple KB that defines the relevant conditions of the Wumpus World as in Fig 7.4.
1522
# See Sec. 7.4.3
1523
wumpus_kb = PropKB()
1524
1525
P11, P12, P21, P22, P31, B11, B21 = expr('P11, P12, P21, P22, P31, B11, B21')
1526
wumpus_kb.tell(~P11)
1527
wumpus_kb.tell(B11 | '<=>' | (P12 | P21))
1528
wumpus_kb.tell(B21 | '<=>' | (P11 | P22 | P31))
1529
wumpus_kb.tell(~B11)
1530
wumpus_kb.tell(B21)
1531
1532
test_kb = FolKB(
1533
map(expr, ['Farmer(Mac)',
1534
'Rabbit(Pete)',
1535
'Mother(MrsMac, Mac)',
1536
'Mother(MrsRabbit, Pete)',
1537
'(Rabbit(r) & Farmer(f)) ==> Hates(f, r)',
1538
'(Mother(m, c)) ==> Loves(m, c)',
1539
'(Mother(m, r) & Rabbit(r)) ==> Rabbit(m)',
1540
'(Farmer(f)) ==> Human(f)',
1541
# Note that this order of conjuncts
1542
# would result in infinite recursion:
1543
# '(Human(h) & Mother(m, h)) ==> Human(m)'
1544
'(Mother(m, h) & Human(h)) ==> Human(m)']))
1545
1546
crime_kb = FolKB(
1547
map(expr, ['(American(x) & Weapon(y) & Sells(x, y, z) & Hostile(z)) ==> Criminal(x)',
1548
'Owns(Nono, M1)',
1549
'Missile(M1)',
1550
'(Missile(x) & Owns(Nono, x)) ==> Sells(West, x, Nono)',
1551
'Missile(x) ==> Weapon(x)',
1552
'Enemy(x, America) ==> Hostile(x)',
1553
'American(West)',
1554
'Enemy(Nono, America)']))
1555
1556
1557
# ______________________________________________________________________________
1558
1559
# Example application (not in the book).
1560
# You can use the Expr class to do symbolic differentiation. This used to be
1561
# a part of AI; now it is considered a separate field, Symbolic Algebra.
1562
1563
1564
def diff(y, x):
1565
"""Return the symbolic derivative, dy/dx, as an Expr.
1566
However, you probably want to simplify the results with simp.
1567
>>> diff(x * x, x)
1568
((x * 1) + (x * 1))
1569
"""
1570
if y == x:
1571
return 1
1572
elif not y.args:
1573
return 0
1574
else:
1575
u, op, v = y.args[0], y.op, y.args[-1]
1576
if op == '+':
1577
return diff(u, x) + diff(v, x)
1578
elif op == '-' and len(y.args) == 1:
1579
return -diff(u, x)
1580
elif op == '-':
1581
return diff(u, x) - diff(v, x)
1582
elif op == '*':
1583
return u * diff(v, x) + v * diff(u, x)
1584
elif op == '/':
1585
return (v * diff(u, x) - u * diff(v, x)) / (v * v)
1586
elif op == '**' and isnumber(x.op):
1587
return (v * u ** (v - 1) * diff(u, x))
1588
elif op == '**':
1589
return (v * u ** (v - 1) * diff(u, x) +
1590
u ** v * Expr('log')(u) * diff(v, x))
1591
elif op == 'log':
1592
return diff(u, x) / u
1593
else:
1594
raise ValueError("Unknown op: {} in diff({}, {})".format(op, y, x))
1595
1596
1597
def simp(x):
1598
"""Simplify the expression x."""
1599
if isnumber(x) or not x.args:
1600
return x
1601
args = list(map(simp, x.args))
1602
u, op, v = args[0], x.op, args[-1]
1603
if op == '+':
1604
if v == 0:
1605
return u
1606
if u == 0:
1607
return v
1608
if u == v:
1609
return 2 * u
1610
if u == -v or v == -u:
1611
return 0
1612
elif op == '-' and len(args) == 1:
1613
if u.op == '-' and len(u.args) == 1:
1614
return u.args[0] # --y ==> y
1615
elif op == '-':
1616
if v == 0:
1617
return u
1618
if u == 0:
1619
return -v
1620
if u == v:
1621
return 0
1622
if u == -v or v == -u:
1623
return 0
1624
elif op == '*':
1625
if u == 0 or v == 0:
1626
return 0
1627
if u == 1:
1628
return v
1629
if v == 1:
1630
return u
1631
if u == v:
1632
return u ** 2
1633
elif op == '/':
1634
if u == 0:
1635
return 0
1636
if v == 0:
1637
return Expr('Undefined')
1638
if u == v:
1639
return 1
1640
if u == -v or v == -u:
1641
return 0
1642
elif op == '**':
1643
if u == 0:
1644
return 0
1645
if v == 0:
1646
return 1
1647
if u == 1:
1648
return 1
1649
if v == 1:
1650
return u
1651
elif op == 'log':
1652
if u == 1:
1653
return 0
1654
else:
1655
raise ValueError("Unknown op: " + op)
1656
# If we fall through to here, we can not simplify further
1657
return Expr(op, *args)
1658
1659
1660
def d(y, x):
1661
"""Differentiate and then simplify.
1662
>>> d(x * x - x, x)
1663
((2 * x) - 1)
1664
"""
1665
return simp(diff(y, x))
1666
1667