Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
aimacode
GitHub Repository: aimacode/aima-python
Path: blob/master/logic.py
615 views
1
"""
2
Representations and Inference for Logic. (Chapters 7-9, 12)
3
4
Covers both Propositional and First-Order Logic. First we have four
5
important data types:
6
7
KB Abstract class holds a knowledge base of logical expressions
8
KB_Agent Abstract class subclasses agents.Agent
9
Expr A logical expression, imported from utils.py
10
substitution Implemented as a dictionary of var:value pairs, {x:1, y:x}
11
12
Be careful: some functions take an Expr as argument, and some take a KB.
13
14
Logical expressions can be created with Expr or expr, imported from utils, TODO
15
or with expr, which adds the capability to write a string that uses
16
the connectives ==>, <==, <=>, or <=/=>. But be careful: these have the
17
operator precedence of commas; you may need to add parens to make precedence work.
18
See logic.ipynb for examples.
19
20
Then we implement various functions for doing logical inference:
21
22
pl_true Evaluate a propositional logical sentence in a model
23
tt_entails Say if a statement is entailed by a KB
24
pl_resolution Do resolution on propositional sentences
25
dpll_satisfiable See if a propositional sentence is satisfiable
26
WalkSAT Try to find a solution for a set of clauses
27
28
And a few other functions:
29
30
to_cnf Convert to conjunctive normal form
31
unify Do unification of two FOL sentences
32
diff, simp Symbolic differentiation and simplification
33
"""
34
35
import heapq
36
import itertools
37
import random
38
from collections import defaultdict, Counter
39
40
import networkx as nx
41
42
from agents import Agent, Glitter, Bump, Stench, Breeze, Scream
43
from csp import parse_neighbors, UniversalDict
44
from search import astar_search, PlanRoute
45
from utils import remove_all, unique, first, probability, isnumber, issequence, Expr, expr, subexpressions, extend
46
47
48
class KB:
49
"""A knowledge base to which you can tell and ask sentences.
50
To create a KB, first subclass this class and implement
51
tell, ask_generator, and retract. Why ask_generator instead of ask?
52
The book is a bit vague on what ask means --
53
For a Propositional Logic KB, ask(P & Q) returns True or False, but for an
54
FOL KB, something like ask(Brother(x, y)) might return many substitutions
55
such as {x: Cain, y: Abel}, {x: Abel, y: Cain}, {x: George, y: Jeb}, etc.
56
So ask_generator generates these one at a time, and ask either returns the
57
first one or returns False."""
58
59
def __init__(self, sentence=None):
60
if sentence:
61
self.tell(sentence)
62
63
def tell(self, sentence):
64
"""Add the sentence to the KB."""
65
raise NotImplementedError
66
67
def ask(self, query):
68
"""Return a substitution that makes the query true, or, failing that, return False."""
69
return first(self.ask_generator(query), default=False)
70
71
def ask_generator(self, query):
72
"""Yield all the substitutions that make query true."""
73
raise NotImplementedError
74
75
def retract(self, sentence):
76
"""Remove sentence from the KB."""
77
raise NotImplementedError
78
79
80
class PropKB(KB):
81
"""A KB for propositional logic. Inefficient, with no indexing."""
82
83
def __init__(self, sentence=None):
84
super().__init__(sentence)
85
self.clauses = []
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
# ______________________________________________________________________________
110
111
112
def KBAgentProgram(kb):
113
"""
114
[Figure 7.1]
115
A generic logical knowledge-based agent program.
116
"""
117
steps = itertools.count()
118
119
def program(percept):
120
t = next(steps)
121
kb.tell(make_percept_sentence(percept, t))
122
action = kb.ask(make_action_query(t))
123
kb.tell(make_action_sentence(action, t))
124
return action
125
126
def make_percept_sentence(percept, t):
127
return Expr('Percept')(percept, t)
128
129
def make_action_query(t):
130
return expr('ShouldDo(action, {})'.format(t))
131
132
def make_action_sentence(action, t):
133
return Expr('Did')(action[expr('action')], t)
134
135
return program
136
137
138
def is_symbol(s):
139
"""A string s is a symbol if it starts with an alphabetic char.
140
>>> is_symbol('R2D2')
141
True
142
"""
143
return isinstance(s, str) and s[:1].isalpha()
144
145
146
def is_var_symbol(s):
147
"""A logic variable symbol is an initial-lowercase string.
148
>>> is_var_symbol('EXE')
149
False
150
"""
151
return is_symbol(s) and s[0].islower()
152
153
154
def is_prop_symbol(s):
155
"""A proposition logic symbol is an initial-uppercase string.
156
>>> is_prop_symbol('exe')
157
False
158
"""
159
return is_symbol(s) and s[0].isupper()
160
161
162
def variables(s):
163
"""Return a set of the variables in expression s.
164
>>> variables(expr('F(x, x) & G(x, y) & H(y, z) & R(A, z, 2)')) == {x, y, z}
165
True
166
"""
167
return {x for x in subexpressions(s) if is_variable(x)}
168
169
170
def is_definite_clause(s):
171
"""Returns True for exprs s of the form A & B & ... & C ==> D,
172
where all literals are positive. In clause form, this is
173
~A | ~B | ... | ~C | D, where exactly one clause is positive.
174
>>> is_definite_clause(expr('Farmer(Mac)'))
175
True
176
"""
177
if is_symbol(s.op):
178
return True
179
elif s.op == '==>':
180
antecedent, consequent = s.args
181
return is_symbol(consequent.op) and all(is_symbol(arg.op) for arg in conjuncts(antecedent))
182
else:
183
return False
184
185
186
def parse_definite_clause(s):
187
"""Return the antecedents and the consequent of a definite clause."""
188
assert is_definite_clause(s)
189
if is_symbol(s.op):
190
return [], s
191
else:
192
antecedent, consequent = s.args
193
return conjuncts(antecedent), consequent
194
195
196
# Useful constant Exprs used in examples and code:
197
A, B, C, D, E, F, G, P, Q, a, x, y, z, u = map(Expr, 'ABCDEFGPQaxyzu')
198
199
200
# ______________________________________________________________________________
201
202
203
def tt_entails(kb, alpha):
204
"""
205
[Figure 7.10]
206
Does kb entail the sentence alpha? Use truth tables. For propositional
207
kb's and sentences. Note that the 'kb' should be an Expr which is a
208
conjunction of clauses.
209
>>> tt_entails(expr('P & Q'), expr('Q'))
210
True
211
"""
212
assert not variables(alpha)
213
symbols = list(prop_symbols(kb & alpha))
214
return tt_check_all(kb, alpha, symbols, {})
215
216
217
def tt_check_all(kb, alpha, symbols, model):
218
"""Auxiliary routine to implement tt_entails."""
219
if not symbols:
220
if pl_true(kb, model):
221
result = pl_true(alpha, model)
222
assert result in (True, False)
223
return result
224
else:
225
return True
226
else:
227
P, rest = symbols[0], symbols[1:]
228
return (tt_check_all(kb, alpha, rest, extend(model, P, True)) and
229
tt_check_all(kb, alpha, rest, extend(model, P, False)))
230
231
232
def prop_symbols(x):
233
"""Return the set of all propositional symbols in x."""
234
if not isinstance(x, Expr):
235
return set()
236
elif is_prop_symbol(x.op):
237
return {x}
238
else:
239
return {symbol for arg in x.args for symbol in prop_symbols(arg)}
240
241
242
def constant_symbols(x):
243
"""Return the set of all constant symbols in x."""
244
if not isinstance(x, Expr):
245
return set()
246
elif is_prop_symbol(x.op) and not x.args:
247
return {x}
248
else:
249
return {symbol for arg in x.args for symbol in constant_symbols(arg)}
250
251
252
def predicate_symbols(x):
253
"""Return a set of (symbol_name, arity) in x.
254
All symbols (even functional) with arity > 0 are considered."""
255
if not isinstance(x, Expr) or not x.args:
256
return set()
257
pred_set = {(x.op, len(x.args))} if is_prop_symbol(x.op) else set()
258
pred_set.update({symbol for arg in x.args for symbol in predicate_symbols(arg)})
259
return pred_set
260
261
262
def tt_true(s):
263
"""Is a propositional sentence a tautology?
264
>>> tt_true('P | ~P')
265
True
266
"""
267
s = expr(s)
268
return tt_entails(True, s)
269
270
271
def pl_true(exp, model={}):
272
"""Return True if the propositional logic expression is true in the model,
273
and False if it is false. If the model does not specify the value for
274
every proposition, this may return None to indicate 'not obvious';
275
this may happen even when the expression is tautological.
276
>>> pl_true(P, {}) is None
277
True
278
"""
279
if exp in (True, False):
280
return exp
281
op, args = exp.op, exp.args
282
if is_prop_symbol(op):
283
return model.get(exp)
284
elif op == '~':
285
p = pl_true(args[0], model)
286
if p is None:
287
return None
288
else:
289
return not p
290
elif op == '|':
291
result = False
292
for arg in args:
293
p = pl_true(arg, model)
294
if p is True:
295
return True
296
if p is None:
297
result = None
298
return result
299
elif op == '&':
300
result = True
301
for arg in args:
302
p = pl_true(arg, model)
303
if p is False:
304
return False
305
if p is None:
306
result = None
307
return result
308
p, q = args
309
if op == '==>':
310
return pl_true(~p | q, model)
311
elif op == '<==':
312
return pl_true(p | ~q, model)
313
pt = pl_true(p, model)
314
if pt is None:
315
return None
316
qt = pl_true(q, model)
317
if qt is None:
318
return None
319
if op == '<=>':
320
return pt == qt
321
elif op == '^': # xor or 'not equivalent'
322
return pt != qt
323
else:
324
raise ValueError('Illegal operator in logic expression' + str(exp))
325
326
327
# ______________________________________________________________________________
328
329
# Convert to Conjunctive Normal Form (CNF)
330
331
332
def to_cnf(s):
333
"""
334
[Page 253]
335
Convert a propositional logical sentence to conjunctive normal form.
336
That is, to the form ((A | ~B | ...) & (B | C | ...) & ...)
337
>>> to_cnf('~(B | C)')
338
(~B & ~C)
339
"""
340
s = expr(s)
341
if isinstance(s, str):
342
s = expr(s)
343
s = eliminate_implications(s) # Steps 1, 2 from p. 253
344
s = move_not_inwards(s) # Step 3
345
return distribute_and_over_or(s) # Step 4
346
347
348
def eliminate_implications(s):
349
"""Change implications into equivalent form with only &, |, and ~ as logical operators."""
350
s = expr(s)
351
if not s.args or is_symbol(s.op):
352
return s # Atoms are unchanged.
353
args = list(map(eliminate_implications, s.args))
354
a, b = args[0], args[-1]
355
if s.op == '==>':
356
return b | ~a
357
elif s.op == '<==':
358
return a | ~b
359
elif s.op == '<=>':
360
return (a | ~b) & (b | ~a)
361
elif s.op == '^':
362
assert len(args) == 2 # TODO: relax this restriction
363
return (a & ~b) | (~a & b)
364
else:
365
assert s.op in ('&', '|', '~')
366
return Expr(s.op, *args)
367
368
369
def move_not_inwards(s):
370
"""Rewrite sentence s by moving negation sign inward.
371
>>> move_not_inwards(~(A | B))
372
(~A & ~B)
373
"""
374
s = expr(s)
375
if s.op == '~':
376
def NOT(b):
377
return move_not_inwards(~b)
378
379
a = s.args[0]
380
if a.op == '~':
381
return move_not_inwards(a.args[0]) # ~~A ==> A
382
if a.op == '&':
383
return associate('|', list(map(NOT, a.args)))
384
if a.op == '|':
385
return associate('&', list(map(NOT, a.args)))
386
return s
387
elif is_symbol(s.op) or not s.args:
388
return s
389
else:
390
return Expr(s.op, *list(map(move_not_inwards, s.args)))
391
392
393
def distribute_and_over_or(s):
394
"""Given a sentence s consisting of conjunctions and disjunctions
395
of literals, return an equivalent sentence in CNF.
396
>>> distribute_and_over_or((A & B) | C)
397
((A | C) & (B | C))
398
"""
399
s = expr(s)
400
if s.op == '|':
401
s = associate('|', s.args)
402
if s.op != '|':
403
return distribute_and_over_or(s)
404
if len(s.args) == 0:
405
return False
406
if len(s.args) == 1:
407
return distribute_and_over_or(s.args[0])
408
conj = first(arg for arg in s.args if arg.op == '&')
409
if not conj:
410
return s
411
others = [a for a in s.args if a is not conj]
412
rest = associate('|', others)
413
return associate('&', [distribute_and_over_or(c | rest)
414
for c in conj.args])
415
elif s.op == '&':
416
return associate('&', list(map(distribute_and_over_or, s.args)))
417
else:
418
return s
419
420
421
def associate(op, args):
422
"""Given an associative op, return an expression with the same
423
meaning as Expr(op, *args), but flattened -- that is, with nested
424
instances of the same op promoted to the top level.
425
>>> associate('&', [(A&B),(B|C),(B&C)])
426
(A & B & (B | C) & B & C)
427
>>> associate('|', [A|(B|(C|(A&B)))])
428
(A | B | C | (A & B))
429
"""
430
args = dissociate(op, args)
431
if len(args) == 0:
432
return _op_identity[op]
433
elif len(args) == 1:
434
return args[0]
435
else:
436
return Expr(op, *args)
437
438
439
_op_identity = {'&': True, '|': False, '+': 0, '*': 1}
440
441
442
def dissociate(op, args):
443
"""Given an associative op, return a flattened list result such
444
that Expr(op, *result) means the same as Expr(op, *args).
445
>>> dissociate('&', [A & B])
446
[A, B]
447
"""
448
result = []
449
450
def collect(subargs):
451
for arg in subargs:
452
if arg.op == op:
453
collect(arg.args)
454
else:
455
result.append(arg)
456
457
collect(args)
458
return result
459
460
461
def conjuncts(s):
462
"""Return a list of the conjuncts in the sentence s.
463
>>> conjuncts(A & B)
464
[A, B]
465
>>> conjuncts(A | B)
466
[(A | B)]
467
"""
468
return dissociate('&', [s])
469
470
471
def disjuncts(s):
472
"""Return a list of the disjuncts in the sentence s.
473
>>> disjuncts(A | B)
474
[A, B]
475
>>> disjuncts(A & B)
476
[(A & B)]
477
"""
478
return dissociate('|', [s])
479
480
481
# ______________________________________________________________________________
482
483
484
def pl_resolution(kb, alpha):
485
"""
486
[Figure 7.12]
487
Propositional-logic resolution: say if alpha follows from KB.
488
>>> pl_resolution(horn_clauses_KB, A)
489
True
490
"""
491
clauses = kb.clauses + conjuncts(to_cnf(~alpha))
492
new = set()
493
while True:
494
n = len(clauses)
495
pairs = [(clauses[i], clauses[j])
496
for i in range(n) for j in range(i + 1, n)]
497
for (ci, cj) in pairs:
498
resolvents = pl_resolve(ci, cj)
499
if False in resolvents:
500
return True
501
new = new.union(set(resolvents))
502
if new.issubset(set(clauses)):
503
return False
504
for c in new:
505
if c not in clauses:
506
clauses.append(c)
507
508
509
def pl_resolve(ci, cj):
510
"""Return all clauses that can be obtained by resolving clauses ci and cj."""
511
clauses = []
512
for di in disjuncts(ci):
513
for dj in disjuncts(cj):
514
if di == ~dj or ~di == dj:
515
clauses.append(associate('|', unique(remove_all(di, disjuncts(ci)) + remove_all(dj, disjuncts(cj)))))
516
return clauses
517
518
519
# ______________________________________________________________________________
520
521
522
class PropDefiniteKB(PropKB):
523
"""A KB of propositional definite clauses."""
524
525
def tell(self, sentence):
526
"""Add a definite clause to this KB."""
527
assert is_definite_clause(sentence), "Must be definite clause"
528
self.clauses.append(sentence)
529
530
def ask_generator(self, query):
531
"""Yield the empty substitution if KB implies query; else nothing."""
532
if pl_fc_entails(self.clauses, query):
533
yield {}
534
535
def retract(self, sentence):
536
self.clauses.remove(sentence)
537
538
def clauses_with_premise(self, p):
539
"""Return a list of the clauses in KB that have p in their premise.
540
This could be cached away for O(1) speed, but we'll recompute it."""
541
return [c for c in self.clauses if c.op == '==>' and p in conjuncts(c.args[0])]
542
543
544
def pl_fc_entails(kb, q):
545
"""
546
[Figure 7.15]
547
Use forward chaining to see if a PropDefiniteKB entails symbol q.
548
>>> pl_fc_entails(horn_clauses_KB, expr('Q'))
549
True
550
"""
551
count = {c: len(conjuncts(c.args[0])) for c in kb.clauses if c.op == '==>'}
552
inferred = defaultdict(bool)
553
agenda = [s for s in kb.clauses if is_prop_symbol(s.op)]
554
while agenda:
555
p = agenda.pop()
556
if p == q:
557
return True
558
if not inferred[p]:
559
inferred[p] = True
560
for c in kb.clauses_with_premise(p):
561
count[c] -= 1
562
if count[c] == 0:
563
agenda.append(c.args[1])
564
return False
565
566
567
"""
568
[Figure 7.13]
569
Simple inference in a wumpus world example
570
"""
571
wumpus_world_inference = expr('(B11 <=> (P12 | P21)) & ~B11')
572
573
"""
574
[Figure 7.16]
575
Propositional Logic Forward Chaining example
576
"""
577
horn_clauses_KB = PropDefiniteKB()
578
for clause in ['P ==> Q',
579
'(L & M) ==> P',
580
'(B & L) ==> M',
581
'(A & P) ==> L',
582
'(A & B) ==> L',
583
'A', 'B']:
584
horn_clauses_KB.tell(expr(clause))
585
586
"""
587
Definite clauses KB example
588
"""
589
definite_clauses_KB = PropDefiniteKB()
590
for clause in ['(B & F) ==> E',
591
'(A & E & F) ==> G',
592
'(B & C) ==> F',
593
'(A & B) ==> D',
594
'(E & F) ==> H',
595
'(H & I) ==>J',
596
'A', 'B', 'C']:
597
definite_clauses_KB.tell(expr(clause))
598
599
600
# ______________________________________________________________________________
601
# Heuristics for SAT Solvers
602
603
604
def no_branching_heuristic(symbols, clauses):
605
return first(symbols), True
606
607
608
def min_clauses(clauses):
609
min_len = min(map(lambda c: len(c.args), clauses), default=2)
610
return filter(lambda c: len(c.args) == (min_len if min_len > 1 else 2), clauses)
611
612
613
def moms(symbols, clauses):
614
"""
615
MOMS (Maximum Occurrence in clauses of Minimum Size) heuristic
616
Returns the literal with the most occurrences in all clauses of minimum size
617
"""
618
scores = Counter(l for c in min_clauses(clauses) for l in prop_symbols(c))
619
return max(symbols, key=lambda symbol: scores[symbol]), True
620
621
622
def momsf(symbols, clauses, k=0):
623
"""
624
MOMS alternative heuristic
625
If f(x) the number of occurrences of the variable x in clauses with minimum size,
626
we choose the variable maximizing [f(x) + f(-x)] * 2^k + f(x) * f(-x)
627
Returns x if f(x) >= f(-x) otherwise -x
628
"""
629
scores = Counter(l for c in min_clauses(clauses) for l in disjuncts(c))
630
P = max(symbols,
631
key=lambda symbol: (scores[symbol] + scores[~symbol]) * pow(2, k) + scores[symbol] * scores[~symbol])
632
return P, True if scores[P] >= scores[~P] else False
633
634
635
def posit(symbols, clauses):
636
"""
637
Freeman's POSIT version of MOMs
638
Counts the positive x and negative x for each variable x in clauses with minimum size
639
Returns x if f(x) >= f(-x) otherwise -x
640
"""
641
scores = Counter(l for c in min_clauses(clauses) for l in disjuncts(c))
642
P = max(symbols, key=lambda symbol: scores[symbol] + scores[~symbol])
643
return P, True if scores[P] >= scores[~P] else False
644
645
646
def zm(symbols, clauses):
647
"""
648
Zabih and McAllester's version of MOMs
649
Counts the negative occurrences only of each variable x in clauses with minimum size
650
"""
651
scores = Counter(l for c in min_clauses(clauses) for l in disjuncts(c) if l.op == '~')
652
return max(symbols, key=lambda symbol: scores[~symbol]), True
653
654
655
def dlis(symbols, clauses):
656
"""
657
DLIS (Dynamic Largest Individual Sum) heuristic
658
Choose the variable and value that satisfies the maximum number of unsatisfied clauses
659
Like DLCS but we only consider the literal (thus Cp and Cn are individual)
660
"""
661
scores = Counter(l for c in clauses for l in disjuncts(c))
662
P = max(symbols, key=lambda symbol: scores[symbol])
663
return P, True if scores[P] >= scores[~P] else False
664
665
666
def dlcs(symbols, clauses):
667
"""
668
DLCS (Dynamic Largest Combined Sum) heuristic
669
Cp the number of clauses containing literal x
670
Cn the number of clauses containing literal -x
671
Here we select the variable maximizing Cp + Cn
672
Returns x if Cp >= Cn otherwise -x
673
"""
674
scores = Counter(l for c in clauses for l in disjuncts(c))
675
P = max(symbols, key=lambda symbol: scores[symbol] + scores[~symbol])
676
return P, True if scores[P] >= scores[~P] else False
677
678
679
def jw(symbols, clauses):
680
"""
681
Jeroslow-Wang heuristic
682
For each literal compute J(l) = \sum{l in clause c} 2^{-|c|}
683
Return the literal maximizing J
684
"""
685
scores = Counter()
686
for c in clauses:
687
for l in prop_symbols(c):
688
scores[l] += pow(2, -len(c.args))
689
return max(symbols, key=lambda symbol: scores[symbol]), True
690
691
692
def jw2(symbols, clauses):
693
"""
694
Two Sided Jeroslow-Wang heuristic
695
Compute J(l) also counts the negation of l = J(x) + J(-x)
696
Returns x if J(x) >= J(-x) otherwise -x
697
"""
698
scores = Counter()
699
for c in clauses:
700
for l in disjuncts(c):
701
scores[l] += pow(2, -len(c.args))
702
P = max(symbols, key=lambda symbol: scores[symbol] + scores[~symbol])
703
return P, True if scores[P] >= scores[~P] else False
704
705
706
# ______________________________________________________________________________
707
# DPLL-Satisfiable [Figure 7.17]
708
709
710
def dpll_satisfiable(s, branching_heuristic=no_branching_heuristic):
711
"""Check satisfiability of a propositional sentence.
712
This differs from the book code in two ways: (1) it returns a model
713
rather than True when it succeeds; this is more useful. (2) The
714
function find_pure_symbol is passed a list of unknown clauses, rather
715
than a list of all clauses and the model; this is more efficient.
716
>>> dpll_satisfiable(A |'<=>'| B) == {A: True, B: True}
717
True
718
"""
719
return dpll(conjuncts(to_cnf(s)), prop_symbols(s), {}, branching_heuristic)
720
721
722
def dpll(clauses, symbols, model, branching_heuristic=no_branching_heuristic):
723
"""See if the clauses are true in a partial model."""
724
unknown_clauses = [] # clauses with an unknown truth value
725
for c in clauses:
726
val = pl_true(c, model)
727
if val is False:
728
return False
729
if val is None:
730
unknown_clauses.append(c)
731
if not unknown_clauses:
732
return model
733
P, value = find_pure_symbol(symbols, unknown_clauses)
734
if P:
735
return dpll(clauses, remove_all(P, symbols), extend(model, P, value), branching_heuristic)
736
P, value = find_unit_clause(clauses, model)
737
if P:
738
return dpll(clauses, remove_all(P, symbols), extend(model, P, value), branching_heuristic)
739
P, value = branching_heuristic(symbols, unknown_clauses)
740
return (dpll(clauses, remove_all(P, symbols), extend(model, P, value), branching_heuristic) or
741
dpll(clauses, remove_all(P, symbols), extend(model, P, not value), branching_heuristic))
742
743
744
def find_pure_symbol(symbols, clauses):
745
"""Find a symbol and its value if it appears only as a positive literal
746
(or only as a negative) in clauses.
747
>>> find_pure_symbol([A, B, C], [A|~B,~B|~C,C|A])
748
(A, True)
749
"""
750
for s in symbols:
751
found_pos, found_neg = False, False
752
for c in clauses:
753
if not found_pos and s in disjuncts(c):
754
found_pos = True
755
if not found_neg and ~s in disjuncts(c):
756
found_neg = True
757
if found_pos != found_neg:
758
return s, found_pos
759
return None, None
760
761
762
def find_unit_clause(clauses, model):
763
"""Find a forced assignment if possible from a clause with only 1
764
variable not bound in the model.
765
>>> find_unit_clause([A|B|C, B|~C, ~A|~B], {A:True})
766
(B, False)
767
"""
768
for clause in clauses:
769
P, value = unit_clause_assign(clause, model)
770
if P:
771
return P, value
772
return None, None
773
774
775
def unit_clause_assign(clause, model):
776
"""Return a single variable/value pair that makes clause true in
777
the model, if possible.
778
>>> unit_clause_assign(A|B|C, {A:True})
779
(None, None)
780
>>> unit_clause_assign(B|~C, {A:True})
781
(None, None)
782
>>> unit_clause_assign(~A|~B, {A:True})
783
(B, False)
784
"""
785
P, value = None, None
786
for literal in disjuncts(clause):
787
sym, positive = inspect_literal(literal)
788
if sym in model:
789
if model[sym] == positive:
790
return None, None # clause already True
791
elif P:
792
return None, None # more than 1 unbound variable
793
else:
794
P, value = sym, positive
795
return P, value
796
797
798
def inspect_literal(literal):
799
"""The symbol in this literal, and the value it should take to
800
make the literal true.
801
>>> inspect_literal(P)
802
(P, True)
803
>>> inspect_literal(~P)
804
(P, False)
805
"""
806
if literal.op == '~':
807
return literal.args[0], False
808
else:
809
return literal, True
810
811
812
# ______________________________________________________________________________
813
# CDCL - Conflict-Driven Clause Learning with 1UIP Learning Scheme,
814
# 2WL Lazy Data Structure, VSIDS Branching Heuristic & Restarts
815
816
817
def no_restart(conflicts, restarts, queue_lbd, sum_lbd):
818
return False
819
820
821
def luby(conflicts, restarts, queue_lbd, sum_lbd, unit=512):
822
# in the state-of-art tested with unit value 1, 2, 4, 6, 8, 12, 16, 32, 64, 128, 256 and 512
823
def _luby(i):
824
k = 1
825
while True:
826
if i == (1 << k) - 1:
827
return 1 << (k - 1)
828
elif (1 << (k - 1)) <= i < (1 << k) - 1:
829
return _luby(i - (1 << (k - 1)) + 1)
830
k += 1
831
832
return unit * _luby(restarts) == len(queue_lbd)
833
834
835
def glucose(conflicts, restarts, queue_lbd, sum_lbd, x=100, k=0.7):
836
# in the state-of-art tested with (x, k) as (50, 0.8) and (100, 0.7)
837
# if there were at least x conflicts since the last restart, and then the average LBD of the last
838
# x learnt clauses was at least k times higher than the average LBD of all learnt clauses
839
return len(queue_lbd) >= x and sum(queue_lbd) / len(queue_lbd) * k > sum_lbd / conflicts
840
841
842
def cdcl_satisfiable(s, vsids_decay=0.95, restart_strategy=no_restart):
843
"""
844
>>> cdcl_satisfiable(A |'<=>'| B) == {A: True, B: True}
845
True
846
"""
847
clauses = TwoWLClauseDatabase(conjuncts(to_cnf(s)))
848
symbols = prop_symbols(s)
849
scores = Counter()
850
G = nx.DiGraph()
851
model = {}
852
dl = 0
853
conflicts = 0
854
restarts = 1
855
sum_lbd = 0
856
queue_lbd = []
857
while True:
858
conflict = unit_propagation(clauses, symbols, model, G, dl)
859
if conflict:
860
if dl == 0:
861
return False
862
conflicts += 1
863
dl, learn, lbd = conflict_analysis(G, dl)
864
queue_lbd.append(lbd)
865
sum_lbd += lbd
866
backjump(symbols, model, G, dl)
867
clauses.add(learn, model)
868
scores.update(l for l in disjuncts(learn))
869
for symbol in scores:
870
scores[symbol] *= vsids_decay
871
if restart_strategy(conflicts, restarts, queue_lbd, sum_lbd):
872
backjump(symbols, model, G)
873
queue_lbd.clear()
874
restarts += 1
875
else:
876
if not symbols:
877
return model
878
dl += 1
879
assign_decision_literal(symbols, model, scores, G, dl)
880
881
882
def assign_decision_literal(symbols, model, scores, G, dl):
883
P = max(symbols, key=lambda symbol: scores[symbol] + scores[~symbol])
884
value = True if scores[P] >= scores[~P] else False
885
symbols.remove(P)
886
model[P] = value
887
G.add_node(P, val=value, dl=dl)
888
889
890
def unit_propagation(clauses, symbols, model, G, dl):
891
def check(c):
892
if not model or clauses.get_first_watched(c) == clauses.get_second_watched(c):
893
return True
894
w1, _ = inspect_literal(clauses.get_first_watched(c))
895
if w1 in model:
896
return c in (clauses.get_neg_watched(w1) if model[w1] else clauses.get_pos_watched(w1))
897
w2, _ = inspect_literal(clauses.get_second_watched(c))
898
if w2 in model:
899
return c in (clauses.get_neg_watched(w2) if model[w2] else clauses.get_pos_watched(w2))
900
901
def unit_clause(watching):
902
w, p = inspect_literal(watching)
903
G.add_node(w, val=p, dl=dl)
904
G.add_edges_from(zip(prop_symbols(c) - {w}, itertools.cycle([w])), antecedent=c)
905
symbols.remove(w)
906
model[w] = p
907
908
def conflict_clause(c):
909
G.add_edges_from(zip(prop_symbols(c), itertools.cycle('K')), antecedent=c)
910
911
while True:
912
bcp = False
913
for c in filter(check, clauses.get_clauses()):
914
# we need only visit each clause when one of its two watched literals is assigned to 0 because, until
915
# this happens, we can guarantee that there cannot be more than n-2 literals in the clause assigned to 0
916
first_watched = pl_true(clauses.get_first_watched(c), model)
917
second_watched = pl_true(clauses.get_second_watched(c), model)
918
if first_watched is None and clauses.get_first_watched(c) == clauses.get_second_watched(c):
919
unit_clause(clauses.get_first_watched(c))
920
bcp = True
921
break
922
elif first_watched is False and second_watched is not True:
923
if clauses.update_second_watched(c, model):
924
bcp = True
925
else:
926
# if the only literal with a non-zero value is the other watched literal then
927
if second_watched is None: # if it is free, then the clause is a unit clause
928
unit_clause(clauses.get_second_watched(c))
929
bcp = True
930
break
931
else: # else (it is False) the clause is a conflict clause
932
conflict_clause(c)
933
return True
934
elif second_watched is False and first_watched is not True:
935
if clauses.update_first_watched(c, model):
936
bcp = True
937
else:
938
# if the only literal with a non-zero value is the other watched literal then
939
if first_watched is None: # if it is free, then the clause is a unit clause
940
unit_clause(clauses.get_first_watched(c))
941
bcp = True
942
break
943
else: # else (it is False) the clause is a conflict clause
944
conflict_clause(c)
945
return True
946
if not bcp:
947
return False
948
949
950
def conflict_analysis(G, dl):
951
conflict_clause = next(G[p]['K']['antecedent'] for p in G.pred['K'])
952
P = next(node for node in G.nodes() - 'K' if G.nodes[node]['dl'] == dl and G.in_degree(node) == 0)
953
first_uip = nx.immediate_dominators(G, P)['K']
954
G.remove_node('K')
955
conflict_side = nx.descendants(G, first_uip)
956
while True:
957
for l in prop_symbols(conflict_clause).intersection(conflict_side):
958
antecedent = next(G[p][l]['antecedent'] for p in G.pred[l])
959
conflict_clause = pl_binary_resolution(conflict_clause, antecedent)
960
# the literal block distance is calculated by taking the decision levels from variables of all
961
# literals in the clause, and counting how many different decision levels were in this set
962
lbd = [G.nodes[l]['dl'] for l in prop_symbols(conflict_clause)]
963
if lbd.count(dl) == 1 and first_uip in prop_symbols(conflict_clause):
964
return 0 if len(lbd) == 1 else heapq.nlargest(2, lbd)[-1], conflict_clause, len(set(lbd))
965
966
967
def pl_binary_resolution(ci, cj):
968
for di in disjuncts(ci):
969
for dj in disjuncts(cj):
970
if di == ~dj or ~di == dj:
971
return pl_binary_resolution(associate('|', remove_all(di, disjuncts(ci))),
972
associate('|', remove_all(dj, disjuncts(cj))))
973
return associate('|', unique(disjuncts(ci) + disjuncts(cj)))
974
975
976
def backjump(symbols, model, G, dl=0):
977
delete = {node for node in G.nodes() if G.nodes[node]['dl'] > dl}
978
G.remove_nodes_from(delete)
979
for node in delete:
980
del model[node]
981
symbols |= delete
982
983
984
class TwoWLClauseDatabase:
985
986
def __init__(self, clauses):
987
self.__twl = {}
988
self.__watch_list = defaultdict(lambda: [set(), set()])
989
for c in clauses:
990
self.add(c, None)
991
992
def get_clauses(self):
993
return self.__twl.keys()
994
995
def set_first_watched(self, clause, new_watching):
996
if len(clause.args) > 2:
997
self.__twl[clause][0] = new_watching
998
999
def set_second_watched(self, clause, new_watching):
1000
if len(clause.args) > 2:
1001
self.__twl[clause][1] = new_watching
1002
1003
def get_first_watched(self, clause):
1004
if len(clause.args) == 2:
1005
return clause.args[0]
1006
if len(clause.args) > 2:
1007
return self.__twl[clause][0]
1008
return clause
1009
1010
def get_second_watched(self, clause):
1011
if len(clause.args) == 2:
1012
return clause.args[-1]
1013
if len(clause.args) > 2:
1014
return self.__twl[clause][1]
1015
return clause
1016
1017
def get_pos_watched(self, l):
1018
return self.__watch_list[l][0]
1019
1020
def get_neg_watched(self, l):
1021
return self.__watch_list[l][1]
1022
1023
def add(self, clause, model):
1024
self.__twl[clause] = self.__assign_watching_literals(clause, model)
1025
w1, p1 = inspect_literal(self.get_first_watched(clause))
1026
w2, p2 = inspect_literal(self.get_second_watched(clause))
1027
self.__watch_list[w1][0].add(clause) if p1 else self.__watch_list[w1][1].add(clause)
1028
if w1 != w2:
1029
self.__watch_list[w2][0].add(clause) if p2 else self.__watch_list[w2][1].add(clause)
1030
1031
def remove(self, clause):
1032
w1, p1 = inspect_literal(self.get_first_watched(clause))
1033
w2, p2 = inspect_literal(self.get_second_watched(clause))
1034
del self.__twl[clause]
1035
self.__watch_list[w1][0].discard(clause) if p1 else self.__watch_list[w1][1].discard(clause)
1036
if w1 != w2:
1037
self.__watch_list[w2][0].discard(clause) if p2 else self.__watch_list[w2][1].discard(clause)
1038
1039
def update_first_watched(self, clause, model):
1040
# if a non-zero literal different from the other watched literal is found
1041
found, new_watching = self.__find_new_watching_literal(clause, self.get_first_watched(clause), model)
1042
if found: # then it will replace the watched literal
1043
w, p = inspect_literal(self.get_second_watched(clause))
1044
self.__watch_list[w][0].remove(clause) if p else self.__watch_list[w][1].remove(clause)
1045
self.set_second_watched(clause, new_watching)
1046
w, p = inspect_literal(new_watching)
1047
self.__watch_list[w][0].add(clause) if p else self.__watch_list[w][1].add(clause)
1048
return True
1049
1050
def update_second_watched(self, clause, model):
1051
# if a non-zero literal different from the other watched literal is found
1052
found, new_watching = self.__find_new_watching_literal(clause, self.get_second_watched(clause), model)
1053
if found: # then it will replace the watched literal
1054
w, p = inspect_literal(self.get_first_watched(clause))
1055
self.__watch_list[w][0].remove(clause) if p else self.__watch_list[w][1].remove(clause)
1056
self.set_first_watched(clause, new_watching)
1057
w, p = inspect_literal(new_watching)
1058
self.__watch_list[w][0].add(clause) if p else self.__watch_list[w][1].add(clause)
1059
return True
1060
1061
def __find_new_watching_literal(self, clause, other_watched, model):
1062
# if a non-zero literal different from the other watched literal is found
1063
if len(clause.args) > 2:
1064
for l in disjuncts(clause):
1065
if l != other_watched and pl_true(l, model) is not False:
1066
# then it is returned
1067
return True, l
1068
return False, None
1069
1070
def __assign_watching_literals(self, clause, model=None):
1071
if len(clause.args) > 2:
1072
if model is None or not model:
1073
return [clause.args[0], clause.args[-1]]
1074
else:
1075
return [next(l for l in disjuncts(clause) if pl_true(l, model) is None),
1076
next(l for l in disjuncts(clause) if pl_true(l, model) is False)]
1077
1078
1079
# ______________________________________________________________________________
1080
# Walk-SAT [Figure 7.18]
1081
1082
1083
def WalkSAT(clauses, p=0.5, max_flips=10000):
1084
"""Checks for satisfiability of all clauses by randomly flipping values of variables
1085
>>> WalkSAT([A & ~A], 0.5, 100) is None
1086
True
1087
"""
1088
# Set of all symbols in all clauses
1089
symbols = {sym for clause in clauses for sym in prop_symbols(clause)}
1090
# model is a random assignment of true/false to the symbols in clauses
1091
model = {s: random.choice([True, False]) for s in symbols}
1092
for i in range(max_flips):
1093
satisfied, unsatisfied = [], []
1094
for clause in clauses:
1095
(satisfied if pl_true(clause, model) else unsatisfied).append(clause)
1096
if not unsatisfied: # if model satisfies all the clauses
1097
return model
1098
clause = random.choice(unsatisfied)
1099
if probability(p):
1100
sym = random.choice(list(prop_symbols(clause)))
1101
else:
1102
# Flip the symbol in clause that maximizes number of sat. clauses
1103
def sat_count(sym):
1104
# Return the the number of clauses satisfied after flipping the symbol.
1105
model[sym] = not model[sym]
1106
count = len([clause for clause in clauses if pl_true(clause, model)])
1107
model[sym] = not model[sym]
1108
return count
1109
1110
sym = max(prop_symbols(clause), key=sat_count)
1111
model[sym] = not model[sym]
1112
# If no solution is found within the flip limit, we return failure
1113
return None
1114
1115
1116
# ______________________________________________________________________________
1117
# Map Coloring SAT Problems
1118
1119
1120
def MapColoringSAT(colors, neighbors):
1121
"""Make a SAT for the problem of coloring a map with different colors
1122
for any two adjacent regions. Arguments are a list of colors, and a
1123
dict of {region: [neighbor,...]} entries. This dict may also be
1124
specified as a string of the form defined by parse_neighbors."""
1125
if isinstance(neighbors, str):
1126
neighbors = parse_neighbors(neighbors)
1127
colors = UniversalDict(colors)
1128
clauses = []
1129
for state in neighbors.keys():
1130
clause = [expr(state + '_' + c) for c in colors[state]]
1131
clauses.append(clause)
1132
for t in itertools.combinations(clause, 2):
1133
clauses.append([~t[0], ~t[1]])
1134
visited = set()
1135
adj = set(neighbors[state]) - visited
1136
visited.add(state)
1137
for n_state in adj:
1138
for col in colors[n_state]:
1139
clauses.append([expr('~' + state + '_' + col), expr('~' + n_state + '_' + col)])
1140
return associate('&', map(lambda c: associate('|', c), clauses))
1141
1142
1143
australia_sat = MapColoringSAT(list('RGB'), """SA: WA NT Q NSW V; NT: WA Q; NSW: Q V; T: """)
1144
1145
france_sat = MapColoringSAT(list('RGBY'),
1146
"""AL: LO FC; AQ: MP LI PC; AU: LI CE BO RA LR MP; BO: CE IF CA FC RA
1147
AU; BR: NB PL; CA: IF PI LO FC BO; CE: PL NB NH IF BO AU LI PC; FC: BO
1148
CA LO AL RA; IF: NH PI CA BO CE; LI: PC CE AU MP AQ; LO: CA AL FC; LR:
1149
MP AU RA PA; MP: AQ LI AU LR; NB: NH CE PL BR; NH: PI IF CE NB; NO:
1150
PI; PA: LR RA; PC: PL CE LI AQ; PI: NH NO CA IF; PL: BR NB CE PC; RA:
1151
AU BO FC PA LR""")
1152
1153
usa_sat = MapColoringSAT(list('RGBY'),
1154
"""WA: OR ID; OR: ID NV CA; CA: NV AZ; NV: ID UT AZ; ID: MT WY UT;
1155
UT: WY CO AZ; MT: ND SD WY; WY: SD NE CO; CO: NE KA OK NM; NM: OK TX AZ;
1156
ND: MN SD; SD: MN IA NE; NE: IA MO KA; KA: MO OK; OK: MO AR TX;
1157
TX: AR LA; MN: WI IA; IA: WI IL MO; MO: IL KY TN AR; AR: MS TN LA;
1158
LA: MS; WI: MI IL; IL: IN KY; IN: OH KY; MS: TN AL; AL: TN GA FL;
1159
MI: OH IN; OH: PA WV KY; KY: WV VA TN; TN: VA NC GA; GA: NC SC FL;
1160
PA: NY NJ DE MD WV; WV: MD VA; VA: MD DC NC; NC: SC; NY: VT MA CT NJ;
1161
NJ: DE; DE: MD; MD: DC; VT: NH MA; MA: NH RI CT; CT: RI; ME: NH;
1162
HI: ; AK: """)
1163
1164
1165
# ______________________________________________________________________________
1166
1167
1168
# Expr functions for WumpusKB and HybridWumpusAgent
1169
1170
def facing_east(time):
1171
return Expr('FacingEast', time)
1172
1173
1174
def facing_west(time):
1175
return Expr('FacingWest', time)
1176
1177
1178
def facing_north(time):
1179
return Expr('FacingNorth', time)
1180
1181
1182
def facing_south(time):
1183
return Expr('FacingSouth', time)
1184
1185
1186
def wumpus(x, y):
1187
return Expr('W', x, y)
1188
1189
1190
def pit(x, y):
1191
return Expr('P', x, y)
1192
1193
1194
def breeze(x, y):
1195
return Expr('B', x, y)
1196
1197
1198
def stench(x, y):
1199
return Expr('S', x, y)
1200
1201
1202
def wumpus_alive(time):
1203
return Expr('WumpusAlive', time)
1204
1205
1206
def have_arrow(time):
1207
return Expr('HaveArrow', time)
1208
1209
1210
def percept_stench(time):
1211
return Expr('Stench', time)
1212
1213
1214
def percept_breeze(time):
1215
return Expr('Breeze', time)
1216
1217
1218
def percept_glitter(time):
1219
return Expr('Glitter', time)
1220
1221
1222
def percept_bump(time):
1223
return Expr('Bump', time)
1224
1225
1226
def percept_scream(time):
1227
return Expr('Scream', time)
1228
1229
1230
def move_forward(time):
1231
return Expr('Forward', time)
1232
1233
1234
def shoot(time):
1235
return Expr('Shoot', time)
1236
1237
1238
def turn_left(time):
1239
return Expr('TurnLeft', time)
1240
1241
1242
def turn_right(time):
1243
return Expr('TurnRight', time)
1244
1245
1246
def ok_to_move(x, y, time):
1247
return Expr('OK', x, y, time)
1248
1249
1250
def location(x, y, time=None):
1251
if time is None:
1252
return Expr('L', x, y)
1253
else:
1254
return Expr('L', x, y, time)
1255
1256
1257
# Symbols
1258
1259
def implies(lhs, rhs):
1260
return Expr('==>', lhs, rhs)
1261
1262
1263
def equiv(lhs, rhs):
1264
return Expr('<=>', lhs, rhs)
1265
1266
1267
# Helper Function
1268
1269
def new_disjunction(sentences):
1270
t = sentences[0]
1271
for i in range(1, len(sentences)):
1272
t |= sentences[i]
1273
return t
1274
1275
1276
# ______________________________________________________________________________
1277
1278
1279
class WumpusKB(PropKB):
1280
"""
1281
Create a Knowledge Base that contains the a temporal "Wumpus physics" and temporal rules with time zero.
1282
"""
1283
1284
def __init__(self, dimrow):
1285
super().__init__()
1286
self.dimrow = dimrow
1287
self.tell(~wumpus(1, 1))
1288
self.tell(~pit(1, 1))
1289
1290
for y in range(1, dimrow + 1):
1291
for x in range(1, dimrow + 1):
1292
1293
pits_in = list()
1294
wumpus_in = list()
1295
1296
if x > 1: # West room exists
1297
pits_in.append(pit(x - 1, y))
1298
wumpus_in.append(wumpus(x - 1, y))
1299
1300
if y < dimrow: # North room exists
1301
pits_in.append(pit(x, y + 1))
1302
wumpus_in.append(wumpus(x, y + 1))
1303
1304
if x < dimrow: # East room exists
1305
pits_in.append(pit(x + 1, y))
1306
wumpus_in.append(wumpus(x + 1, y))
1307
1308
if y > 1: # South room exists
1309
pits_in.append(pit(x, y - 1))
1310
wumpus_in.append(wumpus(x, y - 1))
1311
1312
self.tell(equiv(breeze(x, y), new_disjunction(pits_in)))
1313
self.tell(equiv(stench(x, y), new_disjunction(wumpus_in)))
1314
1315
# Rule that describes existence of at least one Wumpus
1316
wumpus_at_least = list()
1317
for x in range(1, dimrow + 1):
1318
for y in range(1, dimrow + 1):
1319
wumpus_at_least.append(wumpus(x, y))
1320
1321
self.tell(new_disjunction(wumpus_at_least))
1322
1323
# Rule that describes existence of at most one Wumpus
1324
for i in range(1, dimrow + 1):
1325
for j in range(1, dimrow + 1):
1326
for u in range(1, dimrow + 1):
1327
for v in range(1, dimrow + 1):
1328
if i != u or j != v:
1329
self.tell(~wumpus(i, j) | ~wumpus(u, v))
1330
1331
# Temporal rules at time zero
1332
self.tell(location(1, 1, 0))
1333
for i in range(1, dimrow + 1):
1334
for j in range(1, dimrow + 1):
1335
self.tell(implies(location(i, j, 0), equiv(percept_breeze(0), breeze(i, j))))
1336
self.tell(implies(location(i, j, 0), equiv(percept_stench(0), stench(i, j))))
1337
if i != 1 or j != 1:
1338
self.tell(~location(i, j, 0))
1339
1340
self.tell(wumpus_alive(0))
1341
self.tell(have_arrow(0))
1342
self.tell(facing_east(0))
1343
self.tell(~facing_north(0))
1344
self.tell(~facing_south(0))
1345
self.tell(~facing_west(0))
1346
1347
def make_action_sentence(self, action, time):
1348
actions = [move_forward(time), shoot(time), turn_left(time), turn_right(time)]
1349
1350
for a in actions:
1351
if action is a:
1352
self.tell(action)
1353
else:
1354
self.tell(~a)
1355
1356
def make_percept_sentence(self, percept, time):
1357
# Glitter, Bump, Stench, Breeze, Scream
1358
flags = [0, 0, 0, 0, 0]
1359
1360
# Things perceived
1361
if isinstance(percept, Glitter):
1362
flags[0] = 1
1363
self.tell(percept_glitter(time))
1364
elif isinstance(percept, Bump):
1365
flags[1] = 1
1366
self.tell(percept_bump(time))
1367
elif isinstance(percept, Stench):
1368
flags[2] = 1
1369
self.tell(percept_stench(time))
1370
elif isinstance(percept, Breeze):
1371
flags[3] = 1
1372
self.tell(percept_breeze(time))
1373
elif isinstance(percept, Scream):
1374
flags[4] = 1
1375
self.tell(percept_scream(time))
1376
1377
# Things not perceived
1378
for i in range(len(flags)):
1379
if flags[i] == 0:
1380
if i == 0:
1381
self.tell(~percept_glitter(time))
1382
elif i == 1:
1383
self.tell(~percept_bump(time))
1384
elif i == 2:
1385
self.tell(~percept_stench(time))
1386
elif i == 3:
1387
self.tell(~percept_breeze(time))
1388
elif i == 4:
1389
self.tell(~percept_scream(time))
1390
1391
def add_temporal_sentences(self, time):
1392
if time == 0:
1393
return
1394
t = time - 1
1395
1396
# current location rules
1397
for i in range(1, self.dimrow + 1):
1398
for j in range(1, self.dimrow + 1):
1399
self.tell(implies(location(i, j, time), equiv(percept_breeze(time), breeze(i, j))))
1400
self.tell(implies(location(i, j, time), equiv(percept_stench(time), stench(i, j))))
1401
s = list()
1402
s.append(equiv(location(i, j, time), location(i, j, time) & ~move_forward(time) | percept_bump(time)))
1403
if i != 1:
1404
s.append(location(i - 1, j, t) & facing_east(t) & move_forward(t))
1405
if i != self.dimrow:
1406
s.append(location(i + 1, j, t) & facing_west(t) & move_forward(t))
1407
if j != 1:
1408
s.append(location(i, j - 1, t) & facing_north(t) & move_forward(t))
1409
if j != self.dimrow:
1410
s.append(location(i, j + 1, t) & facing_south(t) & move_forward(t))
1411
1412
# add sentence about location i,j
1413
self.tell(new_disjunction(s))
1414
1415
# add sentence about safety of location i,j
1416
self.tell(equiv(ok_to_move(i, j, time), ~pit(i, j) & ~wumpus(i, j) & wumpus_alive(time)))
1417
1418
# Rules about current orientation
1419
1420
a = facing_north(t) & turn_right(t)
1421
b = facing_south(t) & turn_left(t)
1422
c = facing_east(t) & ~turn_left(t) & ~turn_right(t)
1423
s = equiv(facing_east(time), a | b | c)
1424
self.tell(s)
1425
1426
a = facing_north(t) & turn_left(t)
1427
b = facing_south(t) & turn_right(t)
1428
c = facing_west(t) & ~turn_left(t) & ~turn_right(t)
1429
s = equiv(facing_west(time), a | b | c)
1430
self.tell(s)
1431
1432
a = facing_east(t) & turn_left(t)
1433
b = facing_west(t) & turn_right(t)
1434
c = facing_north(t) & ~turn_left(t) & ~turn_right(t)
1435
s = equiv(facing_north(time), a | b | c)
1436
self.tell(s)
1437
1438
a = facing_west(t) & turn_left(t)
1439
b = facing_east(t) & turn_right(t)
1440
c = facing_south(t) & ~turn_left(t) & ~turn_right(t)
1441
s = equiv(facing_south(time), a | b | c)
1442
self.tell(s)
1443
1444
# Rules about last action
1445
self.tell(equiv(move_forward(t), ~turn_right(t) & ~turn_left(t)))
1446
1447
# Rule about the arrow
1448
self.tell(equiv(have_arrow(time), have_arrow(t) & ~shoot(t)))
1449
1450
# Rule about Wumpus (dead or alive)
1451
self.tell(equiv(wumpus_alive(time), wumpus_alive(t) & ~percept_scream(time)))
1452
1453
def ask_if_true(self, query):
1454
return pl_resolution(self, query)
1455
1456
1457
# ______________________________________________________________________________
1458
1459
1460
class WumpusPosition:
1461
def __init__(self, x, y, orientation):
1462
self.X = x
1463
self.Y = y
1464
self.orientation = orientation
1465
1466
def get_location(self):
1467
return self.X, self.Y
1468
1469
def set_location(self, x, y):
1470
self.X = x
1471
self.Y = y
1472
1473
def get_orientation(self):
1474
return self.orientation
1475
1476
def set_orientation(self, orientation):
1477
self.orientation = orientation
1478
1479
def __eq__(self, other):
1480
if other.get_location() == self.get_location() and other.get_orientation() == self.get_orientation():
1481
return True
1482
else:
1483
return False
1484
1485
1486
# ______________________________________________________________________________
1487
1488
1489
class HybridWumpusAgent(Agent):
1490
"""
1491
[Figure 7.20]
1492
An agent for the wumpus world that does logical inference.
1493
"""
1494
1495
def __init__(self, dimentions):
1496
self.dimrow = dimentions
1497
self.kb = WumpusKB(self.dimrow)
1498
self.t = 0
1499
self.plan = list()
1500
self.current_position = WumpusPosition(1, 1, 'UP')
1501
super().__init__(self.execute)
1502
1503
def execute(self, percept):
1504
self.kb.make_percept_sentence(percept, self.t)
1505
self.kb.add_temporal_sentences(self.t)
1506
1507
temp = list()
1508
1509
for i in range(1, self.dimrow + 1):
1510
for j in range(1, self.dimrow + 1):
1511
if self.kb.ask_if_true(location(i, j, self.t)):
1512
temp.append(i)
1513
temp.append(j)
1514
1515
if self.kb.ask_if_true(facing_north(self.t)):
1516
self.current_position = WumpusPosition(temp[0], temp[1], 'UP')
1517
elif self.kb.ask_if_true(facing_south(self.t)):
1518
self.current_position = WumpusPosition(temp[0], temp[1], 'DOWN')
1519
elif self.kb.ask_if_true(facing_west(self.t)):
1520
self.current_position = WumpusPosition(temp[0], temp[1], 'LEFT')
1521
elif self.kb.ask_if_true(facing_east(self.t)):
1522
self.current_position = WumpusPosition(temp[0], temp[1], 'RIGHT')
1523
1524
safe_points = list()
1525
for i in range(1, self.dimrow + 1):
1526
for j in range(1, self.dimrow + 1):
1527
if self.kb.ask_if_true(ok_to_move(i, j, self.t)):
1528
safe_points.append([i, j])
1529
1530
if self.kb.ask_if_true(percept_glitter(self.t)):
1531
goals = list()
1532
goals.append([1, 1])
1533
self.plan.append('Grab')
1534
actions = self.plan_route(self.current_position, goals, safe_points)
1535
self.plan.extend(actions)
1536
self.plan.append('Climb')
1537
1538
if len(self.plan) == 0:
1539
unvisited = list()
1540
for i in range(1, self.dimrow + 1):
1541
for j in range(1, self.dimrow + 1):
1542
for k in range(self.t):
1543
if self.kb.ask_if_true(location(i, j, k)):
1544
unvisited.append([i, j])
1545
unvisited_and_safe = list()
1546
for u in unvisited:
1547
for s in safe_points:
1548
if u not in unvisited_and_safe and s == u:
1549
unvisited_and_safe.append(u)
1550
1551
temp = self.plan_route(self.current_position, unvisited_and_safe, safe_points)
1552
self.plan.extend(temp)
1553
1554
if len(self.plan) == 0 and self.kb.ask_if_true(have_arrow(self.t)):
1555
possible_wumpus = list()
1556
for i in range(1, self.dimrow + 1):
1557
for j in range(1, self.dimrow + 1):
1558
if not self.kb.ask_if_true(wumpus(i, j)):
1559
possible_wumpus.append([i, j])
1560
1561
temp = self.plan_shot(self.current_position, possible_wumpus, safe_points)
1562
self.plan.extend(temp)
1563
1564
if len(self.plan) == 0:
1565
not_unsafe = list()
1566
for i in range(1, self.dimrow + 1):
1567
for j in range(1, self.dimrow + 1):
1568
if not self.kb.ask_if_true(ok_to_move(i, j, self.t)):
1569
not_unsafe.append([i, j])
1570
temp = self.plan_route(self.current_position, not_unsafe, safe_points)
1571
self.plan.extend(temp)
1572
1573
if len(self.plan) == 0:
1574
start = list()
1575
start.append([1, 1])
1576
temp = self.plan_route(self.current_position, start, safe_points)
1577
self.plan.extend(temp)
1578
self.plan.append('Climb')
1579
1580
action = self.plan[0]
1581
self.plan = self.plan[1:]
1582
self.kb.make_action_sentence(action, self.t)
1583
self.t += 1
1584
1585
return action
1586
1587
def plan_route(self, current, goals, allowed):
1588
problem = PlanRoute(current, goals, allowed, self.dimrow)
1589
return astar_search(problem).solution()
1590
1591
def plan_shot(self, current, goals, allowed):
1592
shooting_positions = set()
1593
1594
for loc in goals:
1595
x = loc[0]
1596
y = loc[1]
1597
for i in range(1, self.dimrow + 1):
1598
if i < x:
1599
shooting_positions.add(WumpusPosition(i, y, 'EAST'))
1600
if i > x:
1601
shooting_positions.add(WumpusPosition(i, y, 'WEST'))
1602
if i < y:
1603
shooting_positions.add(WumpusPosition(x, i, 'NORTH'))
1604
if i > y:
1605
shooting_positions.add(WumpusPosition(x, i, 'SOUTH'))
1606
1607
# Can't have a shooting position from any of the rooms the Wumpus could reside
1608
orientations = ['EAST', 'WEST', 'NORTH', 'SOUTH']
1609
for loc in goals:
1610
for orientation in orientations:
1611
shooting_positions.remove(WumpusPosition(loc[0], loc[1], orientation))
1612
1613
actions = list()
1614
actions.extend(self.plan_route(current, shooting_positions, allowed))
1615
actions.append('Shoot')
1616
return actions
1617
1618
1619
# ______________________________________________________________________________
1620
1621
1622
def SAT_plan(init, transition, goal, t_max, SAT_solver=cdcl_satisfiable):
1623
"""
1624
[Figure 7.22]
1625
Converts a planning problem to Satisfaction problem by translating it to a cnf sentence.
1626
>>> transition = {'A': {'Left': 'A', 'Right': 'B'}, 'B': {'Left': 'A', 'Right': 'C'}, 'C': {'Left': 'B', 'Right': 'C'}}
1627
>>> SAT_plan('A', transition, 'C', 1) is None
1628
True
1629
"""
1630
1631
# Functions used by SAT_plan
1632
def translate_to_SAT(init, transition, goal, time):
1633
clauses = []
1634
states = [state for state in transition]
1635
1636
# Symbol claiming state s at time t
1637
state_counter = itertools.count()
1638
for s in states:
1639
for t in range(time + 1):
1640
state_sym[s, t] = Expr('S_{}'.format(next(state_counter)))
1641
1642
# Add initial state axiom
1643
clauses.append(state_sym[init, 0])
1644
1645
# Add goal state axiom
1646
clauses.append(state_sym[first(clause[0] for clause in state_sym
1647
if set(conjuncts(clause[0])).issuperset(conjuncts(goal))), time]) \
1648
if isinstance(goal, Expr) else clauses.append(state_sym[goal, time])
1649
1650
# All possible transitions
1651
transition_counter = itertools.count()
1652
for s in states:
1653
for action in transition[s]:
1654
s_ = transition[s][action]
1655
for t in range(time):
1656
# Action 'action' taken from state 's' at time 't' to reach 's_'
1657
action_sym[s, action, t] = Expr('T_{}'.format(next(transition_counter)))
1658
1659
# Change the state from s to s_
1660
clauses.append(action_sym[s, action, t] | '==>' | state_sym[s, t])
1661
clauses.append(action_sym[s, action, t] | '==>' | state_sym[s_, t + 1])
1662
1663
# Allow only one state at any time
1664
for t in range(time + 1):
1665
# must be a state at any time
1666
clauses.append(associate('|', [state_sym[s, t] for s in states]))
1667
1668
for s in states:
1669
for s_ in states[states.index(s) + 1:]:
1670
# for each pair of states s, s_ only one is possible at time t
1671
clauses.append((~state_sym[s, t]) | (~state_sym[s_, t]))
1672
1673
# Restrict to one transition per timestep
1674
for t in range(time):
1675
# list of possible transitions at time t
1676
transitions_t = [tr for tr in action_sym if tr[2] == t]
1677
1678
# make sure at least one of the transitions happens
1679
clauses.append(associate('|', [action_sym[tr] for tr in transitions_t]))
1680
1681
for tr in transitions_t:
1682
for tr_ in transitions_t[transitions_t.index(tr) + 1:]:
1683
# there cannot be two transitions tr and tr_ at time t
1684
clauses.append(~action_sym[tr] | ~action_sym[tr_])
1685
1686
# Combine the clauses to form the cnf
1687
return associate('&', clauses)
1688
1689
def extract_solution(model):
1690
true_transitions = [t for t in action_sym if model[action_sym[t]]]
1691
# Sort transitions based on time, which is the 3rd element of the tuple
1692
true_transitions.sort(key=lambda x: x[2])
1693
return [action for s, action, time in true_transitions]
1694
1695
# Body of SAT_plan algorithm
1696
for t in range(t_max + 1):
1697
# dictionaries to help extract the solution from model
1698
state_sym = {}
1699
action_sym = {}
1700
1701
cnf = translate_to_SAT(init, transition, goal, t)
1702
model = SAT_solver(cnf)
1703
if model is not False:
1704
return extract_solution(model)
1705
return None
1706
1707
1708
# ______________________________________________________________________________
1709
1710
1711
def unify(x, y, s={}):
1712
"""
1713
[Figure 9.1]
1714
Unify expressions x,y with substitution s; return a substitution that
1715
would make x,y equal, or None if x,y can not unify. x and y can be
1716
variables (e.g. Expr('x')), constants, lists, or Exprs.
1717
>>> unify(x, 3, {})
1718
{x: 3}
1719
"""
1720
if s is None:
1721
return None
1722
elif x == y:
1723
return s
1724
elif is_variable(x):
1725
return unify_var(x, y, s)
1726
elif is_variable(y):
1727
return unify_var(y, x, s)
1728
elif isinstance(x, Expr) and isinstance(y, Expr):
1729
return unify(x.args, y.args, unify(x.op, y.op, s))
1730
elif isinstance(x, str) or isinstance(y, str):
1731
return None
1732
elif issequence(x) and issequence(y) and len(x) == len(y):
1733
if not x:
1734
return s
1735
return unify(x[1:], y[1:], unify(x[0], y[0], s))
1736
else:
1737
return None
1738
1739
1740
def is_variable(x):
1741
"""A variable is an Expr with no args and a lowercase symbol as the op."""
1742
return isinstance(x, Expr) and not x.args and x.op[0].islower()
1743
1744
1745
def unify_var(var, x, s):
1746
if var in s:
1747
return unify(s[var], x, s)
1748
elif x in s:
1749
return unify(var, s[x], s)
1750
elif occur_check(var, x, s):
1751
return None
1752
else:
1753
new_s = extend(s, var, x)
1754
cascade_substitution(new_s)
1755
return new_s
1756
1757
1758
def occur_check(var, x, s):
1759
"""Return true if variable var occurs anywhere in x
1760
(or in subst(s, x), if s has a binding for x)."""
1761
if var == x:
1762
return True
1763
elif is_variable(x) and x in s:
1764
return occur_check(var, s[x], s)
1765
elif isinstance(x, Expr):
1766
return (occur_check(var, x.op, s) or
1767
occur_check(var, x.args, s))
1768
elif isinstance(x, (list, tuple)):
1769
return first(e for e in x if occur_check(var, e, s))
1770
else:
1771
return False
1772
1773
1774
def subst(s, x):
1775
"""Substitute the substitution s into the expression x.
1776
>>> subst({x: 42, y:0}, F(x) + y)
1777
(F(42) + 0)
1778
"""
1779
if isinstance(x, list):
1780
return [subst(s, xi) for xi in x]
1781
elif isinstance(x, tuple):
1782
return tuple([subst(s, xi) for xi in x])
1783
elif not isinstance(x, Expr):
1784
return x
1785
elif is_var_symbol(x.op):
1786
return s.get(x, x)
1787
else:
1788
return Expr(x.op, *[subst(s, arg) for arg in x.args])
1789
1790
1791
def cascade_substitution(s):
1792
"""This method allows to return a correct unifier in normal form
1793
and perform a cascade substitution to s.
1794
For every mapping in s perform a cascade substitution on s.get(x)
1795
and if it is replaced with a function ensure that all the function
1796
terms are correct updates by passing over them again.
1797
>>> s = {x: y, y: G(z)}
1798
>>> cascade_substitution(s)
1799
>>> s == {x: G(z), y: G(z)}
1800
True
1801
"""
1802
1803
for x in s:
1804
s[x] = subst(s, s.get(x))
1805
if isinstance(s.get(x), Expr) and not is_variable(s.get(x)):
1806
# Ensure Function Terms are correct updates by passing over them again
1807
s[x] = subst(s, s.get(x))
1808
1809
1810
def unify_mm(x, y, s={}):
1811
"""Unify expressions x,y with substitution s using an efficient rule-based
1812
unification algorithm by Martelli & Montanari; return a substitution that
1813
would make x,y equal, or None if x,y can not unify. x and y can be
1814
variables (e.g. Expr('x')), constants, lists, or Exprs.
1815
>>> unify_mm(x, 3, {})
1816
{x: 3}
1817
"""
1818
1819
set_eq = extend(s, x, y)
1820
s = set_eq.copy()
1821
while True:
1822
trans = 0
1823
for x, y in set_eq.items():
1824
if x == y:
1825
# if x = y this mapping is deleted (rule b)
1826
del s[x]
1827
elif not is_variable(x) and is_variable(y):
1828
# if x is not a variable and y is a variable, rewrite it as y = x in s (rule a)
1829
if s.get(y, None) is None:
1830
s[y] = x
1831
del s[x]
1832
else:
1833
# if a mapping already exist for variable y then apply
1834
# variable elimination (there is a chance to apply rule d)
1835
s[x] = vars_elimination(y, s)
1836
elif not is_variable(x) and not is_variable(y):
1837
# in which case x and y are not variables, if the two root function symbols
1838
# are different, stop with failure, else apply term reduction (rule c)
1839
if x.op is y.op and len(x.args) == len(y.args):
1840
term_reduction(x, y, s)
1841
del s[x]
1842
else:
1843
return None
1844
elif isinstance(y, Expr):
1845
# in which case x is a variable and y is a function or a variable (e.g. F(z) or y),
1846
# if y is a function, we must check if x occurs in y, then stop with failure, else
1847
# try to apply variable elimination to y (rule d)
1848
if occur_check(x, y, s):
1849
return None
1850
s[x] = vars_elimination(y, s)
1851
if y == s.get(x):
1852
trans += 1
1853
else:
1854
trans += 1
1855
if trans == len(set_eq):
1856
# if no transformation has been applied, stop with success
1857
return s
1858
set_eq = s.copy()
1859
1860
1861
def term_reduction(x, y, s):
1862
"""Apply term reduction to x and y if both are functions and the two root function
1863
symbols are equals (e.g. F(x1, x2, ..., xn) and F(x1', x2', ..., xn')) by returning
1864
a new mapping obtained by replacing x: y with {x1: x1', x2: x2', ..., xn: xn'}
1865
"""
1866
for i in range(len(x.args)):
1867
if x.args[i] in s:
1868
s[s.get(x.args[i])] = y.args[i]
1869
else:
1870
s[x.args[i]] = y.args[i]
1871
1872
1873
def vars_elimination(x, s):
1874
"""Apply variable elimination to x: if x is a variable and occurs in s, return
1875
the term mapped by x, else if x is a function recursively applies variable
1876
elimination to each term of the function."""
1877
if not isinstance(x, Expr):
1878
return x
1879
if is_variable(x):
1880
return s.get(x, x)
1881
return Expr(x.op, *[vars_elimination(arg, s) for arg in x.args])
1882
1883
1884
def standardize_variables(sentence, dic=None):
1885
"""Replace all the variables in sentence with new variables."""
1886
if dic is None:
1887
dic = {}
1888
if not isinstance(sentence, Expr):
1889
return sentence
1890
elif is_var_symbol(sentence.op):
1891
if sentence in dic:
1892
return dic[sentence]
1893
else:
1894
v = Expr('v_{}'.format(next(standardize_variables.counter)))
1895
dic[sentence] = v
1896
return v
1897
else:
1898
return Expr(sentence.op, *[standardize_variables(a, dic) for a in sentence.args])
1899
1900
1901
standardize_variables.counter = itertools.count()
1902
1903
1904
# ______________________________________________________________________________
1905
1906
1907
def parse_clauses_from_dimacs(dimacs_cnf):
1908
"""Converts a string into CNF clauses according to the DIMACS format used in SAT competitions"""
1909
return map(lambda c: associate('|', c),
1910
map(lambda c: [expr('~X' + str(abs(l))) if l < 0 else expr('X' + str(l)) for l in c],
1911
map(lambda line: map(int, line.split()),
1912
filter(None, ' '.join(
1913
filter(lambda line: line[0] not in ('c', 'p'),
1914
filter(None, dimacs_cnf.strip().replace('\t', ' ').split('\n')))).split(' 0')))))
1915
1916
1917
# ______________________________________________________________________________
1918
1919
1920
class FolKB(KB):
1921
"""A knowledge base consisting of first-order definite clauses.
1922
>>> kb0 = FolKB([expr('Farmer(Mac)'), expr('Rabbit(Pete)'),
1923
... expr('(Rabbit(r) & Farmer(f)) ==> Hates(f, r)')])
1924
>>> kb0.tell(expr('Rabbit(Flopsie)'))
1925
>>> kb0.retract(expr('Rabbit(Pete)'))
1926
>>> kb0.ask(expr('Hates(Mac, x)'))[x]
1927
Flopsie
1928
>>> kb0.ask(expr('Wife(Pete, x)'))
1929
False
1930
"""
1931
1932
def __init__(self, clauses=None):
1933
super().__init__()
1934
self.clauses = [] # inefficient: no indexing
1935
if clauses:
1936
for clause in clauses:
1937
self.tell(clause)
1938
1939
def tell(self, sentence):
1940
if is_definite_clause(sentence):
1941
self.clauses.append(sentence)
1942
else:
1943
raise Exception('Not a definite clause: {}'.format(sentence))
1944
1945
def ask_generator(self, query):
1946
return fol_bc_ask(self, query)
1947
1948
def retract(self, sentence):
1949
self.clauses.remove(sentence)
1950
1951
def fetch_rules_for_goal(self, goal):
1952
return self.clauses
1953
1954
1955
def fol_fc_ask(kb, alpha):
1956
"""
1957
[Figure 9.3]
1958
A simple forward-chaining algorithm.
1959
"""
1960
# TODO: improve efficiency
1961
kb_consts = list({c for clause in kb.clauses for c in constant_symbols(clause)})
1962
1963
def enum_subst(p):
1964
query_vars = list({v for clause in p for v in variables(clause)})
1965
for assignment_list in itertools.product(kb_consts, repeat=len(query_vars)):
1966
theta = {x: y for x, y in zip(query_vars, assignment_list)}
1967
yield theta
1968
1969
# check if we can answer without new inferences
1970
for q in kb.clauses:
1971
phi = unify_mm(q, alpha)
1972
if phi is not None:
1973
yield phi
1974
1975
while True:
1976
new = []
1977
for rule in kb.clauses:
1978
p, q = parse_definite_clause(rule)
1979
for theta in enum_subst(p):
1980
if set(subst(theta, p)).issubset(set(kb.clauses)):
1981
q_ = subst(theta, q)
1982
if all([unify_mm(x, q_) is None for x in kb.clauses + new]):
1983
new.append(q_)
1984
phi = unify_mm(q_, alpha)
1985
if phi is not None:
1986
yield phi
1987
if not new:
1988
break
1989
for clause in new:
1990
kb.tell(clause)
1991
return None
1992
1993
1994
def fol_bc_ask(kb, query):
1995
"""
1996
[Figure 9.6]
1997
A simple backward-chaining algorithm for first-order logic.
1998
KB should be an instance of FolKB, and query an atomic sentence.
1999
"""
2000
return fol_bc_or(kb, query, {})
2001
2002
2003
def fol_bc_or(kb, goal, theta):
2004
for rule in kb.fetch_rules_for_goal(goal):
2005
lhs, rhs = parse_definite_clause(standardize_variables(rule))
2006
for theta1 in fol_bc_and(kb, lhs, unify_mm(rhs, goal, theta)):
2007
yield theta1
2008
2009
2010
def fol_bc_and(kb, goals, theta):
2011
if theta is None:
2012
pass
2013
elif not goals:
2014
yield theta
2015
else:
2016
first, rest = goals[0], goals[1:]
2017
for theta1 in fol_bc_or(kb, subst(theta, first), theta):
2018
for theta2 in fol_bc_and(kb, rest, theta1):
2019
yield theta2
2020
2021
2022
# A simple KB that defines the relevant conditions of the Wumpus World as in Figure 7.4.
2023
# See Sec. 7.4.3
2024
wumpus_kb = PropKB()
2025
2026
P11, P12, P21, P22, P31, B11, B21 = expr('P11, P12, P21, P22, P31, B11, B21')
2027
wumpus_kb.tell(~P11)
2028
wumpus_kb.tell(B11 | '<=>' | (P12 | P21))
2029
wumpus_kb.tell(B21 | '<=>' | (P11 | P22 | P31))
2030
wumpus_kb.tell(~B11)
2031
wumpus_kb.tell(B21)
2032
2033
test_kb = FolKB(map(expr, ['Farmer(Mac)',
2034
'Rabbit(Pete)',
2035
'Mother(MrsMac, Mac)',
2036
'Mother(MrsRabbit, Pete)',
2037
'(Rabbit(r) & Farmer(f)) ==> Hates(f, r)',
2038
'(Mother(m, c)) ==> Loves(m, c)',
2039
'(Mother(m, r) & Rabbit(r)) ==> Rabbit(m)',
2040
'(Farmer(f)) ==> Human(f)',
2041
# Note that this order of conjuncts
2042
# would result in infinite recursion:
2043
# '(Human(h) & Mother(m, h)) ==> Human(m)'
2044
'(Mother(m, h) & Human(h)) ==> Human(m)']))
2045
2046
crime_kb = FolKB(map(expr, ['(American(x) & Weapon(y) & Sells(x, y, z) & Hostile(z)) ==> Criminal(x)',
2047
'Owns(Nono, M1)',
2048
'Missile(M1)',
2049
'(Missile(x) & Owns(Nono, x)) ==> Sells(West, x, Nono)',
2050
'Missile(x) ==> Weapon(x)',
2051
'Enemy(x, America) ==> Hostile(x)',
2052
'American(West)',
2053
'Enemy(Nono, America)']))
2054
2055
2056
# ______________________________________________________________________________
2057
2058
# Example application (not in the book).
2059
# You can use the Expr class to do symbolic differentiation. This used to be
2060
# a part of AI; now it is considered a separate field, Symbolic Algebra.
2061
2062
2063
def diff(y, x):
2064
"""Return the symbolic derivative, dy/dx, as an Expr.
2065
However, you probably want to simplify the results with simp.
2066
>>> diff(x * x, x)
2067
((x * 1) + (x * 1))
2068
"""
2069
if y == x:
2070
return 1
2071
elif not y.args:
2072
return 0
2073
else:
2074
u, op, v = y.args[0], y.op, y.args[-1]
2075
if op == '+':
2076
return diff(u, x) + diff(v, x)
2077
elif op == '-' and len(y.args) == 1:
2078
return -diff(u, x)
2079
elif op == '-':
2080
return diff(u, x) - diff(v, x)
2081
elif op == '*':
2082
return u * diff(v, x) + v * diff(u, x)
2083
elif op == '/':
2084
return (v * diff(u, x) - u * diff(v, x)) / (v * v)
2085
elif op == '**' and isnumber(x.op):
2086
return v * u ** (v - 1) * diff(u, x)
2087
elif op == '**':
2088
return (v * u ** (v - 1) * diff(u, x) +
2089
u ** v * Expr('log')(u) * diff(v, x))
2090
elif op == 'log':
2091
return diff(u, x) / u
2092
else:
2093
raise ValueError('Unknown op: {} in diff({}, {})'.format(op, y, x))
2094
2095
2096
def simp(x):
2097
"""Simplify the expression x."""
2098
if isnumber(x) or not x.args:
2099
return x
2100
args = list(map(simp, x.args))
2101
u, op, v = args[0], x.op, args[-1]
2102
if op == '+':
2103
if v == 0:
2104
return u
2105
if u == 0:
2106
return v
2107
if u == v:
2108
return 2 * u
2109
if u == -v or v == -u:
2110
return 0
2111
elif op == '-' and len(args) == 1:
2112
if u.op == '-' and len(u.args) == 1:
2113
return u.args[0] # --y ==> y
2114
elif op == '-':
2115
if v == 0:
2116
return u
2117
if u == 0:
2118
return -v
2119
if u == v:
2120
return 0
2121
if u == -v or v == -u:
2122
return 0
2123
elif op == '*':
2124
if u == 0 or v == 0:
2125
return 0
2126
if u == 1:
2127
return v
2128
if v == 1:
2129
return u
2130
if u == v:
2131
return u ** 2
2132
elif op == '/':
2133
if u == 0:
2134
return 0
2135
if v == 0:
2136
return Expr('Undefined')
2137
if u == v:
2138
return 1
2139
if u == -v or v == -u:
2140
return 0
2141
elif op == '**':
2142
if u == 0:
2143
return 0
2144
if v == 0:
2145
return 1
2146
if u == 1:
2147
return 1
2148
if v == 1:
2149
return u
2150
elif op == 'log':
2151
if u == 1:
2152
return 0
2153
else:
2154
raise ValueError('Unknown op: ' + op)
2155
# If we fall through to here, we can not simplify further
2156
return Expr(op, *args)
2157
2158
2159
def d(y, x):
2160
"""Differentiate and then simplify.
2161
>>> d(x * x - x, x)
2162
((2 * x) - 1)
2163
"""
2164
return simp(diff(y, x))
2165
2166