├── paip ├── __init__.py ├── tests │ ├── __init__.py │ ├── test_eliza.py │ ├── test_gps.py │ ├── test_search.py │ ├── test_othello.py │ ├── test_logic.py │ └── test_emycin.py ├── examples │ ├── __init__.py │ ├── eliza │ │ ├── __init__.py │ │ ├── support.py │ │ └── eliza.py │ ├── emycin │ │ ├── __init__.py │ │ └── mycin.py │ ├── gps │ │ ├── __init__.py │ │ ├── school.py │ │ ├── monkeys.py │ │ └── blocks.py │ ├── logic │ │ ├── __init__.py │ │ ├── find_list.py │ │ ├── find_length.py │ │ ├── find_list_length_4.py │ │ ├── find_elements.py │ │ ├── transitive.py │ │ ├── likes.py │ │ └── find_lists_lengths.py │ ├── othello │ │ ├── __init__.py │ │ └── othello.py │ ├── search │ │ ├── __init__.py │ │ ├── pathfinding.py │ │ └── gps.py │ └── prolog │ │ ├── transitive.prolog │ │ ├── pair.prolog │ │ ├── graph.prolog │ │ └── family.prolog ├── abandoned │ ├── generate_blocks.py │ ├── pattern.py │ └── logic.lisp ├── sentences.py ├── gps.py ├── eliza.py ├── search.py └── othello.py ├── .gitignore ├── run_tests.py ├── LICENSE ├── run_examples.py ├── README.md └── prolog.py /paip/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /paip/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /paip/examples/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /paip/examples/eliza/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /paip/examples/emycin/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /paip/examples/gps/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /paip/examples/logic/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /paip/examples/othello/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /paip/examples/search/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .DS_Store 3 | *.pyc 4 | *.sublime* 5 | -------------------------------------------------------------------------------- /paip/examples/prolog/transitive.prolog: -------------------------------------------------------------------------------- 1 | # example of transitive relation 2 | 3 | <- likes(joe, judy) 4 | <- likes(judy, jorge) 5 | <- likes(?x, ?x) 6 | <- likes(?x, ?y) :- likes(?x, ?z), likes(?z, ?y) 7 | -------------------------------------------------------------------------------- /paip/examples/prolog/pair.prolog: -------------------------------------------------------------------------------- 1 | # defining lists in prolog 2 | 3 | <- first(?x, pair(?x, ?more)) 4 | <- rest(?more, pair(?x, ?more)) 5 | 6 | <- member(?x, pair(?x, ?more)) 7 | <- member(?x, pair(?y, ?more)) :- member(?x, ?more) 8 | 9 | <- length(nil, 0) 10 | <- length(pair(?x, nil), inc(0)) 11 | <- length(pair(?first, ?rest), inc(?n)) :- length(?rest, ?n) 12 | -------------------------------------------------------------------------------- /paip/examples/prolog/graph.prolog: -------------------------------------------------------------------------------- 1 | # construct the graph 2 | 3 | # a---b---e 4 | # \ / / 5 | # c---d 6 | 7 | <- linked(a, b) 8 | <- linked(b, c) 9 | <- linked(a, c) 10 | <- linked(c, d) 11 | <- linked(d, e) 12 | <- linked(b, e) 13 | 14 | # node ?y is reachable from node ?x if there exists a path from ?x to ?y 15 | <- reachable(?x, ?y) :- linked(?x, ?z), linked(?z, ?y) 16 | <- reachable(?x, ?y) :- linked(?x, ?y) 17 | <- reachable(?x, ?y) :- linked(?y, ?x) # undirected graph 18 | 19 | -------------------------------------------------------------------------------- /run_tests.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import unittest 4 | 5 | tests_dir = 'paip/tests' 6 | 7 | for file in os.listdir(tests_dir): 8 | # Uncomment to enable test logging: 9 | #logging.basicConfig(level=logging.DEBUG) 10 | 11 | if not file.startswith('test_') or not file.endswith('.py'): 12 | continue 13 | qual_file = os.path.join(tests_dir, file) 14 | module = qual_file.replace('/', '.')[:-3] # leave off .py 15 | print 'Testing module %s' % module 16 | unittest.main(module, exit=False) 17 | -------------------------------------------------------------------------------- /paip/examples/logic/find_list.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from paip import logic 3 | 4 | def main(): 5 | x = logic.Var('x') 6 | y = logic.Var('y') 7 | a = logic.Var('a') 8 | more = logic.Var('more') 9 | 10 | member_first = logic.Clause( 11 | logic.Relation('member', (x, logic.Relation('pair', (x, more))))) 12 | 13 | member_last = logic.Clause( 14 | logic.Relation('member', (x, logic.Relation('pair', (y, x))))) 15 | 16 | member_rest = logic.Clause( 17 | logic.Relation('member', (x, logic.Relation('pair', (y, more)))), 18 | [logic.Relation('member', (x, more))]) 19 | 20 | db = {} 21 | logic.store(db, member_first) 22 | logic.store(db, member_last) 23 | logic.store(db, member_rest) 24 | 25 | print 'Database:' 26 | print db 27 | print 28 | 29 | query = logic.Relation('member', (logic.Atom('foo'), x)) 30 | print 'Query:', query 31 | print 32 | 33 | logic.prolog_prove([query], db) 34 | -------------------------------------------------------------------------------- /paip/abandoned/generate_blocks.py: -------------------------------------------------------------------------------- 1 | import json 2 | import sys 3 | 4 | def move_op(a, b, c): 5 | return { 6 | 'action': 'move %s from %s to %s' % (a, b, c), 7 | 'preconds': [ 8 | 'space on %s' % a, 9 | 'space on %s' % c, 10 | '%s on %s' % (a, b) 11 | ], 12 | 'add': move_ons(a, b, c), 13 | 'delete': move_ons(a, c, b), 14 | } 15 | 16 | 17 | def move_ons(a, b, c): 18 | moves = ['%s on %s' % (a, c)] 19 | if b != 'table': 20 | moves.append('space on %s' % b) 21 | return moves 22 | 23 | 24 | def generate(blocks): 25 | ops = [] 26 | for a in blocks: 27 | for b in blocks: 28 | if a == b: continue 29 | for c in blocks: 30 | if c in (a, b): continue 31 | ops.append(move_op(a, b, c)) 32 | ops.append(move_op(a, 'table', b)) 33 | ops.append(move_op(a, b, 'table')) 34 | print json.dumps(ops, indent=4) 35 | 36 | 37 | if __name__ == '__main__': 38 | generate(sys.argv[1:]) 39 | -------------------------------------------------------------------------------- /paip/examples/logic/find_length.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from paip import logic 3 | 4 | def main(): 5 | x = logic.Var('x') 6 | y = logic.Var('y') 7 | z = logic.Var('z') 8 | a = logic.Var('a') 9 | nil = logic.Atom('nil') 10 | more = logic.Var('more') 11 | zero = logic.Atom('0') 12 | 13 | length_nil = logic.Clause(logic.Relation('length', (nil, zero))) 14 | length_one = logic.Clause( 15 | logic.Relation('length', 16 | (logic.Relation('pair', (x, more)), 17 | logic.Relation('+1', [a]))), 18 | [logic.Relation('length', (more, a))]) 19 | 20 | db = {} 21 | logic.store(db, length_nil) 22 | logic.store(db, length_one) 23 | 24 | print 'Database:' 25 | print db 26 | print 27 | 28 | list = logic.Relation( 29 | 'pair', (x, logic.Relation( 30 | 'pair', (y, logic.Relation( 31 | 'pair', (z, nil)))))) 32 | 33 | query = logic.Relation('length', (list, a)) 34 | print 'Query:', query 35 | print 36 | 37 | logic.prolog_prove([query], db) 38 | -------------------------------------------------------------------------------- /paip/examples/prolog/family.prolog: -------------------------------------------------------------------------------- 1 | # a family tree example 2 | # from http://www.cs.toronto.edu/~hojjat/384f06/simple-prolog-examples.html 3 | # for some reason, in this family tree, reproduction is asexual. 4 | 5 | <- male(james1) 6 | <- male(charles1) 7 | <- male(charles2) 8 | <- male(james2) 9 | <- male(george1) 10 | 11 | <- female(catherine) 12 | <- female(elizabeth) 13 | <- female(sophia) 14 | 15 | # parent(?x, ?y) means ?y is the parent of ?x 16 | <- parent(charles1, james1) 17 | <- parent(elizabeth, james1) 18 | <- parent(charles2, charles1) 19 | <- parent(catherine, charles1) 20 | <- parent(james2, charles1) 21 | <- parent(sophia, elizabeth) 22 | <- parent(george1, sophia) 23 | 24 | <- mother(?x, ?m) :- parent(?x, ?m), female(?m) 25 | <- father(?x, ?f) :- parent(?x, ?f), male(?f) 26 | <- sibling(?x, ?y) :- parent(?x, ?p), parent(?y, ?p) 27 | 28 | <- sister(?x, ?y) :- sibling(?x, ?y), female(?y) 29 | <- brother(?x, ?y) :- sibling(?x, ?y), male(?y) 30 | <- grandparent(?x, ?y) :- parent(?x, ?z), parent(?z, ?y) 31 | 32 | <- ancestor(?x, ?y) :- parent(?x, ?y) 33 | <- ancestor(?x, ?y) :- ancestor(?x, ?z), ancestor(?z, ?y) 34 | -------------------------------------------------------------------------------- /paip/examples/logic/find_list_length_4.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from paip import logic 3 | 4 | def main(): 5 | x = logic.Var('x') 6 | y = logic.Var('y') 7 | z = logic.Var('z') 8 | a = logic.Var('a') 9 | nil = logic.Atom('nil') 10 | more = logic.Var('more') 11 | zero = logic.Atom('0') 12 | 13 | length_nil = logic.Clause(logic.Relation('length', (nil, zero))) 14 | length_one = logic.Clause( 15 | logic.Relation('length', 16 | (logic.Relation('pair', (x, more)), 17 | logic.Relation('+1', [a]))), 18 | [logic.Relation('length', (more, a))]) 19 | 20 | db = {} 21 | logic.store(db, length_nil) 22 | logic.store(db, length_one) 23 | 24 | print 'Database:' 25 | print db 26 | print 27 | 28 | four = logic.Relation( 29 | '+1', [logic.Relation( 30 | '+1', [logic.Relation( 31 | '+1', [logic.Relation('+1', [zero])])])]) 32 | 33 | query = logic.Relation('length', (x, four)) 34 | print 'Query:', query 35 | print 36 | 37 | logic.prolog_prove([query], db) 38 | -------------------------------------------------------------------------------- /paip/examples/logic/find_elements.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from paip import logic 3 | 4 | def main(): 5 | x = logic.Var('x') 6 | y = logic.Var('y') 7 | a = logic.Var('a') 8 | nil = logic.Atom('nil') 9 | more = logic.Var('more') 10 | 11 | member_first = logic.Clause( 12 | logic.Relation('member', (x, logic.Relation('pair', (x, more))))) 13 | 14 | member_last = logic.Clause( 15 | logic.Relation('member', (x, logic.Relation('pair', (y, x))))) 16 | 17 | member_rest = logic.Clause( 18 | logic.Relation('member', (x, logic.Relation('pair', (y, more)))), 19 | [logic.Relation('member', (x, more))]) 20 | 21 | db = {} 22 | logic.store(db, member_first) 23 | logic.store(db, member_last) 24 | logic.store(db, member_rest) 25 | 26 | list = logic.Relation( 27 | 'pair', (logic.Atom('foo'), logic.Relation( 28 | 'pair', (logic.Atom('bar'), logic.Relation( 29 | 'pair', (logic.Atom('baz'), nil)))))) 30 | 31 | print 'Database:' 32 | print db 33 | print 34 | 35 | query = logic.Relation('member', (x, list)) 36 | print 'Query:', query 37 | print 38 | 39 | logic.prolog_prove([query], db) 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Daniel Connelly 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /paip/examples/logic/transitive.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from paip import logic 3 | 4 | def main(): 5 | #logging.basicConfig(level=logging.DEBUG) 6 | db = {} 7 | 8 | kim = logic.Atom('Kim') 9 | robin = logic.Atom('Robin') 10 | sandy = logic.Atom('Sandy') 11 | lee = logic.Atom('Lee') 12 | cats = logic.Atom('cats') 13 | x = logic.Var('x') 14 | y = logic.Var('y') 15 | z = logic.Var('z') 16 | 17 | self_likes = logic.Clause(logic.Relation('likes', (x, x))) 18 | transitive_likes = logic.Clause(logic.Relation('likes', (x, y)), 19 | (logic.Relation('likes', (x, z)), logic.Relation('likes', (z, y)))) 20 | 21 | klr = logic.Clause(logic.Relation('likes', (kim, robin))) 22 | sll = logic.Clause(logic.Relation('likes', (sandy, lee))) 23 | slk = logic.Clause(logic.Relation('likes', (sandy, kim))) 24 | rlc = logic.Clause(logic.Relation('likes', (robin, cats))) 25 | llr = logic.Clause(logic.Relation('likes', (lee, robin))) 26 | 27 | logic.store(db, klr) 28 | logic.store(db, sll) 29 | logic.store(db, slk) 30 | logic.store(db, rlc) 31 | logic.store(db, llr) 32 | 33 | logic.store(db, self_likes) 34 | logic.store(db, transitive_likes) 35 | 36 | print 'Database:' 37 | print db 38 | print 39 | 40 | query = logic.Relation('likes', (sandy, logic.Var('who'))) 41 | print 'Query:', str(query) 42 | print 43 | 44 | logic.prolog_prove([query], db) 45 | 46 | -------------------------------------------------------------------------------- /paip/examples/gps/school.py: -------------------------------------------------------------------------------- 1 | from paip.gps import gps 2 | 3 | problem = { 4 | "start": ["son at home", "have money", "have phone book", "car needs battery"], 5 | "finish": ["son at school"], 6 | "ops": [ 7 | { 8 | "action": "drive son to school", 9 | "preconds": ["son at home", "car works"], 10 | "add": ["son at school"], 11 | "delete": ["son at home"] 12 | }, 13 | { 14 | "action": "shop installs battery", 15 | "preconds": ["car needs battery", "shop knows problem", "shop has money"], 16 | "add": ["car works"], 17 | "delete": [] 18 | }, 19 | { 20 | "action": "tell shop problem", 21 | "preconds": ["in communication with shop"], 22 | "add": ["shop knows problem"], 23 | "delete": [] 24 | }, 25 | { 26 | "action": "telephone shop", 27 | "preconds": ["know phone number"], 28 | "add": ["in communication with shop"], 29 | "delete": [] 30 | }, 31 | { 32 | "action": "look up number", 33 | "preconds": ["have phone book"], 34 | "add": ["know phone number"], 35 | "delete": [] 36 | }, 37 | { 38 | "action": "give shop money", 39 | "preconds": ["have money"], 40 | "add": ["shop has money"], 41 | "delete": ["have money"] 42 | } 43 | ] 44 | } 45 | 46 | def main(): 47 | start = problem['start'] 48 | finish = problem['finish'] 49 | ops = problem['ops'] 50 | for action in gps(start, finish, ops): 51 | print action 52 | 53 | if __name__ == '__main__': 54 | main() 55 | -------------------------------------------------------------------------------- /paip/examples/logic/likes.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from paip import logic 3 | 4 | def main(): 5 | #logging.basicConfig(level=logging.DEBUG) 6 | db = {} 7 | 8 | kim = logic.Atom('Kim') 9 | robin = logic.Atom('Robin') 10 | sandy = logic.Atom('Sandy') 11 | lee = logic.Atom('Lee') 12 | cats = logic.Atom('cats') 13 | x = logic.Var('x') 14 | 15 | sandy_likes = logic.Relation('likes', (sandy, x)) 16 | likes_cats = logic.Relation('likes', (x, cats)) 17 | sandy_likes_rule = logic.Clause(sandy_likes, [likes_cats]) 18 | 19 | kim_likes = logic.Relation('likes', (kim, x)) 20 | likes_lee = logic.Relation('likes', (x, lee)) 21 | likes_kim = logic.Relation('likes', (x, kim)) 22 | kim_likes_rule = logic.Clause(kim_likes, [likes_lee, likes_kim]) 23 | 24 | likes_self = logic.Clause(logic.Relation('likes', (x, x))) 25 | klr = logic.Clause(logic.Relation('likes', (kim, robin))) 26 | sll = logic.Clause(logic.Relation('likes', (sandy, lee))) 27 | slk = logic.Clause(logic.Relation('likes', (sandy, kim))) 28 | rlc = logic.Clause(logic.Relation('likes', (robin, cats))) 29 | 30 | logic.store(db, sandy_likes_rule) 31 | logic.store(db, kim_likes_rule) 32 | logic.store(db, likes_self) 33 | logic.store(db, klr) 34 | logic.store(db, sll) 35 | logic.store(db, slk) 36 | logic.store(db, rlc) 37 | 38 | print 'Database:' 39 | print db 40 | print 41 | 42 | query = logic.Relation('likes', (sandy, logic.Var('who'))) 43 | print 'Query:', str(query) 44 | print 45 | 46 | logic.prolog_prove([query], db) 47 | 48 | -------------------------------------------------------------------------------- /paip/examples/gps/monkeys.py: -------------------------------------------------------------------------------- 1 | from paip.gps import gps 2 | 3 | problem = { 4 | "start": ["at door", "on floor", "has ball", "hungry", "chair at door"], 5 | "finish": ["not hungry"], 6 | "ops": [ 7 | { 8 | "action": "climb on chair", 9 | "preconds": ["chair at middle room", "at middle room", "on floor"], 10 | "add": ["at bananas", "on chair"], 11 | "delete": ["at middle room", "on floor"] 12 | }, 13 | { 14 | "action": "push chair from door to middle room", 15 | "preconds": ["chair at door", "at door"], 16 | "add": ["chair at middle room", "at middle room"], 17 | "delete": ["chair at door", "at door"] 18 | }, 19 | { 20 | "action": "walk from door to middle room", 21 | "preconds": ["at door", "on floor"], 22 | "add": ["at middle room"], 23 | "delete": ["at door"] 24 | }, 25 | { 26 | "action": "grasp bananas", 27 | "preconds": ["at bananas", "empty handed"], 28 | "add": ["has bananas"], 29 | "delete": ["empty handed"] 30 | }, 31 | { 32 | "action": "drop ball", 33 | "preconds": ["has ball"], 34 | "add": ["empty handed"], 35 | "delete": ["has ball"] 36 | }, 37 | { 38 | "action": "eat bananas", 39 | "preconds": ["has bananas"], 40 | "add": ["empty handed", "not hungry"], 41 | "delete": ["has bananas", "hungry"] 42 | } 43 | ] 44 | } 45 | 46 | def main(): 47 | start = problem['start'] 48 | finish = problem['finish'] 49 | ops = problem['ops'] 50 | for action in gps(start, finish, ops): 51 | print action 52 | 53 | if __name__ == '__main__': 54 | main() 55 | -------------------------------------------------------------------------------- /run_examples.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import logging 3 | import os 4 | import sys 5 | 6 | 7 | parser = argparse.ArgumentParser(description='Run example AI programs.') 8 | parser.add_argument('--log', action='store_true', help='Turn on logging') 9 | 10 | 11 | def main(): 12 | print 'Please choose an example to run:' 13 | modules = [] 14 | for i, name in enumerate(discover_modules('paip/examples')): 15 | __import__(name) 16 | module = sys.modules[name] 17 | modules.append(module) 18 | print '%d\t%s' % (i, module.__name__) 19 | while True: 20 | try: 21 | choice = raw_input('>> ') 22 | if not choice: 23 | continue 24 | ind = int(choice) 25 | except ValueError: 26 | print 'That is not a valid option. Please try again.' 27 | continue 28 | except EOFError: 29 | print 'Goodbye.' 30 | return 31 | try: 32 | module = modules[ind] 33 | except IndexError: 34 | print 'That is not a valid option. Please try again.' 35 | else: 36 | print module.__doc__ 37 | return module.main() 38 | 39 | 40 | def discover_modules(root): 41 | modules = [] 42 | for root, dirs, files in os.walk(root): 43 | for f in (f for f in files if f.endswith('.py') and '__init__' not in f): 44 | path = os.path.join(root, f)[:-3] 45 | if root.startswith('.'): 46 | path = path[2:] 47 | modules.append(path.replace(os.sep, '.')) 48 | return modules 49 | 50 | 51 | if __name__ == '__main__': 52 | args = vars(parser.parse_args()) 53 | if args['log']: 54 | logging.basicConfig(level=logging.DEBUG) 55 | main() 56 | -------------------------------------------------------------------------------- /paip/examples/logic/find_lists_lengths.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from paip import logic 3 | 4 | def main(): 5 | x = logic.Var('x') 6 | y = logic.Var('y') 7 | z = logic.Var('z') 8 | a = logic.Var('a') 9 | nil = logic.Atom('nil') 10 | more = logic.Var('more') 11 | zero = logic.Atom('0') 12 | 13 | length_nil = logic.Clause(logic.Relation('length', (nil, zero))) 14 | length_one = logic.Clause( 15 | logic.Relation('length', 16 | (logic.Relation('pair', (x, more)), 17 | logic.Relation('+1', [a]))), 18 | [logic.Relation('length', (more, a))]) 19 | 20 | member_first = logic.Clause( 21 | logic.Relation('member', (x, logic.Relation('pair', (x, more))))) 22 | 23 | member_last = logic.Clause( 24 | logic.Relation('member', (x, logic.Relation('pair', (y, x))))) 25 | 26 | member_end = logic.Clause( 27 | logic.Relation('member', (x, logic.Relation('pair', (x, nil))))) 28 | 29 | member_rest = logic.Clause( 30 | logic.Relation('member', (x, logic.Relation('pair', (y, more)))), 31 | [logic.Relation('member', (x, more))]) 32 | 33 | db = {} 34 | logic.store(db, length_nil) 35 | logic.store(db, length_one) 36 | logic.store(db, member_end) 37 | logic.store(db, member_first) 38 | logic.store(db, member_last) 39 | logic.store(db, member_rest) 40 | 41 | print 'Database:' 42 | print db 43 | print 44 | 45 | four = logic.Relation( 46 | '+1', [logic.Relation( 47 | '+1', [logic.Relation( 48 | '+1', [logic.Relation('+1', [zero])])])]) 49 | 50 | foo = logic.Atom('foo') 51 | 52 | has_foo = logic.Relation('member', (foo, x)) 53 | length_4 = logic.Relation('length', (x, a)) 54 | 55 | print 'Query:', has_foo, length_4 56 | print 57 | 58 | logic.prolog_prove([has_foo, length_4], db) 59 | -------------------------------------------------------------------------------- /paip/examples/eliza/support.py: -------------------------------------------------------------------------------- 1 | import json 2 | import sys 3 | from paip import eliza 4 | 5 | rules = { 6 | "?*x hello ?*y": [ 7 | "Hello, my name is SUPPORT. How can I help you today?", 8 | ], 9 | "?*x manager ?*y": [ 10 | "Our manager is not available right now. How can I help you?" 11 | ], 12 | "?*x problem with ?*y": [ 13 | "Have you tried turning it off and back on?", 14 | "When did you first observe ?y to be a problem?", 15 | "How long has ?y been a problem?", 16 | "That is not covered by the warranty.", 17 | ], 18 | "?*x trouble with ?*y": [ 19 | ("I'm sorry, ?y is handled by another department.\n" + 20 | "Please wait while I transfer your call."), 21 | "What seems to be the problem?", 22 | ], 23 | "?*x how to ?*y": [ 24 | "Please consult the manual for more details.", 25 | "I'm sorry, I do not have that information.", 26 | "Can you be more specific?", 27 | "?y will void your warranty." 28 | ], 29 | "?*x why ?*y": [ 30 | "I'm sorry, I can't discuss that.", 31 | ], 32 | "?*x ago ?*y": [ 33 | "I'm sorry, your warranty ended a week earlier.", 34 | ] 35 | } 36 | 37 | default_responses = [ 38 | "I do not understand.", 39 | "Please elaborate.", 40 | "Please hold." 41 | ] 42 | 43 | def main(): 44 | # We need the rules in a list containing elements of the following form: 45 | # `(input pattern, [output pattern 1, output pattern 2, ...]` 46 | rules_list = [] 47 | for pattern, transforms in rules.items(): 48 | # Remove the punctuation from the pattern to simplify matching. 49 | pattern = eliza.remove_punct(str(pattern.upper())) # kill unicode 50 | transforms = [str(t).upper() for t in transforms] 51 | rules_list.append((pattern, transforms)) 52 | eliza.interact('SUPPORT> ', rules_list, map(str.upper, default_responses)) 53 | 54 | if __name__ == '__main__': 55 | main(sys.argv[1:]) 56 | -------------------------------------------------------------------------------- /paip/examples/othello/othello.py: -------------------------------------------------------------------------------- 1 | from paip import othello 2 | 3 | def check(move, player, board): 4 | return othello.is_valid(move) and othello.is_legal(move, player, board) 5 | 6 | def human(player, board): 7 | print othello.print_board(board) 8 | print 'Your move?' 9 | while True: 10 | move = raw_input('> ') 11 | if move and check(int(move), player, board): 12 | return int(move) 13 | elif move: 14 | print 'Illegal move--try again.' 15 | 16 | def get_choice(prompt, options): 17 | print prompt 18 | print 'Options:', options.keys() 19 | while True: 20 | choice = raw_input('> ') 21 | if choice in options: 22 | return options[choice] 23 | elif choice: 24 | print 'Invalid choice.' 25 | 26 | def get_players(): 27 | print 'Welcome to OTHELLO!' 28 | options = { 'human': human, 29 | 'random': othello.random_strategy, 30 | 'max-diff': othello.maximizer(othello.score), 31 | 'max-weighted-diff': othello.maximizer(othello.weighted_score), 32 | 'minimax-diff': othello.minimax_searcher(3, othello.score), 33 | 'minimax-weighted-diff': 34 | othello.minimax_searcher(3, othello.weighted_score), 35 | 'ab-diff': othello.alphabeta_searcher(3, othello.score), 36 | 'ab-weighted-diff': 37 | othello.alphabeta_searcher(3, othello.weighted_score) } 38 | black = get_choice('BLACK: choose a strategy', options) 39 | white = get_choice('WHITE: choose a strategy', options) 40 | return black, white 41 | 42 | def main(): 43 | try: 44 | black, white = get_players() 45 | board, score = othello.play(black, white) 46 | except othello.IllegalMoveError as e: 47 | print e 48 | return 49 | except EOFError as e: 50 | print 'Goodbye.' 51 | return 52 | print 'Final score:', score 53 | print '%s wins!' % ('Black' if score > 0 else 'White') 54 | print othello.print_board(board) 55 | -------------------------------------------------------------------------------- /paip/examples/search/pathfinding.py: -------------------------------------------------------------------------------- 1 | """ 2 | An application of A* pathfinding: find the best path through a map. The map is 3 | represented as a grid, where 1 is an obstacle and 0 is an open space. Movement 4 | can be up, down, left, right, and diagonal in one step. 5 | """ 6 | 7 | ## Pathfinding 8 | 9 | def find_path(map, begin, end): 10 | """Find the best path between the begin and end position in the map.""" 11 | start_path = [search.Path(begin)] 12 | cost = lambda loc1, loc2: abs(loc1[0] - loc2[0]) + abs(loc1[1] - loc2[1]) 13 | remaining = lambda loc: cost(loc, end) 14 | done = lambda loc: loc == end 15 | 16 | path = search.a_star(start_path, done, map_successors, cost, remaining) 17 | return path.collect() 18 | 19 | 20 | def map_successors(location): 21 | """Get the locations accessible from the current location.""" 22 | row, col = location 23 | possible = [(row + dy, col + dx) for dy in (-1, 0, 1) for dx in (-1, 0, 1)] 24 | # out of all the neighbor locations, filter out the current one, any 25 | # locations outside the map border, and the locations containing obstacles. 26 | successors = [(row, col) for (row, col) in possible 27 | if 0 <= row < len(MAP) and 0 <= col < len(MAP[0]) 28 | and MAP[row][col] == 0 29 | and (row, col) != location] 30 | return successors 31 | 32 | 33 | ## Utilities 34 | 35 | def print_map(map): 36 | """Pretty-prints the given map to standard output.""" 37 | print '-' * (2 * len(map) + 3) 38 | for row in map: 39 | print '|', 40 | for col in row: 41 | print '%s' % (col if col == 1 or col == 'X' else ' '), 42 | print '|' 43 | print '-' * (2 * len(map) + 3) 44 | 45 | 46 | ## Running from the command line 47 | 48 | from paip import search 49 | 50 | 51 | MAP = [ 52 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 53 | [0, 0, 1, 0, 0, 0, 0, 0, 0, 0], 54 | [0, 1, 1, 0, 1, 1, 0, 0, 1, 0], 55 | [0, 0, 1, 0, 1, 1, 0, 1, 1, 0], 56 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 57 | [0, 1, 1, 1, 0, 1, 1, 0, 0, 0], 58 | [0, 0, 0, 0, 0, 1, 1, 0, 0, 0], 59 | [0, 0, 0, 0, 1, 1, 0, 0, 1, 0], 60 | [0, 0, 0, 0, 0, 0, 0, 0, 1, 0], 61 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 62 | ] 63 | 64 | 65 | def main(): 66 | print_map(MAP) 67 | begin = (0, 0) 68 | end = (len(MAP)-1, len(MAP)-1) 69 | for (row, col) in find_path(MAP, begin, end): 70 | MAP[row][col] = 'X' 71 | print_map(MAP) 72 | 73 | 74 | if __name__ == '__main__': 75 | main() 76 | -------------------------------------------------------------------------------- /paip/sentences.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Random sentence generation using context-free generative grammars. 5 | 6 | Translated from chapter 2 of "Paradigms of Artificial Intelligence 7 | Programming" by Peter Norvig. 8 | """ 9 | 10 | __author__ = 'Daniel Connelly' 11 | __email__ = 'dconnelly@gatech.edu' 12 | 13 | 14 | import random 15 | 16 | 17 | SIMPLE_ENGLISH = { 18 | 'verb': ['hit', 'took', 'saw', 'liked'], 19 | 'noun': ['man', 'ball', 'woman', 'table'], 20 | 'article': ['the', 'a'], 21 | 22 | 'sentence': [['noun phrase', 'verb phrase']], 23 | 'noun phrase': [['article', 'noun']], 24 | 'verb phrase': [['verb', 'noun phrase']], 25 | } 26 | 27 | 28 | BIGGER_ENGLISH = { 29 | 'prep': ['to', 'in', 'by', 'with', 'on'], 30 | 'adj': ['big', 'little', 'blue', 'green', 'adiabatic'], 31 | 'article': ['a', 'the'], 32 | 'name': ['Pat', 'Kim', 'Lee', 'Terry', 'Robin'], 33 | 'noun': ['man', 'ball', 'woman', 'table'], 34 | 'verb': ['hit', 'took', 'saw', 'liked'], 35 | 'pronoun': ['he', 'she', 'it', 'these', 'those', 'that'], 36 | 37 | 'sentence': [['noun phrase', 'verb phrase']], 38 | 'noun phrase': [['article', 'adj*', 'noun', 'pp*'], 39 | ['name'], 40 | ['pronoun']], 41 | 'verb phrase': [['verb', 'noun phrase', 'pp*']], 42 | 'pp*': [[], ['pp']], 43 | 'adj*': [[], ['adj']], 44 | 'pp': [['prep', 'noun phrase']], 45 | } 46 | 47 | 48 | def generate(grammar, phrase): 49 | """Recursively rewrites each subphrase until only terminals remain. 50 | 51 | grammar is a dictionary defining a context-free grammar, where each 52 | (key, value) item defines a rewriting rule. 53 | Each subphrase of phrase is recursively rewritten unless it does not 54 | appear as a key in the grammar. 55 | """ 56 | if isinstance(phrase, list): 57 | phrases = (generate(grammar, p) for p in phrase) 58 | return " ".join(p for p in phrases if p) 59 | elif phrase in grammar: 60 | return generate(grammar, random.choice(grammar[phrase])) 61 | else: 62 | return phrase 63 | 64 | 65 | def generate_tree(grammar, phrase): 66 | """Generates a sentence from the grammar and returns its parse tree. 67 | 68 | The sentence is generated in the same manner as in generate, but the 69 | returned value is a nested list where the first element of each sublist 70 | is the name of the rule generating the phrase. 71 | """ 72 | if isinstance(phrase, list): 73 | return [generate_tree(grammar, p) for p in phrase] 74 | elif phrase in grammar: 75 | return [phrase] + generate_tree(grammar, random.choice(grammar[phrase])) 76 | else: 77 | return [phrase] 78 | 79 | 80 | if __name__ == '__main__': 81 | print generate(BIGGER_ENGLISH, 'sentence') 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | paip-python 2 | =========== 3 | 4 | Python implementations of some of the classic AI programs from Peter Norvig's 5 | fantastic textbook "Paradigms of Artificial Intelligence Programming." 6 | 7 | About 8 | ----- 9 | 10 | This is meant to be a learning resource for beginning AI programmers. Although 11 | PAIP is a fantastic book, it is no longer common for students to have a 12 | background in Lisp programming, as many universities have replaced Lisp with 13 | other languages in introductory programming and introductory artificial 14 | intelligence courses. It is my hope that making the programs from PAIP 15 | available in a commonly-taught language will provide a useful hands-on resource 16 | for beginning AI students. 17 | 18 | The following programs are available, each with annotated source: 19 | 20 | - [General Problem Solver][GPS], means-ends analysis problem solving 21 | - [Eliza][], a pattern-matching psychiatrist 22 | - [Search][], a collection of search algorithms 23 | - [Logic][], a library for logic programming 24 | - [Prolog][], a basic Prolog interpreter 25 | - [Emycin][], an expert system shell 26 | - [Othello][], some game-playing strategies for the Othello board game 27 | 28 | Unit tests and some example applications are provided for each of these; see the 29 | `paip/tests` and `paip/examples` directories or the links from the annotated 30 | sources. 31 | 32 | [GPS]: http://dhconnelly.github.io/paip-python/docs/paip/gps.html 33 | [Eliza]: http://dhconnelly.github.io/paip-python/docs/paip/eliza.html 34 | [Search]: http://dhconnelly.github.io/paip-python/docs/paip/search.html 35 | [Logic]: http://dhconnelly.github.io/paip-python/docs/paip/logic.html 36 | [Prolog]: http://dhconnelly.github.io/paip-python/docs/prolog.html 37 | [Emycin]: http://dhconnelly.github.io/paip-python/docs/paip/emycin.html 38 | [Othello]: http://dhconnelly.github.io/paip-python/docs/paip/othello.html 39 | 40 | Getting Started 41 | --------------- 42 | 43 | Get the source code from [GitHub](https://github.com/dhconnelly/paip-python) or 44 | just [download](https://github.com/dhconnelly/paip-python/zipball/master) the 45 | latest version. 46 | 47 | Also make sure you have [Python 2.7](http://python.org/download/releases). 48 | 49 | - To run the examples: `python run_examples.py` and follow the prompts. 50 | - To run the Prolog interpreter: `./prolog.py`. Pass the `-h` flag for more 51 | details on its use and capabilities. 52 | - To run the unit tests: `python run_tests.py`. 53 | - To build the documentation: `python build_docs.py`. 54 | 55 | Contributing 56 | ------------ 57 | 58 | - fork on [GitHub](https://github.com/dhconnelly/paip-python) 59 | - write code in `paip/` 60 | - add unit tests in `paip/tests` 61 | - make sure all tests pass: `python run_tests.py` 62 | - send me a pull request 63 | 64 | Author 65 | ------ 66 | 67 | These programs were written by [Daniel Connelly](http://dhconnelly.com) at 68 | Georgia Tech as an independent project supervised by [Professor Ashok 69 | Goel](http://home.cc.gatech.edu/dil/3). 70 | -------------------------------------------------------------------------------- /paip/tests/test_eliza.py: -------------------------------------------------------------------------------- 1 | from paip import eliza 2 | import unittest 3 | 4 | 5 | class TestIsVariable(unittest.TestCase): 6 | def test_is_variable(self): 7 | self.assertTrue(eliza.is_variable('?x')) 8 | self.assertTrue(eliza.is_variable('?subj')) 9 | 10 | def test_is_not_variable(self): 11 | self.assertFalse(eliza.is_variable('is it?')) 12 | self.assertFalse(eliza.is_variable('? why?')) 13 | self.assertFalse(eliza.is_variable('?')) 14 | self.assertFalse(eliza.is_variable(['?x'])) 15 | self.assertFalse(eliza.is_variable('?foo bar')) 16 | 17 | 18 | class TestIsSegment(unittest.TestCase): 19 | def test_is_segment(self): 20 | self.assertTrue(eliza.is_segment(['?*foo', 'bar'])) 21 | self.assertTrue(eliza.is_segment(['?*x'])) 22 | 23 | def test_is_not_segment(self): 24 | self.assertFalse(eliza.is_segment('?*foo bar')) 25 | self.assertFalse(eliza.is_segment(['?*'])) 26 | self.assertFalse(eliza.is_segment(['?*foo bar'])) 27 | 28 | 29 | class TestContainsTokens(unittest.TestCase): 30 | def test_contains_tokens(self): 31 | self.assertTrue(eliza.contains_tokens(['foo', 'bar'])) 32 | 33 | def test_does_not_contain_tokens(self): 34 | self.assertFalse(eliza.contains_tokens('foo bar')) 35 | self.assertFalse(eliza.contains_tokens([])) 36 | 37 | 38 | class TestMatchVariable(unittest.TestCase): 39 | def test_bind_unbound_variable(self): 40 | self.assertEqual(eliza.match_variable('foo', 'bar', {'baz': 'quux'}), 41 | {'baz': 'quux', 'foo': 'bar'}) 42 | 43 | def test_bind_bound_variable_success(self): 44 | self.assertEqual(eliza.match_variable('foo', 'bar', {'foo': 'bar'}), 45 | {'foo': 'bar'}) 46 | 47 | def test_bind_bound_variable_fail(self): 48 | self.assertFalse(eliza.match_variable('foo', 'bar', {'foo': 'baz'})) 49 | 50 | 51 | class TestMatchSegment(unittest.TestCase): 52 | def test_segment_match_rest(self): 53 | self.assertEqual( 54 | {'bar': 'baz', 'foo': ['bah', 'bla']}, 55 | eliza.match_segment('foo', [], ['bah', 'bla'], {'bar': 'baz'})) 56 | 57 | def test_segment_first_match(self): 58 | self.assertEqual( 59 | {'foo': ['blue'], 'x': ['red']}, 60 | eliza.match_segment('foo', ['is', '?x', 'today'], 61 | ['blue', 'is', 'red', 'today'], {})) 62 | 63 | def test_segment_second_match(self): 64 | phrase = 'blue is red today and today is tomorrow' 65 | self.assertEqual( 66 | {'foo': ['blue'], 'x': ['red', 'today', 'and'], 'y': ['tomorrow']}, 67 | eliza.match_segment('foo', ['is', '?*x', 'today', 'is', '?y'], 68 | phrase.split(), {})) 69 | 70 | def test_segment_no_match(self): 71 | phrase = 'red is blue is not now' 72 | self.assertFalse(eliza.match_segment('foo', ['is', '?y', 'now', '?z'], 73 | phrase.split(), {})) 74 | 75 | 76 | class TestMatchPattern(unittest.TestCase): 77 | def test_no_more_vars(self): 78 | self.assertEqual({}, eliza.match_pattern(['hello', 'world'], 79 | ['hello', 'world'], {})) 80 | 81 | def test_match_no_more_vars_fail(self): 82 | self.assertFalse(eliza.match_pattern(['hello', 'world'], 83 | ['hello', 'bob'], {})) 84 | 85 | def test_match_segment(self): 86 | self.assertEqual({'x': ['hello', 'bob']}, 87 | eliza.match_pattern(['?*x', 'world'], 88 | ['hello', 'bob', 'world'], {})) 89 | 90 | def test_match_var(self): 91 | self.assertEqual({'x': ['bob']}, eliza.match_pattern('?x', 'bob', {})) 92 | 93 | def test_match_pattern(self): 94 | self.assertEqual( 95 | {'y': ['bob'], 'x': ['john', 'jay']}, 96 | eliza.match_pattern( 97 | 'hello ?y my name is ?*x pleased to meet you'.split(), 98 | 'hello bob my name is john jay pleased to meet you'.split(), 99 | {})) 100 | 101 | def test_empty_input(self): 102 | self.assertFalse(eliza.match_pattern(['foo', '?x'], [], {})) 103 | 104 | def test_empty_pattern(self): 105 | self.assertFalse(eliza.match_pattern([], ['foo', 'bar'], {})) 106 | -------------------------------------------------------------------------------- /paip/abandoned/pattern.py: -------------------------------------------------------------------------------- 1 | class SyntaxError(Exception): 2 | def __init__(msg): 3 | self.msg = msg 4 | 5 | def __str__(msg): 6 | print 'Syntax error: %s' % self.msg 7 | 8 | class Binding(object): 9 | def __init__(self, var, type=None): 10 | self.var = var 11 | self.type = type 12 | 13 | def __repr__(self): 14 | return '%sBinding("%s")' % (self.type if self.type else '', self.var) 15 | 16 | class Constant(object): 17 | def __init__(self, val): 18 | self.val = val 19 | 20 | def __repr__(self): 21 | return '"%s"' % self.val 22 | 23 | class Pattern(object): 24 | def __init__(self, exprs): 25 | self.exprs = exprs 26 | 27 | def __repr__(self): 28 | return 'Pattern(%s)' % self.exprs 29 | 30 | class Block(object): 31 | AND, OR, NOT = 'and', 'or', 'not' 32 | 33 | def __init__(self, patterns, type): 34 | self.patterns = patterns 35 | self.type = type 36 | 37 | def __repr__(self): 38 | return '%s(%s)' % (self.type, self.patterns) 39 | 40 | class Scanner(object): 41 | def __init__(self, text): 42 | self.text = text 43 | self.index = 0 44 | 45 | def peek(self): 46 | if self.index == len(self.text): 47 | return None 48 | ch = self.text[self.index] 49 | return ch 50 | 51 | def get(self): 52 | ch = self.peek() 53 | self.index += 1 54 | return ch 55 | 56 | # Token types and recognizers 57 | 58 | SPACE = ' ' 59 | NEWLINE = '\n' 60 | TAB = '\t' 61 | LPAREN = '(' 62 | RPAREN = ')' 63 | COMMA = ',' 64 | STAR = '*' 65 | PLUS = '+' 66 | QUESTION = '?' 67 | 68 | WHITESPACE = (SPACE, NEWLINE, TAB) 69 | def is_whitespace(token): 70 | return token in WHITESPACE 71 | 72 | SYMBOL = 'symbol' 73 | SYMBOLS = (LPAREN, RPAREN, COMMA, STAR, PLUS, QUESTION) 74 | def is_symbol(token): 75 | return token in SYMBOLS 76 | 77 | KEYWORD = 'keyword' 78 | KEYWORDS = (Block.AND, Block.OR, Block.NOT) 79 | def is_keyword(token): 80 | return token in KEYWORDS 81 | 82 | CONSTANT = 'constant' 83 | def is_constant(token): 84 | return token and token[0] != '?' 85 | 86 | class Lexer(object): 87 | def __init__(self, scanner): 88 | self.scanner = scanner 89 | 90 | def gettok(self): 91 | # skip over whitespace and make sure there's still input 92 | while is_whitespace(self.scanner.peek()): 93 | self.scanner.get() 94 | if not self.scanner.peek(): 95 | return None 96 | 97 | # check if the next character is a delimiting symbol 98 | if self.scanner.peek() in SYMBOLS: 99 | symbol = self.scanner.get() 100 | return symbol, symbol 101 | 102 | # read the token up to the next whitespace 103 | token = "" 104 | while self.scanner.peek() not in (None,) + WHITESPACE + SYMBOLS: 105 | token += self.scanner.get() 106 | 107 | # determine token type and return 108 | if is_keyword(token): 109 | return KEYWORD, token 110 | elif is_constant(token): 111 | return CONSTANT, token 112 | else: 113 | raise Exception('Unrecognized token: %s' % token) 114 | 115 | 116 | class Parser(object): 117 | def __init__(self, lexer): 118 | self.lexer = lexer 119 | self.gettok() 120 | 121 | def gettok(self): 122 | got = self.lexer.gettok() 123 | if got: 124 | self.type, self.token = got 125 | else: 126 | self.type, self.token = None, None 127 | 128 | def found(self, type): 129 | if self.type == type: 130 | return True 131 | return False 132 | 133 | def consume(self, type=None): 134 | if type and self.type != type: 135 | raise SyntaxError('expected %s, got %s' % (type, self.type)) 136 | token = self.token 137 | self.gettok() 138 | return token 139 | 140 | # pattern = expr* 141 | def pattern(self): 142 | exprs = [] 143 | while self.token: 144 | try: 145 | exprs.append(self.expr()) 146 | except: 147 | break 148 | return Pattern(exprs) 149 | 150 | # expr = CONSTANT | "?" binding | ("and" | "or" | "not") "(" block ")" 151 | def expr(self): 152 | if self.found(CONSTANT): 153 | return Constant(self.consume()) 154 | elif self.found(QUESTION): 155 | self.gettok() 156 | return self.binding() 157 | elif self.found(KEYWORD): 158 | which = self.consume() 159 | self.consume(LPAREN) 160 | block = self.block() 161 | self.consume(RPAREN) 162 | return Block(block, which) 163 | else: 164 | raise SyntaxError('expr cannot start with %s' % self.token) 165 | 166 | # binding = ("*" | "+" | "?")? CONST 167 | def binding(self): 168 | type = None 169 | if self.found(STAR) or self.found(PLUS) or self.found(QUESTION): 170 | type = self.consume() 171 | const = self.consume(CONSTANT) 172 | return Binding(const, type) 173 | 174 | # block = pattern [, pattern]* 175 | def block(self): 176 | patterns = [self.pattern()] 177 | while self.found(COMMA): 178 | self.gettok() 179 | patterns.append(self.pattern()) 180 | return patterns 181 | 182 | def parse(pattern): 183 | p = Parser(Lexer(Scanner(pattern))) 184 | return p.pattern() 185 | 186 | -------------------------------------------------------------------------------- /paip/tests/test_gps.py: -------------------------------------------------------------------------------- 1 | from paip import gps 2 | import unittest 3 | 4 | 5 | throw = {'action': 'throw baseball', 6 | 'preconds': ['have baseball', 'arm up'], 7 | 'add': ['arm down', 'baseball in air', 'throwing baseball'], 8 | 'delete': ['have baseball', 'arm up']} 9 | 10 | raise_arm = {'action': 'raise arm', 11 | 'preconds': ['arm down'], 12 | 'add': ['arm up', 'raising arm'], 13 | 'delete': ['arm down']} 14 | 15 | grab_baseball = {'action': 'grab baseball', 16 | 'preconds': ['hand empty', 'arm down'], 17 | 'add': ['have baseball', 'grabbing baseball'], 18 | 'delete': ['hand empty']} 19 | 20 | drink_beer = {'action': 'drink beer', 21 | 'preconds': ['arm down', 'hand empty'], 22 | 'add': ['satisfied', 'drinking beer'], 23 | 'delete': []} 24 | 25 | ops = [throw, raise_arm, grab_baseball, drink_beer] 26 | 27 | 28 | class TestApplyOperator(unittest.TestCase): 29 | def test_apply_operator_preconds_satisfied(self): 30 | current = ['have baseball', 'arm up', 'have food'] 31 | expected = ['arm down', 32 | 'baseball in air', 33 | 'have food', 34 | 'throwing baseball'] 35 | goal = 'baseball in air' 36 | final = gps.apply_operator(throw, current, ops, goal, []) 37 | self.assertEqual(set(final), set(expected)) 38 | 39 | def test_apply_operator_recurse(self): 40 | current = ['hand empty', 'arm down', 'have food'] 41 | expected = ['arm down', 42 | 'baseball in air', 43 | 'have food', 44 | 'grabbing baseball', 45 | 'raising arm', 46 | 'throwing baseball'] 47 | goal = 'baseball in air' 48 | final = gps.apply_operator(throw, current, ops, goal, []) 49 | self.assertEqual(set(final), set(expected)) 50 | 51 | def test_apply_operator_recurse_fail(self): 52 | current = ['hand empty', 'have food', 'arm up'] 53 | goal = 'baseball in air' 54 | self.assertFalse(gps.apply_operator(throw, current, ops, goal, [])) 55 | 56 | 57 | class TestAchieve(unittest.TestCase): 58 | def test_achieve_already_done(self): 59 | expected = ['hand empty', 'arm down'] 60 | final = gps.achieve(expected, ops, 'arm down', []) 61 | self.assertEqual(set(expected), set(final)) 62 | 63 | def test_achieve_prevent_loop(self): 64 | current = ['hand empty', 'arm down'] 65 | goal = 'baseball in air' 66 | stack = ['baseball in air', 'levitate'] 67 | self.assertFalse(gps.achieve(current, ops, goal, stack)) 68 | 69 | def test_achieve(self): 70 | current = ['hand empty', 'arm down'] 71 | goal = 'baseball in air' 72 | expected = ['arm down', 73 | 'baseball in air', 74 | 'grabbing baseball', 75 | 'raising arm', 76 | 'throwing baseball'] 77 | final = gps.achieve(current, ops, goal, []) 78 | self.assertEqual(set(expected), set(final)) 79 | 80 | def test_achieve_try_another_op(self): 81 | current = ['hand empty', 'arm down'] 82 | levitate_baseball = {'action': 'levitate baseball', 83 | 'preconds': ['magic'], 84 | 'add': ['baseball in air'], 85 | 'delete': []} 86 | goal = 'baseball in air' 87 | new_ops = [levitate_baseball] + ops 88 | final = gps.achieve(current, new_ops, goal, []) 89 | expected = ['arm down', 90 | 'baseball in air', 91 | 'grabbing baseball', 92 | 'raising arm', 93 | 'throwing baseball'] 94 | self.assertEqual(set(expected), set(final)) 95 | 96 | 97 | class TestAchieveAll(unittest.TestCase): 98 | def test_achieve_all(self): 99 | current = ['hand empty', 'arm down'] 100 | goals = ['satisfied', 'baseball in air'] 101 | expected = ['satisfied', 102 | 'arm down', 103 | 'baseball in air', 104 | 'grabbing baseball', 105 | 'raising arm', 106 | 'throwing baseball', 107 | 'drinking beer'] 108 | final = gps.achieve_all(current, ops, goals, []) 109 | self.assertEqual(set(expected), set(final)) 110 | 111 | def test_achieve_all_one_impossible(self): 112 | current = ['hand empty', 'arm down'] 113 | goals = ['satisfied', 'baseball in air', 'awesome'] 114 | self.assertFalse(gps.achieve_all(current, ops, goals, [])) 115 | 116 | def test_achieve_all_clobbers_sibling(self): 117 | current = ['hand empty', 'arm down'] 118 | goals = ['baseball in air', 'satisfied'] 119 | self.assertFalse(gps.achieve_all(current, ops, goals, [])) 120 | 121 | 122 | class TestGps(unittest.TestCase): 123 | def test_gps(self): 124 | current = ['hand empty', 'arm down'] 125 | goals = ['satisfied', 'baseball in air'] 126 | expected = ['Executing drink beer', 127 | 'Executing grab baseball', 128 | 'Executing raise arm', 129 | 'Executing throw baseball'] 130 | final = gps.gps(current, goals, ops) 131 | self.assertEqual(final, expected) # order matters this time 132 | 133 | def test_gps_fail(self): 134 | current = ['hand empty', 'arm down'] 135 | goals = ['satisfied', 'baseball in air', 'awesome'] 136 | self.assertFalse(gps.gps(current, goals, ops)) 137 | -------------------------------------------------------------------------------- /paip/gps.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | The **General Problem Solver** is a framework for applying *means-ends analysis* 5 | to solve problems that can be specified by a list of initial states, a list of 6 | goal states, and a list of operators that induce state transitions. 7 | 8 | Each operator is specified by an action name, a list of precondition states that 9 | must hold before the operator is applied, a list of states that will hold after 10 | the operator is applied (the *add-list*), and a list of states that will no 11 | longer hold after the operator is applied (the *delete-list*). To achieve a 12 | goal state, GPS uses means-ends analysis: each operator is examined to find one 13 | that contains the goal state in its add-list (it looks for an *appropriate* 14 | operator). It then tries to achieve all of that operator's precondition states. 15 | If not all of the preconditions can be achieved (the operator is not 16 | *applicable*), then GPS looks for another appropriate operator. If none exists, 17 | then the goal can't be achieved. When all of the goal states have been 18 | achieved, the problem is solved. 19 | 20 | The following programs demonstrate using GPS to solve some famous AI problems: 21 | 22 | - [Monkey and Bananas](examples/gps/monkeys.html) 23 | - [Blocks World](examples/gps/blocks.html) 24 | - [Driving to school](examples/gps/school.html) 25 | 26 | This implementation is inspired by chapter 4 of "Paradigms of Artificial 27 | Intelligence Programming" by Peter Norvig. 28 | """ 29 | 30 | ## General Problem Solver 31 | 32 | def gps(initial_states, goal_states, operators): 33 | """ 34 | Find a sequence of operators that will achieve all of the goal states. 35 | 36 | Returns a list of actions that will achieve all of the goal states, or 37 | None if no such sequence exists. Each operator is specified by an 38 | action name, a list of preconditions, an add-list, and a delete-list. 39 | """ 40 | 41 | # To keep track of which operators have been applied, we add additional 42 | # 'executing ...' states to each operator's add-list. These will never be 43 | # deleted by another operator, so when the problem is solved we can find 44 | # them in the list of current states. 45 | prefix = 'Executing ' 46 | for operator in operators: 47 | operator['add'].append(prefix + operator['action']) 48 | 49 | final_states = achieve_all(initial_states, operators, goal_states, []) 50 | if not final_states: 51 | return None 52 | return [state for state in final_states if state.startswith(prefix)] 53 | 54 | 55 | ## Achieving subgoals 56 | 57 | def achieve_all(states, ops, goals, goal_stack): 58 | """ 59 | Achieve each state in goals and make sure they still hold at the end. 60 | 61 | The goal stack keeps track of our recursion: which preconditions are we 62 | trying to satisfy by achieving the specified goals? 63 | """ 64 | 65 | # We try to achieve each goal in the order they are given. If any one 66 | # goal state cannot be achieved, then the problem cannot be solved. 67 | for goal in goals: 68 | states = achieve(states, ops, goal, goal_stack) 69 | if not states: 70 | return None 71 | 72 | # We must ensure that we haven't removed a goal state in the process of 73 | # solving other states--having done so is called the "prerequisite clobbers 74 | # sibling goal problem". 75 | for goal in goals: 76 | if goal not in states: 77 | return None 78 | 79 | return states 80 | 81 | 82 | def achieve(states, operators, goal, goal_stack): 83 | """ 84 | Achieve the goal state using means-ends analysis. 85 | 86 | Identifies an appropriate and applicable operator--one that contains the 87 | goal state in its add-list and has all its preconditions satisified. 88 | Applies the operator and returns the result. Returns None if no such 89 | operator is found or infinite recursion is detected in the goal stack. 90 | """ 91 | 92 | debug(len(goal_stack), 'Achieving: %s' % goal) 93 | 94 | # Let's check to see if the state already holds before we do anything. 95 | if goal in states: 96 | return states 97 | 98 | # Prevent going in circles: look through the goal stack to see if the 99 | # specified goal appears there. If so, then we are indirectly trying to 100 | # achieve goal while already in the process of achieving it. For example, 101 | # while trying to achieve state A, we try to achieve state B--a precondition 102 | # for applying an appropriate operator. However, to achieve B, we try to 103 | # satisfy the preconditions for another operator that contains A in its 104 | # preconditions. 105 | if goal in goal_stack: 106 | return None 107 | 108 | for op in operators: 109 | # Is op appropriate? Look through its add-list to see if goal is there. 110 | if goal not in op['add']: 111 | continue 112 | # Is op applicable? Try to apply it--if one of its preconditions cannot 113 | # be satisifed, then it will return None. 114 | result = apply_operator(op, states, operators, goal, goal_stack) 115 | if result: 116 | return result 117 | 118 | 119 | ## Using operators 120 | 121 | def apply_operator(operator, states, ops, goal, goal_stack): 122 | """ 123 | Applies operator and returns the resulting states. 124 | 125 | Achieves all of operator's preconditions and returns the states that hold 126 | after processing its add-list and delete-list. If any of its preconditions 127 | cannot be satisfied, returns None. 128 | """ 129 | 130 | debug(len(goal_stack), 'Consider: %s' % operator['action']) 131 | 132 | # Satisfy all of operator's preconditions. 133 | result = achieve_all(states, ops, operator['preconds'], [goal] + goal_stack) 134 | if not result: 135 | return None 136 | 137 | debug(len(goal_stack), 'Action: %s' % operator['action']) 138 | 139 | # Merge the old states with operator's add-list, filtering out delete-list. 140 | add_list, delete_list = operator['add'], operator['delete'] 141 | return [state for state in result if state not in delete_list] + add_list 142 | 143 | 144 | ## Helper functions 145 | 146 | import logging 147 | 148 | def debug(level, msg): 149 | logging.debug(' %s %s' % (level * ' ', msg)) 150 | -------------------------------------------------------------------------------- /paip/examples/emycin/mycin.py: -------------------------------------------------------------------------------- 1 | """ 2 | Mycin: a medical expert system. 3 | 4 | This is a small example of an expert system that uses the 5 | [Emycin](../../emycin.html) shell. It defines a few contexts, parameters, and 6 | rules, and presents a rudimentary user interface to collect data about an 7 | infection in order to determine the identity of the infecting organism. 8 | 9 | In a more polished system, we could: 10 | 11 | - define and use a domain-specific language for the expert; 12 | - present a more polished interface, perhaps a GUI, for user interaction; 13 | - offer a data serialization mechanism to save state between sessions. 14 | 15 | This implementation comes from chapter 16 of Peter Norvig's "Paradigms of 16 | Artificial Intelligence Programming. 17 | 18 | """ 19 | 20 | ### Utility functions 21 | 22 | def eq(x, y): 23 | """Function for testing value equality.""" 24 | return x == y 25 | 26 | def boolean(string): 27 | """ 28 | Function for reading True or False from a string. Raises an error if the 29 | string is not True or False. 30 | """ 31 | if string == 'True': 32 | return True 33 | if string == 'False': 34 | return False 35 | raise ValueError('bool must be True or False') 36 | 37 | 38 | ### Setting up initial data 39 | 40 | # Here we define the contexts, parameters, and rules for our system. This is 41 | # the job of the expert, and in a more polished system, we would define and use 42 | # a domain-specific language to make this easier. 43 | 44 | def define_contexts(sh): 45 | # Patient and Culture have some initial goals--parameters that should be 46 | # collected before reasoning begins. This might be useful in some domains; 47 | # for example, this might be legally required in a medical system. 48 | sh.define_context(Context('patient', ['name', 'sex', 'age'])) 49 | sh.define_context(Context('culture', ['site', 'days-old'])) 50 | 51 | # Finding the identity of the organism is our goal. 52 | sh.define_context(Context('organism', goals=['identity'])) 53 | 54 | def define_params(sh): 55 | # Patient params 56 | sh.define_param(Parameter('name', 'patient', cls=str, ask_first=True)) 57 | sh.define_param(Parameter('sex', 'patient', enum=['M', 'F'], ask_first=True)) 58 | sh.define_param(Parameter('age', 'patient', cls=int, ask_first=True)) 59 | sh.define_param(Parameter('burn', 'patient', 60 | enum=['no', 'mild', 'serious'], ask_first=True)) 61 | sh.define_param(Parameter('compromised-host', 'patient', cls=boolean)) 62 | 63 | # Culture params 64 | sh.define_param(Parameter('site', 'culture', enum=['blood'], ask_first=True)) 65 | sh.define_param(Parameter('days-old', 'culture', cls=int, ask_first=True)) 66 | 67 | # Organism params 68 | organisms = ['pseudomonas', 'klebsiella', 'enterobacteriaceae', 69 | 'staphylococcus', 'bacteroides', 'streptococcus'] 70 | sh.define_param(Parameter('identity', 'organism', enum=organisms, ask_first=True)) 71 | sh.define_param(Parameter('gram', 'organism', 72 | enum=['acid-fast', 'pos', 'neg'], ask_first=True)) 73 | sh.define_param(Parameter('morphology', 'organism', enum=['rod', 'coccus'])) 74 | sh.define_param(Parameter('aerobicity', 'organism', enum=['aerobic', 'anaerobic'])) 75 | sh.define_param(Parameter('growth-conformation', 'organism', 76 | enum=['chains', 'pairs', 'clumps'])) 77 | 78 | def define_rules(sh): 79 | sh.define_rule(Rule(52, 80 | [('site', 'culture', eq, 'blood'), 81 | ('gram', 'organism', eq, 'neg'), 82 | ('morphology', 'organism', eq, 'rod'), 83 | ('burn', 'patient', eq, 'serious')], 84 | [('identity', 'organism', eq, 'pseudomonas')], 85 | 0.4)) 86 | sh.define_rule(Rule(71, 87 | [('gram', 'organism', eq, 'pos'), 88 | ('morphology', 'organism', eq, 'coccus'), 89 | ('growth-conformation', 'organism', eq, 'clumps')], 90 | [('identity', 'organism', eq, 'staphylococcus')], 91 | 0.7)) 92 | sh.define_rule(Rule(73, 93 | [('site', 'culture', eq, 'blood'), 94 | ('gram', 'organism', eq, 'neg'), 95 | ('morphology', 'organism', eq, 'rod'), 96 | ('aerobicity', 'organism', eq, 'anaerobic')], 97 | [('identity', 'organism', eq, 'bacteroides')], 98 | 0.9)) 99 | sh.define_rule(Rule(75, 100 | [('gram', 'organism', eq, 'neg'), 101 | ('morphology', 'organism', eq, 'rod'), 102 | ('compromised-host', 'patient', eq, True)], 103 | [('identity', 'organism', eq, 'pseudomonas')], 104 | 0.6)) 105 | sh.define_rule(Rule(107, 106 | [('gram', 'organism', eq, 'neg'), 107 | ('morphology', 'organism', eq, 'rod'), 108 | ('aerobicity', 'organism', eq, 'aerobic')], 109 | [('identity', 'organism', eq, 'enterobacteriaceae')], 110 | 0.8)) 111 | sh.define_rule(Rule(165, 112 | [('gram', 'organism', eq, 'pos'), 113 | ('morphology', 'organism', eq, 'coccus'), 114 | ('growth-conformation', 'organism', eq, 'chains')], 115 | [('identity', 'organism', eq, 'streptococcus')], 116 | 0.7)) 117 | 118 | 119 | ### Running the system 120 | 121 | import logging 122 | from paip.emycin import Parameter, Context, Rule, Shell 123 | 124 | def report_findings(findings): 125 | for inst, result in findings.items(): 126 | print 'Findings for %s-%d:' % (inst[0], inst[1]) 127 | for param, vals in result.items(): 128 | possibilities = ['%s: %f' % (val[0], val[1]) for val in vals.items()] 129 | print '%s: %s' % (param, ', '.join(possibilities)) 130 | 131 | def main(): 132 | sh = Shell() 133 | define_contexts(sh) 134 | define_params(sh) 135 | define_rules(sh) 136 | report_findings(sh.execute(['patient', 'culture', 'organism'])) 137 | -------------------------------------------------------------------------------- /paip/examples/eliza/eliza.py: -------------------------------------------------------------------------------- 1 | import json 2 | import sys 3 | from paip import eliza 4 | 5 | rules = { 6 | "?*x hello ?*y": [ 7 | "How do you do. Please state your problem." 8 | ], 9 | "?*x computer ?*y": [ 10 | "Do computers worry you?", 11 | "What do you think about machines?", 12 | "Why do you mention computers?", 13 | "What do you think machines have to do with your problem?", 14 | ], 15 | "?*x name ?*y": [ 16 | "I am not interested in names", 17 | ], 18 | "?*x sorry ?*y": [ 19 | "Please don't apologize", 20 | "Apologies are not necessary", 21 | "What feelings do you have when you apologize", 22 | ], 23 | "?*x I remember ?*y": [ 24 | "Do you often think of ?y?", 25 | "Does thinking of ?y bring anything else to mind?", 26 | "What else do you remember?", 27 | "Why do you recall ?y right now?", 28 | "What in the present situation reminds you of ?y?", 29 | "What is the connection between me and ?y?", 30 | ], 31 | "?*x do you remember ?*y": [ 32 | "Did you think I would forget ?y?", 33 | "Why do you think I should recall ?y now?", 34 | "What about ?y?", 35 | "You mentioned ?y", 36 | ], 37 | "?*x I want ?*y": [ 38 | "What would it mean if you got ?y?", 39 | "Why do you want ?y?", 40 | "Suppose you got ?y soon." 41 | ], 42 | "?*x if ?*y": [ 43 | "Do you really think it's likely that ?y?", 44 | "Do you wish that ?y?", 45 | "What do you think about ?y?", 46 | "Really--if ?y?" 47 | ], 48 | "?*x I dreamt ?*y": [ 49 | "How do you feel about ?y in reality?", 50 | ], 51 | "?*x dream ?*y": [ 52 | "What does this dream suggest to you?", 53 | "Do you dream often?", 54 | "What persons appear in your dreams?", 55 | "Don't you believe that dream has to do with your problem?", 56 | ], 57 | "?*x my mother ?*y": [ 58 | "Who else in your family ?y?", 59 | "Tell me more about your family", 60 | ], 61 | "?*x my father ?*y": [ 62 | "Your father?", 63 | "Does he influence you strongly?", 64 | "What else comes to mind when you think of your father?", 65 | ], 66 | "?*x I am glad ?*y": [ 67 | "How have I helped you to be ?y?", 68 | "What makes you happy just now?", 69 | "Can you explain why you are suddenly ?y?", 70 | ], 71 | "?*x I am sad ?*y": [ 72 | "I am sorry to hear you are depressed", 73 | "I'm sure it's not pleasant to be sad", 74 | ], 75 | "?*x are like ?*y": [ 76 | "What resemblence do you see between ?x and ?y?", 77 | ], 78 | "?*x is like ?*y": [ 79 | "In what way is it that ?x is like ?y?", 80 | "What resemblence do you see?", 81 | "Could there really be some connection?", 82 | "How?", 83 | ], 84 | "?*x alike ?*y": [ 85 | "In what way?", 86 | "What similarities are there?", 87 | ], 88 | "?* same ?*y": [ 89 | "What other connections do you see?", 90 | ], 91 | "?*x no ?*y": [ 92 | "Why not?", 93 | "You are being a bit negative.", 94 | "Are you saying 'No' just to be negative?" 95 | ], 96 | "?*x I was ?*y": [ 97 | "Were you really?", 98 | "Perhaps I already knew you were ?y.", 99 | "Why do you tell me you were ?y now?" 100 | ], 101 | "?*x was I ?*y": [ 102 | "What if you were ?y?", 103 | "Do you think you were ?y?", 104 | "What would it mean if you were ?y?", 105 | ], 106 | "?*x I am ?*y": [ 107 | "In what way are you ?y?", 108 | "Do you want to be ?y?", 109 | ], 110 | "?*x am I ?*y": [ 111 | "Do you believe you are ?y?", 112 | "Would you want to be ?y?", 113 | "You wish I would tell you you are ?y?", 114 | "What would it mean if you were ?y?", 115 | ], 116 | "?*x am ?*y": [ 117 | "Why do you say 'AM?'", 118 | "I don't understand that" 119 | ], 120 | "?*x are you ?*y": [ 121 | "Why are you interested in whether I am ?y or not?", 122 | "Would you prefer if I weren't ?y?", 123 | "Perhaps I am ?y in your fantasies", 124 | ], 125 | "?*x you are ?*y": [ 126 | "What makes you think I am ?y?", 127 | ], 128 | "?*x because ?*y": [ 129 | "Is that the real reason?", 130 | "What other reasons might there be?", 131 | "Does that reason seem to explain anything else?", 132 | ], 133 | "?*x were you ?*y": [ 134 | "Perhaps I was ?y?", 135 | "What do you think?", 136 | "What if I had been ?y?", 137 | ], 138 | "?*x I can't ?*y": [ 139 | "Maybe you could ?y now", 140 | "What if you could ?y?", 141 | ], 142 | "?*x I feel ?*y": [ 143 | "Do you often feel ?y?" 144 | ], 145 | "?*x I felt ?*y": [ 146 | "What other feelings do you have?" 147 | ], 148 | "?*x I ?*y you ?*z": [ 149 | "Perhaps in your fantasy we ?y each other", 150 | ], 151 | "?*x why don't you ?*y": [ 152 | "Should you ?y yourself?", 153 | "Do you believe I don't ?y?", 154 | "Perhaps I will ?y in good time", 155 | ], 156 | "?*x yes ?*y": [ 157 | "You seem quite positive", 158 | "You are sure?", 159 | "I understand", 160 | ], 161 | "?*x someone ?*y": [ 162 | "Can you be more specific?", 163 | ], 164 | "?*x everyone ?*y": [ 165 | "Surely not everyone", 166 | "Can you think of anyone in particular?", 167 | "Who, for example?", 168 | "You are thinking of a special person", 169 | ], 170 | "?*x always ?*y": [ 171 | "Can you think of a specific example?", 172 | "When?", 173 | "What incident are you thinking of?", 174 | "Really--always?", 175 | ], 176 | "?*x what ?*y": [ 177 | "Why do you ask?", 178 | "Does that question interest you?", 179 | "What is it you really want to know?", 180 | "What do you think?", 181 | "What comes to your mind when you ask that?", 182 | ], 183 | "?*x perhaps ?*y": [ 184 | "You do not seem quite certain", 185 | ], 186 | "?*x are ?*y": [ 187 | "Did you think they might not be ?y?", 188 | "Possibly they are ?y", 189 | ], 190 | } 191 | 192 | default_responses = [ 193 | "Very interesting", 194 | "I am not sure I understand you fully", 195 | "What does that suggest to you?", 196 | "Please continue", 197 | "Go on", 198 | "Do you feel strongly about discussing such things?", 199 | ] 200 | 201 | def main(): 202 | # We need the rules in a list containing elements of the following form: 203 | # `(input pattern, [output pattern 1, output pattern 2, ...]` 204 | rules_list = [] 205 | for pattern, transforms in rules.items(): 206 | # Remove the punctuation from the pattern to simplify matching. 207 | pattern = eliza.remove_punct(str(pattern.upper())) # kill unicode 208 | transforms = [str(t).upper() for t in transforms] 209 | rules_list.append((pattern, transforms)) 210 | eliza.interact('ELIZA> ', rules_list, map(str.upper, default_responses)) 211 | 212 | if __name__ == '__main__': 213 | main(sys.argv[1:]) 214 | -------------------------------------------------------------------------------- /paip/examples/gps/blocks.py: -------------------------------------------------------------------------------- 1 | from paip.gps import gps 2 | 3 | problem = { 4 | "start": ["space on a", "a on b", "b on c", "c on table", "space on table"], 5 | "finish": ["space on c", "c on b", "b on a", "a on table", "space on table"], 6 | "ops": [ 7 | { 8 | "action": "move a from b to c", 9 | "preconds": [ 10 | "space on a", 11 | "space on c", 12 | "a on b" 13 | ], 14 | "add": [ 15 | "a on c", 16 | "space on b" 17 | ], 18 | "delete": [ 19 | "a on b", 20 | "space on c" 21 | ] 22 | }, 23 | { 24 | "action": "move a from table to b", 25 | "preconds": [ 26 | "space on a", 27 | "space on b", 28 | "a on table" 29 | ], 30 | "add": [ 31 | "a on b" 32 | ], 33 | "delete": [ 34 | "a on table", 35 | "space on b" 36 | ] 37 | }, 38 | { 39 | "action": "move a from b to table", 40 | "preconds": [ 41 | "space on a", 42 | "space on table", 43 | "a on b" 44 | ], 45 | "add": [ 46 | "a on table", 47 | "space on b" 48 | ], 49 | "delete": [ 50 | "a on b" 51 | ] 52 | }, 53 | { 54 | "action": "move a from c to b", 55 | "preconds": [ 56 | "space on a", 57 | "space on b", 58 | "a on c" 59 | ], 60 | "add": [ 61 | "a on b", 62 | "space on c" 63 | ], 64 | "delete": [ 65 | "a on c", 66 | "space on b" 67 | ] 68 | }, 69 | { 70 | "action": "move a from table to c", 71 | "preconds": [ 72 | "space on a", 73 | "space on c", 74 | "a on table" 75 | ], 76 | "add": [ 77 | "a on c" 78 | ], 79 | "delete": [ 80 | "a on table", 81 | "space on c" 82 | ] 83 | }, 84 | { 85 | "action": "move a from c to table", 86 | "preconds": [ 87 | "space on a", 88 | "space on table", 89 | "a on c" 90 | ], 91 | "add": [ 92 | "a on table", 93 | "space on c" 94 | ], 95 | "delete": [ 96 | "a on c" 97 | ] 98 | }, 99 | { 100 | "action": "move b from a to c", 101 | "preconds": [ 102 | "space on b", 103 | "space on c", 104 | "b on a" 105 | ], 106 | "add": [ 107 | "b on c", 108 | "space on a" 109 | ], 110 | "delete": [ 111 | "b on a", 112 | "space on c" 113 | ] 114 | }, 115 | { 116 | "action": "move b from table to a", 117 | "preconds": [ 118 | "space on b", 119 | "space on a", 120 | "b on table" 121 | ], 122 | "add": [ 123 | "b on a" 124 | ], 125 | "delete": [ 126 | "b on table", 127 | "space on a" 128 | ] 129 | }, 130 | { 131 | "action": "move b from a to table", 132 | "preconds": [ 133 | "space on b", 134 | "space on table", 135 | "b on a" 136 | ], 137 | "add": [ 138 | "b on table", 139 | "space on a" 140 | ], 141 | "delete": [ 142 | "b on a" 143 | ] 144 | }, 145 | { 146 | "action": "move b from c to a", 147 | "preconds": [ 148 | "space on b", 149 | "space on a", 150 | "b on c" 151 | ], 152 | "add": [ 153 | "b on a", 154 | "space on c" 155 | ], 156 | "delete": [ 157 | "b on c", 158 | "space on a" 159 | ] 160 | }, 161 | { 162 | "action": "move b from table to c", 163 | "preconds": [ 164 | "space on b", 165 | "space on c", 166 | "b on table" 167 | ], 168 | "add": [ 169 | "b on c" 170 | ], 171 | "delete": [ 172 | "b on table", 173 | "space on c" 174 | ] 175 | }, 176 | { 177 | "action": "move b from c to table", 178 | "preconds": [ 179 | "space on b", 180 | "space on table", 181 | "b on c" 182 | ], 183 | "add": [ 184 | "b on table", 185 | "space on c" 186 | ], 187 | "delete": [ 188 | "b on c" 189 | ] 190 | }, 191 | { 192 | "action": "move c from a to b", 193 | "preconds": [ 194 | "space on c", 195 | "space on b", 196 | "c on a" 197 | ], 198 | "add": [ 199 | "c on b", 200 | "space on a" 201 | ], 202 | "delete": [ 203 | "c on a", 204 | "space on b" 205 | ] 206 | }, 207 | { 208 | "action": "move c from table to a", 209 | "preconds": [ 210 | "space on c", 211 | "space on a", 212 | "c on table" 213 | ], 214 | "add": [ 215 | "c on a" 216 | ], 217 | "delete": [ 218 | "c on table", 219 | "space on a" 220 | ] 221 | }, 222 | { 223 | "action": "move c from a to table", 224 | "preconds": [ 225 | "space on c", 226 | "space on table", 227 | "c on a" 228 | ], 229 | "add": [ 230 | "c on table", 231 | "space on a" 232 | ], 233 | "delete": [ 234 | "c on a" 235 | ] 236 | }, 237 | { 238 | "action": "move c from b to a", 239 | "preconds": [ 240 | "space on c", 241 | "space on a", 242 | "c on b" 243 | ], 244 | "add": [ 245 | "c on a", 246 | "space on b" 247 | ], 248 | "delete": [ 249 | "c on b", 250 | "space on a" 251 | ] 252 | }, 253 | { 254 | "action": "move c from table to b", 255 | "preconds": [ 256 | "space on c", 257 | "space on b", 258 | "c on table" 259 | ], 260 | "add": [ 261 | "c on b" 262 | ], 263 | "delete": [ 264 | "c on table", 265 | "space on b" 266 | ] 267 | }, 268 | { 269 | "action": "move c from b to table", 270 | "preconds": [ 271 | "space on c", 272 | "space on table", 273 | "c on b" 274 | ], 275 | "add": [ 276 | "c on table", 277 | "space on b" 278 | ], 279 | "delete": [ 280 | "c on b" 281 | ] 282 | } 283 | ] 284 | } 285 | 286 | def main(): 287 | start = problem['start'] 288 | finish = problem['finish'] 289 | ops = problem['ops'] 290 | for action in gps(start, finish, ops): 291 | print action 292 | 293 | if __name__ == '__main__': 294 | main() 295 | -------------------------------------------------------------------------------- /paip/eliza.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | **Eliza** is a pattern-matching automated psychiatrist. Given a set of rules 5 | in the form of input/output patterns, Eliza will attempt to recognize user input 6 | phrases and generate relevant psychobabble responses. 7 | 8 | Each rule is specified by an input pattern and a list of output patterns. A 9 | pattern is a sentence consisting of space-separated words and variables. Input 10 | pattern variables come in two forms: single variables and segment variables; 11 | single variables (which take the form `?x`) match a single word, while segment 12 | variables (which take the form `?*x`) can match a phrase. Output pattern 13 | variables are only single variables. The variable names contained in an input 14 | pattern should be the same as those in the corresponding output pattern, and 15 | each segment variable `?*x` in an input pattern corresponds to the single 16 | variable `?x` in the output pattern. 17 | 18 | The conversation proceeds by reading a sentence from the user, searching through 19 | the rules to find an input pattern that matches, replacing variables in the 20 | output pattern, and printing the results to the user. 21 | 22 | For examples of using this scheme, see the following programs: 23 | 24 | - [Eliza](examples/eliza/eliza.html) 25 | - [Automated technical support system](examples/eliza/support.html) 26 | 27 | This implementation is inspired by Chapter 5 of "Paradigms of Artificial 28 | Intelligence Programming" by Peter Norvig. 29 | """ 30 | 31 | import random 32 | import string 33 | 34 | ## Talking to the computer 35 | 36 | def interact(prompt, rules, default_responses): 37 | """Have a conversation with a user.""" 38 | # Read a line, process it, and print the results until no input remains. 39 | while True: 40 | try: 41 | # Remove the punctuation from the input and convert to upper-case 42 | # to simplify matching. 43 | input = remove_punct(raw_input(prompt).upper()) 44 | if not input: 45 | continue 46 | except: 47 | break 48 | print respond(rules, input, default_responses) 49 | 50 | 51 | def respond(rules, input, default_responses): 52 | """Respond to an input sentence according to the given rules.""" 53 | 54 | input = input.split() # match_pattern expects a list of tokens 55 | 56 | # Look through rules and find input patterns that matches the input. 57 | matching_rules = [] 58 | for pattern, transforms in rules: 59 | pattern = pattern.split() 60 | replacements = match_pattern(pattern, input) 61 | if replacements: 62 | matching_rules.append((transforms, replacements)) 63 | 64 | # When rules are found, choose one and one of its responses at random. 65 | # If no rule applies, we use the default rule. 66 | if matching_rules: 67 | responses, replacements = random.choice(matching_rules) 68 | response = random.choice(responses) 69 | else: 70 | replacements = {} 71 | response = random.choice(default_responses) 72 | 73 | # Replace the variables in the output pattern with the values matched from 74 | # the input string. 75 | for variable, replacement in replacements.items(): 76 | replacement = ' '.join(switch_viewpoint(replacement)) 77 | if replacement: 78 | response = response.replace('?' + variable, replacement) 79 | 80 | return response 81 | 82 | 83 | ## Pattern matching 84 | 85 | def match_pattern(pattern, input, bindings=None): 86 | """ 87 | Determine if the input string matches the given pattern. 88 | 89 | Expects pattern and input to be lists of tokens, where each token is a word 90 | or a variable. 91 | 92 | Returns a dictionary containing the bindings of variables in the input 93 | pattern to values in the input string, or False when the input doesn't match 94 | the pattern. 95 | """ 96 | 97 | # Check to see if matching failed before we got here. 98 | if bindings is False: 99 | return False 100 | 101 | # When the pattern and the input are identical, we have a match, and 102 | # no more bindings need to be found. 103 | if pattern == input: 104 | return bindings 105 | 106 | bindings = bindings or {} 107 | 108 | # Match input and pattern according to their types. 109 | if is_segment(pattern): 110 | token = pattern[0] # segment variable is the first token 111 | var = token[2:] # segment variable is of the form ?*x 112 | return match_segment(var, pattern[1:], input, bindings) 113 | elif is_variable(pattern): 114 | var = pattern[1:] # single variables are of the form ?foo 115 | return match_variable(var, [input], bindings) 116 | elif contains_tokens(pattern) and contains_tokens(input): 117 | # Recurse: 118 | # try to match the first tokens of both pattern and input. The bindings 119 | # that result are used to match the remainder of both lists. 120 | return match_pattern(pattern[1:], 121 | input[1:], 122 | match_pattern(pattern[0], input[0], bindings)) 123 | else: 124 | return False 125 | 126 | 127 | def match_segment(var, pattern, input, bindings, start=0): 128 | """ 129 | Match the segment variable against the input. 130 | 131 | pattern and input should be lists of tokens. 132 | 133 | Looks for a substring of input that begins at start and is immediately 134 | followed by the first word in pattern. If such a substring exists, 135 | matching continues recursively and the resulting bindings are returned; 136 | otherwise returns False. 137 | """ 138 | 139 | # If there are no words in pattern following var, we can just match var 140 | # to the remainder of the input. 141 | if not pattern: 142 | return match_variable(var, input, bindings) 143 | 144 | # Get the segment boundary word and look for the first occurrence in 145 | # the input starting from index start. 146 | word = pattern[0] 147 | try: 148 | pos = start + input[start:].index(word) 149 | except ValueError: 150 | # When the boundary word doesn't appear in the input, no match. 151 | return False 152 | 153 | # Match the located substring to the segment variable and recursively 154 | # pattern match using the resulting bindings. 155 | var_match = match_variable(var, input[:pos], dict(bindings)) 156 | match = match_pattern(pattern, input[pos:], var_match) 157 | 158 | # If pattern matching fails with this substring, try a longer one. 159 | if not match: 160 | return match_segment(var, pattern, input, bindings, start + 1) 161 | 162 | return match 163 | 164 | 165 | def match_variable(var, replacement, bindings): 166 | """Bind the input to the variable and update the bindings.""" 167 | binding = bindings.get(var) 168 | if not binding: 169 | # The variable isn't yet bound. 170 | bindings.update({var: replacement}) 171 | return bindings 172 | if replacement == bindings[var]: 173 | # The variable is already bound to that input. 174 | return bindings 175 | 176 | # The variable is already bound, but not to that input--fail. 177 | return False 178 | 179 | 180 | ## Pattern matching utilities 181 | 182 | def contains_tokens(pattern): 183 | """Test if pattern is a list of subpatterns.""" 184 | return type(pattern) is list and len(pattern) > 0 185 | 186 | 187 | def is_variable(pattern): 188 | """Test if pattern is a single variable.""" 189 | return (type(pattern) is str 190 | and pattern[0] == '?' 191 | and len(pattern) > 1 192 | and pattern[1] != '*' 193 | and pattern[1] in string.letters 194 | and ' ' not in pattern) 195 | 196 | 197 | def is_segment(pattern): 198 | """Test if pattern begins with a segment variable.""" 199 | return (type(pattern) is list 200 | and pattern 201 | and len(pattern[0]) > 2 202 | and pattern[0][0] == '?' 203 | and pattern[0][1] == '*' 204 | and pattern[0][2] in string.letters 205 | and ' ' not in pattern[0]) 206 | 207 | 208 | ## Translating user input 209 | 210 | def replace(word, replacements): 211 | """Replace word with rep if (word, rep) occurs in replacements.""" 212 | for old, new in replacements: 213 | if word == old: 214 | return new 215 | return word 216 | 217 | 218 | def switch_viewpoint(words): 219 | """Swap some common pronouns for interacting with a robot.""" 220 | replacements = [('I', 'YOU'), 221 | ('YOU', 'I'), 222 | ('ME', 'YOU'), 223 | ('MY', 'YOUR'), 224 | ('AM', 'ARE'), 225 | ('ARE', 'AM')] 226 | return [replace(word, replacements) for word in words] 227 | 228 | 229 | def remove_punct(string): 230 | """Remove common punctuation marks.""" 231 | if string.endswith('?'): 232 | string = string[:-1] 233 | return (string.replace(',', '') 234 | .replace('.', '') 235 | .replace(';', '') 236 | .replace('!', '')) 237 | -------------------------------------------------------------------------------- /paip/tests/test_search.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import random 3 | import paip.search as search 4 | 5 | 6 | class Graph(object): 7 | def __init__(self, data, neighbors=None): 8 | self.data = data 9 | self.neighbors = neighbors or [] 10 | 11 | def add_neighbor(self, node): 12 | self.neighbors.append(node) 13 | 14 | def __repr__(self): 15 | return str(self.data) 16 | 17 | def __eq__(self, other): 18 | return self.data == other.data 19 | 20 | 21 | class SearchTest(unittest.TestCase): 22 | def path_tracking_test(self, alg, start, finish, expected_path): 23 | path = [] 24 | def done(node): 25 | return node == finish 26 | def successors(node): 27 | path.append(node) 28 | return node.neighbors 29 | found = alg(start, done, successors) 30 | self.assertEqual(found, finish) 31 | self.assertEqual(path, expected_path) 32 | 33 | 34 | # ---------------------------------------------------------------------------- 35 | ## Tree search tests 36 | 37 | # A 38 | # B/ \C 39 | # D/ \E \F 40 | # /G |H \I \J \K 41 | # L/ \M \N \O 42 | # P/ QRST \UV 43 | 44 | a = Graph('a') 45 | b = Graph('b') 46 | c = Graph('c') 47 | d = Graph('d') 48 | e = Graph('e') 49 | f = Graph('f') 50 | g = Graph('g') 51 | h = Graph('h') 52 | i = Graph('i') 53 | j = Graph('j') 54 | k = Graph('k') 55 | l = Graph('l') 56 | m = Graph('m') 57 | n = Graph('n') 58 | o = Graph('o') 59 | p = Graph('p') 60 | q = Graph('q') 61 | r = Graph('r') 62 | s = Graph('s') 63 | t = Graph('t') 64 | u = Graph('u') 65 | v = Graph('v') 66 | 67 | a.neighbors = [b, c] 68 | b.neighbors = [d, e] 69 | c.neighbors = [f] 70 | e.neighbors = [g, h, i] 71 | f.neighbors = [j, k] 72 | g.neighbors = [l, m] 73 | h.neighbors = [n] 74 | j.neighbors = [o] 75 | l.neighbors = [p] 76 | m.neighbors = [q, r, s, t] 77 | n.neighbors = [u, v] 78 | 79 | 80 | class TreeSearchTest(SearchTest): 81 | def test_dfs(self): 82 | expected_path = [a, b, d, e, g, l, p, m, q, r] 83 | self.path_tracking_test(search.dfs, a, s, expected_path) 84 | 85 | def test_bfs(self): 86 | expected_path = [a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r] 87 | self.path_tracking_test(search.bfs, a, s, expected_path) 88 | 89 | def test_best_first_search(self): 90 | def cost(n): 91 | return abs(ord(n.data) - ord(s.data)) 92 | def alg(start, done, next): 93 | return search.best_first_search(start, done, next, cost) 94 | expected_path = [a, c, f, k, j, o, b, e, i, h, n, u, v, g, m] 95 | self.path_tracking_test(alg, a, s, expected_path) 96 | 97 | def test_beam_search(self): 98 | def cost(n): 99 | return -ord(n.data) 100 | def alg(start, done, next): 101 | return search.beam_search(start, done, next, cost, 3) 102 | expected_path = [a, c, f, k, j, o, b, e, i, h, n, v, u, g, m, t] 103 | self.path_tracking_test(alg, a, s, expected_path) 104 | 105 | def test_widening_search(self): 106 | def cost(n): 107 | return -ord(n.data) 108 | def alg(start, done, next): 109 | return search.widening_search(start, done, next, cost) 110 | expected_path = [ 111 | a, c, f, k, # beam width 1 112 | a, c, f, k, j, o, # beam width 2 113 | a, c, f, k, j, o, b, e, i, h, n, v, u, g, m, t # beam width 3 114 | ] 115 | self.path_tracking_test(alg, a, s, expected_path) 116 | 117 | 118 | # ---------------------------------------------------------------------------- 119 | ## Graph search tests 120 | 121 | g1 = Graph(1) 122 | g2 = Graph(2) 123 | g3 = Graph(3) 124 | g4 = Graph(4) 125 | g5 = Graph(5) 126 | g6 = Graph(6) 127 | g1.neighbors = [g4, g5, g6] 128 | g2.neighbors = [g1, g4] 129 | g3.neighbors = [g1, g2, g4] 130 | g4.neighbors = [g3] 131 | g5.neighbors = [] 132 | g6.neighbors = [g2, g4] 133 | 134 | 135 | class GraphSearchTest(SearchTest): 136 | def test_bfs(self): 137 | expected_path = [g6, g2, g4, g1, g3] 138 | self.path_tracking_test(search.graph_search_bfs, g6, g5, expected_path) 139 | 140 | def test_dfs(self): 141 | expected_path = [g6, g2, g1] 142 | self.path_tracking_test(search.graph_search_dfs, g6, g5, expected_path) 143 | 144 | 145 | # ---------------------------------------------------------------------------- 146 | ## Pathfinding utilities tests 147 | 148 | # NOTE: Same graph data as the graph search tests 149 | 150 | p1 = search.Path(g1, cost=1) 151 | p2 = search.Path(g2, cost=3) 152 | p3 = search.Path(g3, cost=5) 153 | p4 = search.Path(g4, cost=7) 154 | 155 | p2.prev_path = p1 156 | p3.prev_path = p2 157 | p4.prev_path = p3 158 | 159 | paths = [p1, p2, p3, p4] 160 | 161 | def comp(path1, path2): 162 | return path1.cost - path2.cost 163 | 164 | def cost(node1, node2): 165 | return abs(node1.data - node2.data) 166 | 167 | def collect_paths(paths): 168 | return [(p.cost, p.collect()) for p in paths] 169 | 170 | 171 | class PathTest(unittest.TestCase): 172 | def test_find_path(self): 173 | found = search.find_path(g3, paths) 174 | self.assertEqual(p3, found) 175 | 176 | def test_find_path_none(self): 177 | found = search.find_path(g5, paths) 178 | self.assertFalse(found) 179 | 180 | def test_insert_path_begin(self): 181 | look_in = [p2, p3, p4] 182 | search.insert_path(p1, look_in, comp) 183 | self.assertEqual(paths, look_in) 184 | 185 | def test_insert_path_middle(self): 186 | look_in = [p1, p2, p4] 187 | search.insert_path(p3, look_in, comp) 188 | self.assertEqual(paths, look_in) 189 | 190 | def test_insert_path_end(self): 191 | look_in = [p1, p2, p3] 192 | search.insert_path(p4, look_in, comp) 193 | self.assertEqual(paths, look_in) 194 | 195 | def test_collect_path(self): 196 | path = p4.collect() 197 | expected = [g1, g2, g3, g4] 198 | self.assertEqual(expected, path) 199 | 200 | def test_replace_if_better(self): 201 | look_in = list(paths) 202 | replace_in = [] 203 | path = search.Path(g3, cost=4) 204 | y = search.replace_if_better(path, comp, look_in, replace_in) 205 | self.assertEqual([p1, p2, p4], look_in) 206 | self.assertEqual([path], replace_in) 207 | self.assertTrue(y) 208 | 209 | def test_replace_if_better_not_better(self): 210 | look_in = list(paths) 211 | replace_in = [] 212 | path = search.Path(g3, cost=9) 213 | y = search.replace_if_better(path, comp, look_in, replace_in) 214 | self.assertEqual(paths, look_in) 215 | self.assertEqual([], replace_in) 216 | self.assertFalse(y) 217 | 218 | def test_replace_if_better_not_found(self): 219 | look_in = list(paths) 220 | replace_in = [] 221 | path = search.Path(g5, cost=1) 222 | y = search.replace_if_better(path, comp, look_in, replace_in) 223 | self.assertEqual(paths, look_in) 224 | self.assertEqual([], replace_in) 225 | self.assertFalse(y) 226 | 227 | def test_replace_if_better_same_list(self): 228 | look_in = list(paths) 229 | path = search.Path(g3, cost=4) 230 | y = search.replace_if_better(path, comp, look_in, look_in) 231 | self.assertEqual([p1, p2, path, p4], look_in) 232 | self.assertTrue(y) 233 | 234 | def test_extend_path(self): 235 | start = search.Path(g3) 236 | to = [g1, g2, g4] 237 | 238 | path15 = search.Path(g5, search.Path(g1, None, 0), 4) 239 | path152 = search.Path(g2, path15, 7) 240 | path43 = search.Path(g3, search.Path(g4, None, 0), 1) 241 | path62 = search.Path(g2, search.Path(g6, None, 0), 4) 242 | path621 = search.Path(g1, path62, 5) 243 | path15 = search.Path(g5, search.Path(g1, None, 0), 4) 244 | path32 = search.Path(g2, search.Path(g3, None, 0), 1) 245 | path31 = search.Path(g1, search.Path(g3, None, 0), 2) 246 | path34 = search.Path(g4, search.Path(g3, None, 0), 1) 247 | 248 | current = [path43, path152] 249 | current_after = [path34, path32, path43, path31] 250 | 251 | old = [path15, path621] 252 | old_after = [path15] 253 | 254 | search.extend_path(start, to, current, old, cost, comp) 255 | self.assertEqual(collect_paths(old_after), collect_paths(old)) 256 | self.assertEqual(collect_paths(current_after), collect_paths(current)) 257 | 258 | 259 | # ---------------------------------------------------------------------------- 260 | ## A* tests 261 | 262 | # NOTE: Same graph data as the graph search tests 263 | 264 | class AStarTest(unittest.TestCase): 265 | def a_star_test(self, a, b, heuristic, expected_path, expected_cost): 266 | finished = lambda node: node is b 267 | next = lambda node: node.neighbors 268 | path = search.a_star([search.Path(a)], finished, next, cost, heuristic) 269 | self.assertEqual(expected_path, path.collect()) 270 | self.assertEqual(expected_cost, path.cost) 271 | self.assertEqual(b, path.state) 272 | 273 | def test_dijkstra(self): 274 | h = lambda node: 0 275 | expected = [g6, g4, g3, g1, g5] 276 | self.a_star_test(g6, g5, h, expected, 9) 277 | 278 | def test_a_star(self): 279 | h = lambda node: abs(node.data - g5.data) 280 | expected = [g6, g4, g3, g1, g5] 281 | self.a_star_test(g6, g5, h, expected, 9) 282 | -------------------------------------------------------------------------------- /paip/examples/search/gps.py: -------------------------------------------------------------------------------- 1 | from paip.search import beam_search 2 | 3 | ## GPS implemented as search 4 | 5 | def successors(states, operators): 6 | ret = [] 7 | for op in applicable_ops(states, operators): 8 | ret.append([s for s in states if s not in op['delete']] + op['add']) 9 | return ret 10 | 11 | 12 | def applicable_ops(states, ops): 13 | states = set(states) 14 | return [op for op in ops if set(op['preconds']) <= states] 15 | 16 | 17 | def gps(initial_states, goal_states, operators, beam_width=10): 18 | prefix = 'Executing ' 19 | for operator in operators: 20 | operator['add'].append(prefix + operator['action']) 21 | 22 | def get_successors(states): 23 | return successors(states, operators) 24 | 25 | def goal_reached(states): 26 | for goal in goal_states: 27 | if goal not in states: 28 | return False 29 | return True 30 | 31 | def cost(states): 32 | sum = len([s for s in states if s.startswith(prefix)]) 33 | sum += len([g for g in goal_states if g not in states]) 34 | return sum 35 | 36 | final = beam_search(initial_states, goal_reached, 37 | get_successors, cost, beam_width) 38 | return [state for state in final if state.startswith(prefix)] 39 | 40 | 41 | ## Example problem definition 42 | 43 | problem = { 44 | "start": ["space on a", "a on b", "b on c", "c on table", "space on table"], 45 | "finish": ["space on c", "c on b", "b on a", "a on table", "space on table"], 46 | "ops": [ 47 | { 48 | "action": "move a from b to c", 49 | "preconds": [ 50 | "space on a", 51 | "space on c", 52 | "a on b" 53 | ], 54 | "add": [ 55 | "a on c", 56 | "space on b" 57 | ], 58 | "delete": [ 59 | "a on b", 60 | "space on c" 61 | ] 62 | }, 63 | { 64 | "action": "move a from table to b", 65 | "preconds": [ 66 | "space on a", 67 | "space on b", 68 | "a on table" 69 | ], 70 | "add": [ 71 | "a on b" 72 | ], 73 | "delete": [ 74 | "a on table", 75 | "space on b" 76 | ] 77 | }, 78 | { 79 | "action": "move a from b to table", 80 | "preconds": [ 81 | "space on a", 82 | "space on table", 83 | "a on b" 84 | ], 85 | "add": [ 86 | "a on table", 87 | "space on b" 88 | ], 89 | "delete": [ 90 | "a on b" 91 | ] 92 | }, 93 | { 94 | "action": "move a from c to b", 95 | "preconds": [ 96 | "space on a", 97 | "space on b", 98 | "a on c" 99 | ], 100 | "add": [ 101 | "a on b", 102 | "space on c" 103 | ], 104 | "delete": [ 105 | "a on c", 106 | "space on b" 107 | ] 108 | }, 109 | { 110 | "action": "move a from table to c", 111 | "preconds": [ 112 | "space on a", 113 | "space on c", 114 | "a on table" 115 | ], 116 | "add": [ 117 | "a on c" 118 | ], 119 | "delete": [ 120 | "a on table", 121 | "space on c" 122 | ] 123 | }, 124 | { 125 | "action": "move a from c to table", 126 | "preconds": [ 127 | "space on a", 128 | "space on table", 129 | "a on c" 130 | ], 131 | "add": [ 132 | "a on table", 133 | "space on c" 134 | ], 135 | "delete": [ 136 | "a on c" 137 | ] 138 | }, 139 | { 140 | "action": "move b from a to c", 141 | "preconds": [ 142 | "space on b", 143 | "space on c", 144 | "b on a" 145 | ], 146 | "add": [ 147 | "b on c", 148 | "space on a" 149 | ], 150 | "delete": [ 151 | "b on a", 152 | "space on c" 153 | ] 154 | }, 155 | { 156 | "action": "move b from table to a", 157 | "preconds": [ 158 | "space on b", 159 | "space on a", 160 | "b on table" 161 | ], 162 | "add": [ 163 | "b on a" 164 | ], 165 | "delete": [ 166 | "b on table", 167 | "space on a" 168 | ] 169 | }, 170 | { 171 | "action": "move b from a to table", 172 | "preconds": [ 173 | "space on b", 174 | "space on table", 175 | "b on a" 176 | ], 177 | "add": [ 178 | "b on table", 179 | "space on a" 180 | ], 181 | "delete": [ 182 | "b on a" 183 | ] 184 | }, 185 | { 186 | "action": "move b from c to a", 187 | "preconds": [ 188 | "space on b", 189 | "space on a", 190 | "b on c" 191 | ], 192 | "add": [ 193 | "b on a", 194 | "space on c" 195 | ], 196 | "delete": [ 197 | "b on c", 198 | "space on a" 199 | ] 200 | }, 201 | { 202 | "action": "move b from table to c", 203 | "preconds": [ 204 | "space on b", 205 | "space on c", 206 | "b on table" 207 | ], 208 | "add": [ 209 | "b on c" 210 | ], 211 | "delete": [ 212 | "b on table", 213 | "space on c" 214 | ] 215 | }, 216 | { 217 | "action": "move b from c to table", 218 | "preconds": [ 219 | "space on b", 220 | "space on table", 221 | "b on c" 222 | ], 223 | "add": [ 224 | "b on table", 225 | "space on c" 226 | ], 227 | "delete": [ 228 | "b on c" 229 | ] 230 | }, 231 | { 232 | "action": "move c from a to b", 233 | "preconds": [ 234 | "space on c", 235 | "space on b", 236 | "c on a" 237 | ], 238 | "add": [ 239 | "c on b", 240 | "space on a" 241 | ], 242 | "delete": [ 243 | "c on a", 244 | "space on b" 245 | ] 246 | }, 247 | { 248 | "action": "move c from table to a", 249 | "preconds": [ 250 | "space on c", 251 | "space on a", 252 | "c on table" 253 | ], 254 | "add": [ 255 | "c on a" 256 | ], 257 | "delete": [ 258 | "c on table", 259 | "space on a" 260 | ] 261 | }, 262 | { 263 | "action": "move c from a to table", 264 | "preconds": [ 265 | "space on c", 266 | "space on table", 267 | "c on a" 268 | ], 269 | "add": [ 270 | "c on table", 271 | "space on a" 272 | ], 273 | "delete": [ 274 | "c on a" 275 | ] 276 | }, 277 | { 278 | "action": "move c from b to a", 279 | "preconds": [ 280 | "space on c", 281 | "space on a", 282 | "c on b" 283 | ], 284 | "add": [ 285 | "c on a", 286 | "space on b" 287 | ], 288 | "delete": [ 289 | "c on b", 290 | "space on a" 291 | ] 292 | }, 293 | { 294 | "action": "move c from table to b", 295 | "preconds": [ 296 | "space on c", 297 | "space on b", 298 | "c on table" 299 | ], 300 | "add": [ 301 | "c on b" 302 | ], 303 | "delete": [ 304 | "c on table", 305 | "space on b" 306 | ] 307 | }, 308 | { 309 | "action": "move c from b to table", 310 | "preconds": [ 311 | "space on c", 312 | "space on table", 313 | "c on b" 314 | ], 315 | "add": [ 316 | "c on table", 317 | "space on b" 318 | ], 319 | "delete": [ 320 | "c on b" 321 | ] 322 | } 323 | ] 324 | } 325 | 326 | 327 | from paip import search 328 | 329 | 330 | def main(): 331 | start = problem['start'] 332 | finish = problem['finish'] 333 | ops = problem['ops'] 334 | for action in gps(start, finish, ops): 335 | print action 336 | 337 | 338 | if __name__ == '__main__': 339 | main() 340 | -------------------------------------------------------------------------------- /paip/tests/test_othello.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import random 3 | from paip.othello import * 4 | 5 | class MoveTests(unittest.TestCase): 6 | def setUp(self): 7 | b = initial_board() 8 | # fig. 18.2a from PAIP p.598 9 | b[33:35] = [WHITE, BLACK] 10 | b[43:46] = [WHITE, WHITE, BLACK] 11 | b[52:57] = [WHITE, WHITE, WHITE, WHITE, BLACK] 12 | b[64:66] = [BLACK, WHITE] 13 | self.board = b 14 | 15 | def test_is_valid(self): 16 | self.assertTrue(is_valid(27)) 17 | self.assertFalse(is_valid(30)) 18 | self.assertFalse(is_valid(49)) 19 | self.assertFalse(is_valid(-2)) 20 | self.assertFalse(is_valid(124)) 21 | self.assertFalse(is_valid('foo')) 22 | 23 | def test_find_bracketing_piece_none(self): 24 | square = 63 25 | self.assertEqual(None, find_bracket(63, WHITE, self.board, UP)) 26 | self.assertEqual(None, find_bracket(63, WHITE, self.board, DOWN_RIGHT)) 27 | self.assertEqual(None, find_bracket(63, WHITE, self.board, DOWN)) 28 | self.assertEqual(None, find_bracket(63, WHITE, self.board, DOWN_LEFT)) 29 | self.assertEqual(None, find_bracket(63, WHITE, self.board, LEFT)) 30 | self.assertEqual(None, find_bracket(63, WHITE, self.board, UP_LEFT)) 31 | 32 | def test_find_bracket(self): 33 | square = 42 34 | self.assertEqual(45, find_bracket(42, BLACK, self.board, RIGHT)) 35 | self.assertEqual(64, find_bracket(42, BLACK, self.board, DOWN_RIGHT)) 36 | 37 | def test_is_legal_false_not_empty(self): 38 | self.assertFalse(is_legal(64, WHITE, self.board)) 39 | 40 | def test_is_legal_false_no_bracket(self): 41 | self.assertFalse(is_legal(42, WHITE, self.board)) 42 | 43 | def test_is_legal(self): 44 | self.assertTrue(is_legal(42, BLACK, self.board)) 45 | 46 | def test_make_flips_none(self): 47 | b1, b2 = self.board, list(self.board) 48 | make_flips(42, WHITE, b2, RIGHT) 49 | self.assertEqual(b1, b2) 50 | 51 | def test_make_flips(self): 52 | b1, b2 = self.board, list(self.board) 53 | make_flips(42, BLACK, b2, RIGHT) 54 | b1[43:45] = [BLACK, BLACK] 55 | self.assertEqual(b1, b2) 56 | 57 | def test_make_move(self): 58 | b1, b2 = self.board, list(self.board) 59 | make_move(42, BLACK, b2) 60 | b1[42] = BLACK 61 | b1[43:45] = [BLACK, BLACK] 62 | b1[53] = BLACK 63 | self.assertEqual(b1, b2) 64 | 65 | 66 | class GameTests(unittest.TestCase): 67 | def setUp(self): 68 | self.board = initial_board() 69 | for sq in squares(): 70 | self.board[sq] = WHITE 71 | self.board[11] = EMPTY 72 | self.board[88] = BLACK 73 | 74 | def test_any_legal_move_false(self): 75 | self.assertFalse(any_legal_move(WHITE, self.board)) 76 | 77 | def test_any_legal_move(self): 78 | self.assertTrue(any_legal_move(BLACK, self.board)) 79 | 80 | def test_next_player_repeat(self): 81 | self.assertEqual(BLACK, next_player(self.board, BLACK)) 82 | 83 | def test_next_player_none(self): 84 | self.board[11] = WHITE 85 | self.assertEqual(None, next_player(self.board, BLACK)) 86 | 87 | def test_next_player(self): 88 | self.assertEqual(BLACK, next_player(self.board, WHITE)) 89 | 90 | def test_get_move_invalid(self): 91 | strategy = lambda player, board: -23 92 | self.assertRaises(IllegalMoveError, get_move, strategy, BLACK, self.board) 93 | 94 | def test_get_move_illegal(self): 95 | strategy = lambda player, board: 11 96 | self.assertRaises(IllegalMoveError, get_move, strategy, WHITE, self.board) 97 | 98 | def test_get_move(self): 99 | strategy = lambda player, board: 11 100 | self.assertEqual(11, get_move(strategy, BLACK, self.board)) 101 | 102 | def test_score(self): 103 | make_move(11, BLACK, self.board) 104 | self.assertEqual(8 - 56, score(BLACK, self.board)) 105 | 106 | def test_play(self): 107 | player_accesses = [] 108 | def random_strategy(player, board): 109 | player_accesses.append(player) 110 | legal = [sq for sq in squares() if is_legal(sq, player, board)] 111 | return random.choice(legal) 112 | board, score = play(random_strategy, random_strategy) 113 | 114 | # check that no moves remain 115 | self.assertFalse(any_legal_move(BLACK, board)) 116 | self.assertFalse(any_legal_move(WHITE, board)) 117 | 118 | # check that both players had a turn 119 | self.assertTrue(BLACK in player_accesses) 120 | self.assertTrue(WHITE in player_accesses) 121 | 122 | 123 | class StrategyTests(unittest.TestCase): 124 | def setUp(self): 125 | b = initial_board() 126 | # 1 2 3 4 5 6 7 8 127 | # 1 . . . . . . . . 128 | # 2 . . . . . . . . 129 | # 3 . . o @ . o . . 130 | # 4 . . o o @ @ . . 131 | # 5 . o o o o @ . . 132 | # 6 . . . @ o . . . 133 | # 7 . . . . . . . . 134 | # 8 . . . . . . . . 135 | b[33:37] = [WHITE, BLACK, EMPTY, WHITE] 136 | b[43:47] = [WHITE, WHITE, BLACK, BLACK] 137 | b[52:57] = [WHITE, WHITE, WHITE, WHITE, BLACK] 138 | b[64:66] = [BLACK, WHITE] 139 | self.board = b 140 | 141 | def test_maximizer(self): 142 | self.assertEqual(51, maximizer(score)(BLACK, self.board)) 143 | self.assertEqual(47, maximizer(score)(WHITE, self.board)) 144 | 145 | def test_weighted_score(self): 146 | score = weighted_score(BLACK, self.board) 147 | expected = 5 * 3 - (6 * 3 - 5 + 2 * 15) 148 | self.assertEqual(expected, score) 149 | self.assertEqual(-score, weighted_score(WHITE, self.board)) 150 | 151 | def test_final_value(self): 152 | self.assertEqual(MIN_VALUE, final_value(BLACK, self.board)) 153 | self.assertEqual(MAX_VALUE, final_value(WHITE, self.board)) 154 | 155 | def test_minimax_leaf(self): 156 | val = score(BLACK, self.board) 157 | self.assertEqual((val, None), minimax(BLACK, self.board, 0, score)) 158 | self.assertEqual((-val, None), minimax(WHITE, self.board, 0, score)) 159 | 160 | def test_minimax_game_over(self): 161 | result, outcome = play(random_strategy, random_strategy) 162 | if outcome == 0: 163 | val_black, val_white = 0, 0 164 | else: 165 | val_black = MAX_VALUE if outcome > 0 else MIN_VALUE 166 | val_white = -val_black 167 | self.assertEqual((val_black, None), minimax(BLACK, result, 20, score)) 168 | self.assertEqual((val_white, None), minimax(WHITE, result, 20, score)) 169 | 170 | def test_minimax_pass(self): 171 | # remove all black pieces so black has no moves 172 | for sq in squares(): 173 | if self.board[sq] == BLACK: 174 | self.board[sq] = EMPTY 175 | # result: 176 | # 1 2 3 4 5 6 7 8 177 | # 1 . . . . . . . . 178 | # 2 . . . . . . . . 179 | # 3 . . o . . o . . 180 | # 4 . . o o . . . . 181 | # 5 . o o o o . . . 182 | # 6 . . . . o . . . 183 | # 7 . . . . . . . . 184 | # 8 . . . . . . . . 185 | 186 | # leave one move for white 187 | self.board[57:59] = [BLACK, WHITE] 188 | # result: 189 | # 1 2 3 4 5 6 7 8 190 | # 1 . . . . . . . . 191 | # 2 . . . . . . . . 192 | # 3 . . o . . o . . 193 | # 4 . . o o . . . . 194 | # 5 . o o o o . @ o 195 | # 6 . . . . o . . . 196 | # 7 . . . . . . . . 197 | # 8 . . . . . . . . 198 | 199 | accesses = [] 200 | def evaluate(player, board): 201 | accesses.append(player) 202 | return score(player, board) 203 | self.assertEqual(0, len(accesses)) 204 | self.assertEqual((MIN_VALUE, None), minimax(BLACK, self.board, 20, evaluate)) 205 | 206 | def test_minimax(self): 207 | board = initial_board() 208 | for sq in squares(): 209 | board[sq] = EMPTY 210 | 211 | # 1 2 3 4 5 6 7 8 212 | # 1 . . . . . . . . 213 | # 2 . . . . . . . . 214 | # 3 . . . . . . . . 215 | # 4 . . . . . . . . 216 | # 5 . . . . . . . . 217 | # 6 . . . . . . . . 218 | # 7 . @ @ @ o @ . . 219 | # 8 . . . . . . . . 220 | board[72:77] = [BLACK, BLACK, BLACK, WHITE, BLACK] 221 | accesses = [] 222 | def evaluate(player, board): 223 | accesses.append(player) 224 | return score(player, board) 225 | self.assertEqual((MAX_VALUE, 71), minimax(WHITE, board, 20, evaluate)) 226 | self.assertEqual(0, len(accesses)) 227 | 228 | def test_alphabeta(self): 229 | board = initial_board() 230 | for sq in squares(): 231 | board[sq] = EMPTY 232 | 233 | # 1 2 3 4 5 6 7 8 234 | # 1 . @ o . . . . . 235 | # 2 . . . . . . . . 236 | # 3 . @ o o o . . . 237 | # 4 . . . . . . . . 238 | # 5 . @ o o o o o . 239 | # 6 . . . . . . . . 240 | # 7 . @ o o . . . . 241 | # 8 . . . . . . . . 242 | board[12:14] = [BLACK, WHITE] 243 | board[32:35] = [BLACK, WHITE, WHITE, WHITE] 244 | board[52:56] = [BLACK, WHITE, WHITE, WHITE, WHITE, WHITE] 245 | board[72:77] = [BLACK, WHITE, WHITE] 246 | 247 | # 1. 14, value -4, below alpha, ignore 248 | # 2. 36, value 0, above alpha, update alpha 249 | # 3. 58, value 4, above alpha, update alpha 250 | # 2. 75, value -2, alpha above beta, ignore and break 251 | expected_values = [4, 0, -4] 252 | 253 | values = [] 254 | def evaluate(player, board): 255 | val = score(player, board) 256 | values.append(val) 257 | return val 258 | self.assertEqual((4, 58), alphabeta(BLACK, board, -1, 3, 1, evaluate)) 259 | self.assertEqual(expected_values, values) 260 | -------------------------------------------------------------------------------- /paip/abandoned/logic.lisp: -------------------------------------------------------------------------------- 1 | ; Logic programming 2 | 3 | ;;; Overview: 4 | ;;; - single uniform database--fewer data structures 5 | ;;; - facts are represented as relations, can be used to deduce new facts 6 | ;;; - variables are "logic variables", bound by "unification", never change value 7 | ;;; - programmer gives constraints without specifying how to do the evaluation 8 | ;;; - "automatic backtracking" allows the programmer to specify multiple constraints, 9 | ;;; and Prolog will try to find facts that satisfy all of them 10 | 11 | ;; Idea 1: Uniform database 12 | 13 | ;;; Data takes the form of a database of assertions, called "clauses", which come 14 | ;;; in two types: facts and rules. 15 | ;;; Facts state a relationship that holds between two objects 16 | ;;; Rules state contingent facts (predicates?) 17 | 18 | ;;; Two ways to procedurally interpret declarative forms: 19 | ;;; - backward-chaining: reasoning backwards from the goal to the premises 20 | ;;; - forward-chaining: reasoning forwards from the premises to a conclusion 21 | 22 | ;;; Prolog does backward chaining only. 23 | 24 | ;; Idea 2: Unification of logic variables 25 | 26 | ;;; Method for stating that a variable is equal to an expression or another variable. 27 | 28 | (defconstant fail nil "Indicates pat-match failure") 29 | 30 | (defconstant no-bindings '((t . t))) 31 | 32 | (defparameter *occurs-check* t "Should we do the occurs check?") 33 | 34 | (defun variable-p (x) 35 | "Is x a variable (a symbol beginning with ?)?" 36 | (and (symbolp x) (equal (char (symbol-name x) 0) #\?))) 37 | 38 | (defun get-binding (var bindings) 39 | "Find a (variable . value) pair in a binding list." 40 | (assoc var bindings)) 41 | 42 | (defun binding-val (binding) 43 | "Get the value part of a single binding." 44 | (cdr binding)) 45 | 46 | (defun lookup (var bindings) 47 | "Get the value part (for var) from a binding list." 48 | (binding-val (get-binding var bindings))) 49 | 50 | (defun extend-bindings (var val bindings) 51 | "Add a (var . value) pair to a binding list." 52 | (cons (cons var val) 53 | (if (equal bindings no-bindings) 54 | nil 55 | bindings))) 56 | 57 | (defun match-variable (var input bindings) 58 | "Does VAR match input? Uses (or updates) and returns bindings." 59 | (let ((binding (get-binding var bindings))) 60 | (cond ((not binding) (extend-bindings var input bindings)) 61 | ((equal input (binding-val binding)) bindings) 62 | (t fail)))) 63 | 64 | (defun unify (x y &optional (bindings no-bindings)) 65 | "See if x and y match with given bindings." 66 | (cond ((eq bindings fail) fail) 67 | ((eql x y) bindings) 68 | ((variable-p x) (unify-variable x y bindings)) 69 | ((variable-p y) (unify-variable y x bindings)) 70 | ((and (consp x) (consp y)) 71 | (unify (rest x) (rest y) 72 | (unify (first x) (first y) bindings))) 73 | (t fail))) 74 | 75 | (defun unify-variable (var x bindings) 76 | "Unify var with x, using (and maybe extending) bindings." 77 | (cond ((get-binding var bindings) 78 | (unify (lookup var bindings) x bindings)) 79 | ((and (variable-p x) (get-binding x bindings)) 80 | (unify var (lookup x bindings) bindings)) 81 | ((and *occurs-check* (occurs-check var x bindings)) 82 | fail) 83 | (t (extend-bindings var x bindings)))) 84 | 85 | (defun occurs-check (var x bindings) 86 | "Does var occur anywhere inside x? (infinite circular unification!)" 87 | (cond ((eq var x) t) 88 | ((and (variable-p x) (get-binding x bindings)) 89 | (occurs-check var (lookup x bindings) bindings)) 90 | ((consp x) (or (occurs-check var (first x) bindings) 91 | (occurs-check var (rest x) bindings))) 92 | (t nil))) 93 | 94 | (defun subst-bindings (bindings x) 95 | "Substitute the value of variables in bindings into x, taking recursively 96 | bound variables into account." 97 | (cond ((eq bindings fail) fail) 98 | ((eq bindings no-bindings) x) 99 | ((and (variable-p x) (get-binding x bindings)) 100 | (subst-bindings bindings (lookup x bindings))) 101 | ((atom x) x) 102 | (t (cons (subst-bindings bindings (car x)) 103 | (subst-bindings bindings (cdr x)))))) 104 | 105 | (defun unifier (x y) 106 | "Return something that unifies with both x and y (or fail)." 107 | (subst-bindings (unify x y) x)) 108 | 109 | ;; represent clauses as (head . body) 110 | (defun clause-head (clause) (first clause)) 111 | (defun clause-body (clause) (rest clause)) 112 | 113 | ;; indexing--we interpret a clause as: to prove the head, prove the body. 114 | ;; so index on heads. 115 | 116 | ;; clauses are stored on the predicate's plist 117 | ;; DC: this is stupid. we can't reach the property list, it's some magical cloud thing 118 | (defun get-clauses (pred) (get pred 'clauses)) 119 | (defun predicate (relation) (first relation)) 120 | 121 | (defvar *db-predicates* nil 122 | "A list of all predicates stored in the database.") 123 | 124 | (defmacro <- (&rest clause) 125 | `(add-clause ',clause)) 126 | 127 | (defun add-clause (clause) 128 | "Add a clause to the database, indexed by head's predicate" 129 | ;; predicate must be a non-variable symbol. 130 | (let ((pred (predicate (clause-head clause)))) 131 | (assert (and (symbolp pred) (not (variable-p pred)))) 132 | (pushnew pred *db-predicates*) 133 | (setf (get pred 'clauses) 134 | (nconc (get-clauses pred) (list clause))) 135 | pred)) 136 | 137 | (defun clear-db () 138 | "Remove all clauses (for all predicates) from the data base." 139 | (mapc #'clear-predicate *db-predicates*)) 140 | 141 | (defun clear-predicate (predicate) 142 | "Remove the clauses for a single predicate." 143 | (setf (get predicate 'clauses) nil)) 144 | 145 | ;; (defun prove (goal bindings) 146 | ;; "Return a list of possible solutions to a goal." 147 | ;; ;; To prove goal, first find candidate clauses. 148 | ;; ;; For each candidate, check if goal unifies with the head of the clause. 149 | ;; ;; If so, try to prove all the goals in the body of the clause. 150 | ;; ;; --> for facts, there are no goals in the body, which means success. 151 | ;; ;; --> for rules, goals in body are proved one at a time, propagating bindings. 152 | ;; (mapcan #'(lambda (clause) 153 | ;; (let ((new-clause (rename-variables clause))) 154 | ;; (prove-all (clause-body new-clause) 155 | ;; (unify goal (clause-head new-clause) bindings)))) 156 | ;; (get-clauses (predicate goal)))) 157 | 158 | ;; (defun prove-all (goals bindings) 159 | ;; "Return a list of solutions to the conjunction of goals." 160 | ;; (cond ((eq bindings fail) fail) 161 | ;; ((null goals) (list bindings)) 162 | ;; (t (mapcan #'(lambda (goal1-solution) 163 | ;; (prove-all (rest goals) goal1-solution)) 164 | ;; (prove (first goals) bindings))))) 165 | 166 | (defun rename-variables (x) 167 | "Replace all variables in x with new ones." 168 | (sublis (mapcar #'(lambda (var) (cons var (gensym (string var)))) 169 | (variables-in x)) 170 | x)) 171 | 172 | (defun variables-in (exp) 173 | "Return a list of all the variables in EXP." 174 | (unique-find-anywhere-if #'variable-p exp)) 175 | 176 | (defun unique-find-anywhere-if (predicate tree &optional found-so-far) 177 | "Return a list of leaves of tree satisfying predicate, with duplicates removed." 178 | (if (atom tree) 179 | (if (funcall predicate tree) 180 | (adjoin tree found-so-far) 181 | found-so-far) 182 | (unique-find-anywhere-if predicate 183 | (first tree) 184 | (unique-find-anywhere-if predicate 185 | (rest tree) 186 | found-so-far)))) 187 | 188 | (defmacro ?- (&rest goals) `(top-level-prove ',goals)) 189 | 190 | ;; (defun top-level-prove (goals) 191 | ;; "Prove the goals, and print variables readably." 192 | ;; (show-prolog-solutions 193 | ;; (variables-in goals) 194 | ;; (prove-all goals no-bindings))) 195 | 196 | (defun show-prolog-solutions (vars solutions) 197 | "Print the variables in each of the solutions." 198 | (if (null solutions) 199 | (format t "~&No.") 200 | (mapc #'(lambda (solution) (show-prolog-vars vars solution)) 201 | solutions)) 202 | (values)) 203 | 204 | ;; (defun show-prolog-vars (vars bindings) 205 | ;; "Print each variable with its binding." 206 | ;; (if (null vars) 207 | ;; (format t "~&Yes") 208 | ;; (dolist (var vars) 209 | ;; (format t "~&~a = ~a" var 210 | ;; (subst-bindings bindings var)))) 211 | ;; (princ ";")) 212 | 213 | ;; Idea 3: automatic backtracking (redefining prove and prove-all) 214 | 215 | (defun prove-all (goals bindings) 216 | "Find a solution to the conjunction of goals." 217 | (cond ((eq bindings fail) fail) 218 | ((null goals) bindings) 219 | (t (prove (first goals) bindings (rest goals))))) 220 | 221 | (defun prove (goal bindings other-goals) 222 | "Return a list of possible solutions to goal." 223 | (let ((clauses (get-clauses (predicate goal)))) 224 | (if (listp clauses) 225 | (some 226 | #'(lambda (clause) 227 | (let ((new-clause (rename-variables clause))) 228 | (prove-all 229 | (append (clause-body new-clause) other-goals) 230 | (unify goal (clause-head new-clause) bindings)))) 231 | clauses) 232 | ;; If clauses isn't a list, it might be a primitive function to call (atom) 233 | (funcall clauses (rest goal) bindings other-goals)))) 234 | 235 | (defun top-level-prove (goals) 236 | (prove-all `(,@goals (show-prolog-vars ,@(variables-in goals))) 237 | no-bindings) 238 | (format t "~&No.") 239 | (values)) 240 | 241 | (defun show-prolog-vars (vars bindings other-goals) 242 | "Print each variable with its binding. 243 | Then ask the user if more solutions are desired." 244 | (if (null vars) 245 | (format t "~&Yes") 246 | (dolist (var vars) 247 | (format t "~&~a = ~a" var 248 | (subst-bindings bindings var)))) 249 | (if (continue-p) 250 | fail 251 | (prove-all other-goals bindings))) 252 | 253 | (setf (get 'show-prolog-vars 'clauses) 'show-prolog-vars) 254 | 255 | (defun continue-p () 256 | "Ask user if we should continue looking for solutions." 257 | (case (read-char) 258 | (#\; t) 259 | (#\. nil) 260 | (#\newline (continue-p)) 261 | (otherwise 262 | (format t " Type ; to see more or . to stop") 263 | (continue-p)))) 264 | 265 | (defmacro <- (&rest clause) 266 | "Add a clause to the database." 267 | `(add-clause ',(replace-?-vars clause))) 268 | 269 | (defmacro ?- (&rest goals) 270 | "Make a query and print answers." 271 | `(top-level-prove ',(replace-?-vars goals))) 272 | 273 | (defun replace-?-vars (exp) 274 | "Replace any ? within exp with a var of the form ?123." 275 | (cond ((eq exp '?) (gensym "?")) 276 | ((atom exp) exp) 277 | (t (cons (replace-?-vars (first exp)) 278 | (replace-?-vars (rest exp)))))) 279 | -------------------------------------------------------------------------------- /prolog.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | import argparse 4 | import logging 5 | import sys 6 | 7 | from paip import logic 8 | 9 | 10 | ## Parser and REPL 11 | 12 | # QUESTION = "?" 13 | # DEFN_BEGIN = "<-" 14 | # QUERY_BEGIN = QUESTION "-" 15 | # NUM = (-|+)?[0-9]+("."[0-9]+)? 16 | # IDENT: [a-zA-Z][a-zA-Z0-9_]* 17 | # WHEN = ":-" 18 | # LPAREN = "(" 19 | # RPAREN = ")" 20 | # COMMA = "," 21 | 22 | # command: EOF | query | defn 23 | # query: QUERY_BEGIN relation 24 | # defn: DEFN_BEGIN relation (WHEN relation_list)? 25 | # relation_list = relation [COMMA relation]* 26 | # relation: IDENT LPAREN term [COMMA term]* RPAREN 27 | # term: relation | var | atom 28 | # atom: NUM | IDENT 29 | # var: QUESTION IDENT 30 | 31 | 32 | class ParseError(Exception): 33 | def __init__(self, err): 34 | self.err = err 35 | 36 | def __str__(self): 37 | return 'Parse error: %s' % self.err 38 | 39 | 40 | class Parser(object): 41 | k = 2 42 | 43 | def __init__(self, lexer): 44 | self.lexer = lexer 45 | self.lookahead = [] 46 | for i in xrange(Parser.k): 47 | self.lookahead.append(lexer.next()) 48 | 49 | def la(self, i): 50 | return self.lookahead[i-1] 51 | 52 | def match(self, exp_tt): 53 | tt, tok = self.la(1) 54 | if tt != exp_tt: 55 | raise ParseError('Expected %s, got %s' % (exp_tt, tt)) 56 | self.lookahead.pop(0) 57 | self.lookahead.append(self.lexer.next()) 58 | return tok 59 | 60 | def command(self): 61 | tt, tok = self.la(1) 62 | if tt == EOF: 63 | return 64 | if tt == QUERY_BEGIN: 65 | return self.query() 66 | elif tt == DEFN_BEGIN: 67 | return self.defn() 68 | raise ParseError('Unknown command: %s' % tok) 69 | 70 | def query(self): 71 | self.match(QUERY_BEGIN) 72 | return self.relation() 73 | 74 | def defn(self): 75 | self.match(DEFN_BEGIN) 76 | head = self.relation() 77 | tt, tok = self.la(1) 78 | if tt == WHEN: 79 | self.match(WHEN) 80 | return logic.Clause(head, self.relation_list()) 81 | return logic.Clause(head) 82 | 83 | def relation_list(self): 84 | rels = [self.relation()] 85 | tt, tok = self.la(1) 86 | while tt == COMMA: 87 | self.match(COMMA) 88 | rels.append(self.relation()) 89 | tt, tok = self.la(1) 90 | return rels 91 | 92 | def relation(self): 93 | pred = self.match(IDENT) 94 | body = [] 95 | self.match(LPAREN) 96 | body.append(self.term()) 97 | tt, tok = self.la(1) 98 | while tt == COMMA: 99 | self.match(COMMA) 100 | body.append(self.term()) 101 | tt, tok = self.la(1) 102 | self.match(RPAREN) 103 | return logic.Relation(pred, body) 104 | 105 | def term(self): 106 | tt, tok = self.la(1) 107 | if tt == QUESTION: 108 | return self.var() 109 | elif tt == NUM: 110 | return self.atom() 111 | elif tt == IDENT: 112 | tt2, tok2 = self.la(2) 113 | if tt2 == LPAREN: 114 | return self.relation() 115 | else: 116 | return self.atom() 117 | else: 118 | raise ParseError('Unknown term lookahead: %s' % tok) 119 | 120 | def var(self): 121 | self.match(QUESTION) 122 | return logic.Var(self.match(IDENT)) 123 | 124 | def atom(self): 125 | tt, tok = self.la(1) 126 | if tt == NUM: 127 | return logic.Atom(self.match(NUM)) 128 | elif tt == IDENT: 129 | return logic.Atom(self.match(IDENT)) 130 | else: 131 | raise ParseError('Unknown atom: %s' % tok) 132 | 133 | 134 | class TokenError(Exception): 135 | def __init__(self, err): 136 | self.err = err 137 | 138 | def __str__(self): 139 | return 'Token error: %s' % self.err 140 | 141 | 142 | LPAREN = 'LPAREN' 143 | RPAREN = 'RPAREN' 144 | COMMA = 'COMMA' 145 | QUESTION = 'QUESTION' 146 | DEFN_BEGIN = 'DEFN_BEGIN' 147 | QUERY_BEGIN = 'QUERY_BEGIN' 148 | NUM = 'NUM' 149 | IDENT = 'IDENT' 150 | WHEN = 'WHEN' 151 | EOF = 'EOF' 152 | 153 | 154 | class Lexer(object): 155 | def __init__(self, line): 156 | self.line = line 157 | self.pos = 0 158 | self.ch = line[self.pos] 159 | 160 | def eat(self): 161 | ret = self.ch 162 | self.pos += 1 163 | if self.pos >= len(self.line): 164 | self.ch = EOF 165 | else: 166 | self.ch = self.line[self.pos] 167 | return ret 168 | 169 | def match(self, exp): 170 | if self.ch != exp: 171 | raise TokenError('expected %s' % exp) 172 | self.eat() 173 | 174 | def expect(self, is_type): 175 | if not is_type(): 176 | raise TokenError('expected type %s' % repr(is_type)) 177 | 178 | def is_ws(self): 179 | return self.ch in (' ', '\t', '\n') 180 | 181 | def DEFN_BEGIN(self): 182 | self.match('<') 183 | self.match('-') 184 | return DEFN_BEGIN, '<-' 185 | 186 | def is_when(self): 187 | return self.ch == ':' 188 | 189 | def WHEN(self): 190 | self.match(':') 191 | self.match('-') 192 | return WHEN, ':-' 193 | 194 | def is_number(self): 195 | return self.ch in '0123456789' 196 | 197 | def is_num(self): 198 | return self.is_number() or self.ch in ('+', '-') 199 | 200 | def NUM(self): 201 | # get the leading sign 202 | sign = 1 203 | if self.ch == '+': 204 | self.eat() 205 | elif self.ch == '-': 206 | sign = -1 207 | self.eat() 208 | 209 | # read the whole part 210 | num = '' 211 | self.expect(self.is_number) 212 | while self.is_number(): 213 | num += self.eat() 214 | 215 | if not self.ch == '.': 216 | return NUM, int(num) 217 | num += self.eat() 218 | 219 | # read the fractional part 220 | self.expect(self.is_number) 221 | while self.is_number(): 222 | num += self.eat() 223 | return NUM, float(num) 224 | 225 | def is_ident(self): 226 | letters = 'abcdefghijklmnopqrstuvwxyz' 227 | return self.ch in letters or self.ch in letters.upper() 228 | 229 | def IDENT(self): 230 | ident = '' 231 | self.expect(self.is_ident) 232 | while self.is_ident() or self.is_number(): 233 | ident += self.eat() 234 | return IDENT, ident 235 | 236 | def comment(self): 237 | self.match('#') 238 | while self.ch != '\n': 239 | self.eat() 240 | 241 | def next(self): 242 | while self.pos < len(self.line): 243 | if self.is_ws(): 244 | self.eat() 245 | continue 246 | if self.ch == '#': 247 | self.comment() 248 | continue 249 | if self.ch == '<': 250 | return self.DEFN_BEGIN() 251 | if self.ch == '?': 252 | self.eat() 253 | if self.ch == '-': 254 | self.eat() 255 | return QUERY_BEGIN, '?-' 256 | return QUESTION, '?' 257 | if self.is_ident(): 258 | return self.IDENT() 259 | if self.is_num(): 260 | return self.NUM() 261 | if self.is_when(): 262 | return self.WHEN() 263 | if self.ch == '(': 264 | return LPAREN, self.eat() 265 | if self.ch == ')': 266 | return RPAREN, self.eat() 267 | if self.ch == ',': 268 | return COMMA, self.eat() 269 | raise TokenError('no token begins with %s' % self.ch) 270 | return EOF, EOF 271 | 272 | 273 | def tokens(line): 274 | lexer = Lexer(line) 275 | while True: 276 | tokt, tok = lexer.next() 277 | if tokt == EOF: 278 | return 279 | yield tokt, tok 280 | 281 | 282 | def parse(line): 283 | p = Parser(Lexer(line)) 284 | return p.command() 285 | 286 | 287 | def print_db(db): 288 | print 'Database:' 289 | longest = -1 290 | for pred in db: 291 | if len(pred) > longest: 292 | longest = len(pred) 293 | for pred, items in db.items(): 294 | if not isinstance(items, list): 295 | continue 296 | print '%s:' % pred 297 | for item in items: 298 | print '\t', item 299 | 300 | 301 | def read_db(db_file): 302 | db = {} 303 | for line in db_file: 304 | if line == '\n': continue 305 | q = parse(line) 306 | if q: 307 | logic.store(db, q) 308 | return db 309 | 310 | 311 | ## Running 312 | 313 | help='''This interpreter provides basic functionality only--the subset of Prolog known 314 | as "Pure Prolog". That is, only clauses are supported--no lists, user-defined 315 | procedures, or arithmetic evaluation. 316 | 317 | The REPL allows both rule/fact definition as well as goal proving. The syntax 318 | is as follows: 319 | 320 | Defining rules and facts: 321 | 322 | <- this(is, ?a) :- simple(?rule), for(?demonstration, only) 323 | <- coprime(14, 15) 324 | 325 | Proving goals: 326 | 327 | ?- coprime(?x, 9) 328 | 329 | For some example rule databases, see `paip/examples/prolog`. They can be loaded 330 | with the `--db` option. 331 | ''' 332 | 333 | argparser = argparse.ArgumentParser(description='A Prolog implementation.', 334 | formatter_class=argparse.RawDescriptionHelpFormatter, 335 | epilog=help) 336 | 337 | argparser.add_argument('--logging', 338 | action='store_true', 339 | help='Enable logging', 340 | dest='log') 341 | argparser.add_argument('--db', 342 | type=file, 343 | help='Database file', 344 | dest='db_file') 345 | 346 | 347 | def main(): 348 | print 'Welcome to PyLogic. Type "help" for help.' 349 | 350 | args = argparser.parse_args() 351 | db = read_db(args.db_file) if args.db_file else {} 352 | if args.log: 353 | logging.basicConfig(level=logging.DEBUG) 354 | 355 | print_db(db) 356 | while True: 357 | try: 358 | line = raw_input('>> ') 359 | except EOFError: 360 | break 361 | if not line: 362 | continue 363 | if line == 'quit': 364 | break 365 | if line == 'help': 366 | print help 367 | continue 368 | try: 369 | q = parse(line) 370 | except ParseError as e: 371 | print e 372 | continue 373 | except TokenError as e: 374 | print e 375 | continue 376 | 377 | if isinstance(q, logic.Relation): 378 | try: 379 | logic.prolog_prove([q], db) 380 | except KeyboardInterrupt: 381 | print 'Cancelled.' 382 | elif isinstance(q, logic.Clause): 383 | logic.store(db, q) 384 | print_db(db) 385 | 386 | print 'Goodbye.' 387 | 388 | 389 | if __name__ == '__main__': 390 | main() 391 | -------------------------------------------------------------------------------- /paip/search.py: -------------------------------------------------------------------------------- 1 | """ 2 | Searching is one of the most useful strategies in AI programming. AI problems 3 | can often be expressed as state spaces with transitions between states. For 4 | instance, the General Problem Solver could be considered as a search 5 | problem--given some states, we apply state transitions to explore the state 6 | space until the goal is reached. 7 | 8 | For some example applications, see the following programs: 9 | 10 | - [Pathfinding](examples/search/pathfinding.html) 11 | - [GPS by searching](examples/search/gps.html) 12 | 13 | """ 14 | 15 | # ----------------------------------------------------------------------------- 16 | ## Tree Searches 17 | 18 | # Many problems find convenient expression as search trees of state spaces: each 19 | # state has some successor states, and we explore the "tree" of states formed by 20 | # linking each state to its successors. We can explore this state tree by 21 | # holding a set of "candidate", or "current", states, and exploring all its 22 | # successors until the goal is reached. 23 | 24 | ### The general case 25 | 26 | def tree_search(states, goal_reached, get_successors, combine_states): 27 | """ 28 | Given some initial states, explore a state space until reaching the goal. 29 | 30 | `states` should be a list of initial states (which can be anything). 31 | `goal_reached` should be a predicate, where `goal_reached(state)` returns 32 | `True` when `state` is the goal state. 33 | `get_successors` should take a state as input and return a list of that 34 | state's successor states. 35 | `combine_states` should take two lists of states as input--the current 36 | states and a list of new states--and return a combined list of states. 37 | 38 | When the goal is reached, the goal state is returned. 39 | """ 40 | 41 | # If there are no more states to explore, we have failed. 42 | if not states: 43 | return None 44 | 45 | if goal_reached(states[0]): 46 | return states[0] 47 | 48 | # Get the states that follow the first current state and combine them 49 | # with the other current states. 50 | successors = get_successors(states[0]) 51 | next = combine_states(successors, states[1:]) 52 | 53 | # Recursively search from the new list of current states. 54 | return tree_search(next, goal_reached, get_successors, combine_states) 55 | 56 | 57 | ### Depth-first search 58 | 59 | def dfs(start, goal_reached, get_successors): 60 | """ 61 | A tree search where the state space is explored depth-first. 62 | 63 | That is, all of the successors of a single state are fully explored before 64 | exploring a sibling state. 65 | """ 66 | def combine(new_states, existing_states): 67 | # The new states (successors of the first current state) should be 68 | # explored next, before the other states, so they are prepended to the 69 | # list of current states. 70 | return new_states + existing_states 71 | return tree_search([start], goal_reached, get_successors, combine) 72 | 73 | 74 | ### Breadth-first search 75 | 76 | def bfs(start, goal_reached, get_successors): 77 | """ 78 | A tree search where the state space is explored breadth-first. 79 | 80 | That is, after examining a single state, all of its successors should be 81 | examined before any of their successors are explored. 82 | """ 83 | def combine(new_states, existing_states): 84 | # Finish examining all of the sibling states before exploring any of 85 | # their successors--add all the new states at the end of the current 86 | # state list. 87 | return existing_states + new_states 88 | return tree_search([start], goal_reached, get_successors, combine) 89 | 90 | 91 | ### Best-first search 92 | 93 | def best_first_search(start, goal_reached, get_successors, cost): 94 | """ 95 | A tree search where the state space is explored in order of "cost". 96 | 97 | That is, given a list of current states, the "cheapest" state (according 98 | to the function `cost`, which takes a state as input and returns a numerical 99 | cost value) is the next one explored. 100 | """ 101 | def combine(new_states, existing_states): 102 | # Keep the list of current states ordered by cost--the "cheapest" state 103 | # should always be at the front of the list. 104 | return sorted(new_states + existing_states, key=cost) 105 | return tree_search([start], goal_reached, get_successors, combine) 106 | 107 | 108 | ### Beam search 109 | 110 | def beam_search(start, goal_reached, get_successors, cost, beam_width): 111 | """ 112 | A tree search where the state space is explored by considering only the next 113 | `beam_width` cheapest states at any step. 114 | 115 | The downside to this approach is that by eliminating candidate states, the 116 | goal state might never be found! 117 | """ 118 | def combine(new_states, existing_states): 119 | # To combine new and current states, combine and sort them as in 120 | # `best_first_search`, but take only the first `beam_width` states. 121 | return sorted(new_states + existing_states, key=cost)[:beam_width] 122 | return tree_search([start], goal_reached, get_successors, combine) 123 | 124 | 125 | ### Iterative-widening search 126 | 127 | def widening_search(start, goal_reached, successors, cost, width=1, max=100): 128 | """ 129 | A tree search that repeatedly applies `beam_search` with incrementally 130 | increasing beam widths until the goal state is found. This strategy is more 131 | likely to find the goal state than a plain `beam_search`, but at the cost of 132 | exploring the state space more than once. 133 | 134 | `width` and `max` are the starting and maximum beam widths, respectively. 135 | """ 136 | if width > max: # only increment up to max 137 | return 138 | # `beam_search` with the starting width and quit if we've reached the goal. 139 | res = beam_search(start, goal_reached, successors, cost, width) 140 | if res: 141 | return res 142 | # Otherwise, `beam_search` again with a higher beam width. 143 | else: 144 | return widening_search(start, goal_reached, successors, cost, width + 1) 145 | 146 | 147 | # ----------------------------------------------------------------------------- 148 | ## Graph searches 149 | 150 | # For some problem domains, the state space is not really a tree--certain states 151 | # could form "cycles", where a successor of a current state is a state that has 152 | # been previously examined. 153 | # 154 | # The tree search algorithms we've discussed ignore this possibility and treat 155 | # every encountered state as distinct. This could lead to extra work, though, 156 | # as we re-explore certain branches. Graph search takes equivalent states into 157 | # account, keeps track of previously discarded states, and only explores states 158 | # that haven't already been encountered. 159 | 160 | ### The general case 161 | 162 | def graph_search(states, goal_reached, get_successors, combine, old_states=None): 163 | """ 164 | Given some initial states, explore a state space until reaching the goal, 165 | taking care not to re-explore previously visited states. 166 | 167 | `states`, `goal_reached`, `get_successors`, and `combine` are identical to 168 | those arguments in `tree_search`. 169 | `old_states` is a list of previously encountered states--these should not 170 | be re-vistited during the search. 171 | 172 | When the goal is reached, the goal state is returned. 173 | """ 174 | old_states = old_states or [] # initialize, if this is the initial call 175 | 176 | # Check for success and failure. 177 | if not states: 178 | return None 179 | if goal_reached(states[0]): 180 | return states[0] 181 | 182 | def visited(state): 183 | # A state is "visited" if it's in the list of current states or has 184 | # been encountered previously. 185 | return any(state == s for s in states + old_states) 186 | 187 | # Filter out the "visited" states from the next state's successors. 188 | new_states = [s for s in get_successors(states[0]) if not visited(s)] 189 | 190 | # Combine the new states with the existing ones and recurse. 191 | next_states = combine(new_states, states[1:]) 192 | return graph_search(next_states, goal_reached, get_successors, 193 | combine, old_states + [states[0]]) 194 | 195 | ### Exploration strategies 196 | 197 | # Just as for tree search, we can define special cases of graph search that use 198 | # specific exploration strategies: *breadth-first search* and *depth-first 199 | # search* are nearly identical as their tree-search varieties. 200 | 201 | def graph_search_bfs(start, goal_reached, get_successors, old_states=None): 202 | def combine(new_states, existing_states): 203 | return existing_states + new_states 204 | return graph_search([start], goal_reached, get_successors, combine, 205 | old_states) 206 | 207 | 208 | def graph_search_dfs(start, goal_reached, get_successors, old_states=None): 209 | def combine(new_states, existing_states): 210 | return new_states + existing_states 211 | return graph_search([start], goal_reached, get_successors, combine, 212 | old_states) 213 | 214 | 215 | # ----------------------------------------------------------------------------- 216 | ## Application: Pathfinding 217 | 218 | # A common use of searching is in finding the best path between two locations. 219 | # This might be useful for planning airline routes or video game character 220 | # movements. We will develop a specialized pathfinding algorithm that uses 221 | # graph search on path segments to find the shortest path between two points. 222 | 223 | ### Path utilities 224 | 225 | # We first develop some utilities for handling paths and path segments. 226 | 227 | class Path(object): 228 | """`Path` represents one segment of a path traversing a state space.""" 229 | def __init__(self, state, prev_path=None, cost=0): 230 | """ 231 | Create a new path segment by linking `state` to the branch indicated 232 | by `prev_path`, where the cost of the path up to (and including) `state` 233 | is `cost`. 234 | """ 235 | self.state = state 236 | self.prev_path = prev_path 237 | self.cost = cost 238 | 239 | def __repr__(self): 240 | return 'Path(%s, %s, %s)' % (self.state, self.prev_path, self.cost) 241 | 242 | def collect(self): 243 | states = [self.state] 244 | if self.prev_path: 245 | states = self.prev_path.collect() + states 246 | return states 247 | 248 | 249 | def find_path(to_state, paths): 250 | for path in paths: 251 | if to_state == path.state: 252 | return path 253 | 254 | 255 | def insert_path(path, paths, compare): 256 | """ 257 | When inserting a path into an existing list of paths, we want to keep the 258 | list sorted with respect to some comparison function `compare`, which 259 | takes two `Path`s as arguments and returns a number. 260 | """ 261 | for i in xrange(len(paths)): 262 | if compare(path, paths[i]) <= 0: 263 | paths.insert(i, path) 264 | return 265 | paths.append(path) 266 | 267 | 268 | def replace_if_better(path, compare, look_in, replace_in): 269 | """ 270 | Search the list `look_in` for a path that ends at the same state as `path`. 271 | If found, remove that existing path and insert `path` into the list 272 | `replace_in`. Returns True if replacement occurred and False otherwise. 273 | """ 274 | existing = find_path(path.state, look_in) 275 | if existing and compare(path, existing) < 0: 276 | look_in.remove(existing) 277 | insert_path(path, replace_in, compare) 278 | return True 279 | return False 280 | 281 | def extend_path(path, to_states, current_paths, old_paths, cost, compare): 282 | """ 283 | To grow the list of `current_paths` to include the states in `to_states`, 284 | we will extend `path` to each state (assuming the new paths are shorter than 285 | the ones that already exist). 286 | """ 287 | for state in to_states: 288 | # Extend `path` to each new state, updating the cost by adding the 289 | # cost of this extension to the existing path cost. 290 | extend_cost = path.cost + cost(path.state, state) 291 | extended = Path(state, path, extend_cost) 292 | 293 | # Replace any path in `current_paths` or `old_paths` that ends at 294 | # `state` if our new extended path is cheaper. 295 | if find_path(state, current_paths): 296 | replace_if_better(extended, compare, current_paths, current_paths) 297 | elif find_path(state, old_paths): 298 | replace_if_better(extended, compare, old_paths, current_paths) 299 | else: 300 | # If no existing path goes to `path`, just add our new one to the 301 | # end of `current_paths`. 302 | insert_path(extended, current_paths, compare) 303 | 304 | 305 | ### A* Search 306 | 307 | # A\* is a graph search that finds the shortest-path distance from a start state 308 | # to an end state. It works by incrementally extending paths from the start 309 | # state in order of cost and replacing previous paths when shorter ones are 310 | # found that reach the same state. 311 | 312 | # A heuristic function can be supplied to add additional cost to the cost of 313 | # each path; for standard A* search, this function measures the estimated 314 | # distance remaining from the end of a path to the desired goal state. 315 | # Supplying the zero function turns this into the well-known Dijkstra's 316 | # algorithm. 317 | 318 | def a_star(paths, goal_reached, get_successors, cost, heuristic, old_paths=None): 319 | """ 320 | Find the shortest path that satisfies `goal_reached`. The function 321 | `heuristic` can be used to specify an ordering strategy among equal-length 322 | paths. 323 | """ 324 | old_paths = old_paths or [] 325 | 326 | # First check to see if we're done. 327 | if not paths: 328 | return None 329 | if goal_reached(paths[0].state): 330 | return paths[0] 331 | 332 | # We will keep the lists of currently-exploring and previously-explored 333 | # paths ordered by cost, where the cost of a path is computed as the sum 334 | # of the costs of the path segments and the heuristic applied to the final 335 | # state in the path. 336 | def compare(path1, path2): 337 | return ((path1.cost + heuristic(path1.state)) - 338 | (path2.cost + heuristic(path2.state))) 339 | 340 | # At each step, we extend the shortest path we've encountered so far. 341 | path = paths.pop(0) 342 | 343 | # We keep track of all previously seen paths in `old_paths`, so that we can 344 | # weed out newly-extended paths that are no better than previously discovered 345 | # paths to the same state. 346 | insert_path(path, old_paths, compare) 347 | 348 | # Extend our shortest path to all its possible successor states using 349 | # `extend_path`, which will make sure that `paths` and `old_paths` stay 350 | # sorted appropriately and weed out redundant paths. 351 | extend_path(path, get_successors(path.state), paths, old_paths, cost, compare) 352 | 353 | # Repeat with the newly-extended paths. 354 | return a_star(paths, goal_reached, get_successors, cost, heuristic, old_paths) 355 | -------------------------------------------------------------------------------- /paip/tests/test_logic.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import unittest 3 | from paip import logic 4 | 5 | 6 | class VarTests(unittest.TestCase): 7 | def test_lookup_none(self): 8 | bindings = {} 9 | var = logic.Var('x') 10 | self.assertEqual(None, var.lookup(bindings)) 11 | 12 | def test_lookup_immediate(self): 13 | x = logic.Var('x') 14 | y = logic.Atom('y') 15 | bindings = {x: y} 16 | self.assertEqual(y, x.lookup(bindings)) 17 | 18 | def test_lookup_search(self): 19 | x = logic.Var('x') 20 | y = logic.Var('y') 21 | z = logic.Var('z') 22 | w = logic.Atom('w') 23 | bindings = { 24 | x: y, 25 | y: z, 26 | z: w 27 | } 28 | self.assertEqual(w, x.lookup(bindings)) 29 | 30 | def test_lookup_search_no_atom(self): 31 | x = logic.Var('x') 32 | y = logic.Var('y') 33 | z = logic.Var('z') 34 | bindings = { 35 | x: y, 36 | y: z, 37 | } 38 | self.assertEqual(z, x.lookup(bindings)) 39 | 40 | def test_rename_vars(self): 41 | v1 = logic.Var('x') 42 | begin = logic.Var.counter 43 | v2 = v1.rename_vars({v1: logic.Var.get_unused_var()}) 44 | 45 | self.assertEqual(logic.Var('var%d' % begin), v2) 46 | self.assertEqual(begin + 1, logic.Var.counter) 47 | 48 | def test_get_vars(self): 49 | x = logic.Var('x') 50 | self.assertEqual([x], x.get_vars()) 51 | 52 | 53 | class RelationTests(unittest.TestCase): 54 | def test_bind_vars(self): 55 | a = logic.Atom('a') 56 | b = logic.Atom('b') 57 | x = logic.Var('x') 58 | y = logic.Var('y') 59 | 60 | r = logic.Relation('likes', (a, x, y)) 61 | s = logic.Relation('likes', (a, b, y)) 62 | bindings = { x: b } 63 | self.assertEqual(s, r.bind_vars(bindings)) 64 | 65 | def test_rename_vars(self): 66 | a = logic.Atom('a') 67 | x = logic.Var('x') 68 | y = logic.Var('y') 69 | p2 = logic.Relation('pair', (a, y)) 70 | p1 = logic.Relation('pair', (x, p2)) 71 | vs = p1.get_vars() 72 | 73 | begin = logic.Var.counter 74 | rep = {v: logic.Var.get_unused_var() for v in vs} 75 | 76 | u = logic.Var('var%d' % begin) 77 | v = logic.Var('var%d' % (begin+1)) 78 | r = logic.Relation('pair', [u, logic.Relation('pair', [a, v])]) 79 | self.assertEqual(r, p1.rename_vars(rep)) 80 | 81 | def test_rename_repeated_var(self): 82 | x = logic.Var('x') 83 | y = logic.Var('y') 84 | r = logic.Relation('likes', (x, x)) 85 | s = logic.Relation('likes', (y, y)) 86 | self.assertEqual(s, r.rename_vars({x: y})) 87 | 88 | def test_get_vars(self): 89 | a = logic.Atom('a') 90 | x = logic.Var('x') 91 | y = logic.Var('y') 92 | p3 = logic.Relation('pair', (x, x)) 93 | p2 = logic.Relation('pair', (a, p3)) 94 | p1 = logic.Relation('pair', (y, p2)) 95 | self.assertEqual(set([x, y]), set(p1.get_vars())) 96 | 97 | 98 | class ClauseTests(unittest.TestCase): 99 | def test_bind_vars(self): 100 | a = logic.Atom('a') 101 | b = logic.Atom('b') 102 | c = logic.Atom('c') 103 | x = logic.Var('x') 104 | y = logic.Var('y') 105 | z = logic.Var('z') 106 | 107 | r = logic.Relation('likes', (x, y, a)) 108 | s = logic.Relation('likes', (y, a, z)) 109 | t = logic.Relation('hates', (z, b, x)) 110 | 111 | bindings = { z: c, y: b, x: y } 112 | cl1 = logic.Clause(r, (s, t)) 113 | cl2 = logic.Clause( 114 | logic.Relation('likes', (b, b, a)), 115 | (logic.Relation('likes', (b, a, c)), 116 | logic.Relation('hates', (c, b, b)))) 117 | 118 | self.assertEqual(cl2, cl1.bind_vars(bindings)) 119 | 120 | def test_rename_vars(self): 121 | x = logic.Var('x') 122 | y = logic.Var('y') 123 | z = logic.Var('z') 124 | p = logic.Relation('pair', (y, logic.Relation('pair', (x, z)))) 125 | is_member = logic.Relation('member', (x, p)) 126 | is_list = logic.Relation('is_list', [p]) 127 | rule = logic.Clause(is_member, (is_list, p)) 128 | 129 | vs = rule.get_vars() 130 | begin = logic.Var.counter 131 | renames = {v: logic.Var.get_unused_var() for v in vs} 132 | rule2 = rule.rename_vars(renames) 133 | 134 | newx = renames[x] 135 | newy = renames[y] 136 | newz = renames[z] 137 | new_list = logic.Relation('pair', 138 | (newy, logic.Relation('pair', (newx, newz)))) 139 | rule3 = logic.Clause(logic.Relation('member', (newx, new_list)), 140 | (logic.Relation('is_list', [new_list]), new_list)) 141 | 142 | self.assertEqual(rule3, rule2) 143 | 144 | def test_recursive_rename(self): 145 | list = logic.Var('list') 146 | x = logic.Var('x') 147 | y = logic.Var('y') 148 | z = logic.Var('z') 149 | 150 | member = logic.Clause(logic.Relation('member', (x, list)), 151 | [logic.Relation('first', (list, y)), 152 | logic.Relation('rest', (list, z)), 153 | logic.Relation('member', (x, z))]) 154 | 155 | renamed = member.recursive_rename() 156 | bindings = logic.unify(renamed, member, {}) 157 | 158 | self.assertTrue(x in bindings or x in bindings.values()) 159 | self.assertTrue(y in bindings or y in bindings.values()) 160 | self.assertTrue(z in bindings or z in bindings.values()) 161 | 162 | def test_get_vars(self): 163 | a = logic.Atom('a') 164 | b = logic.Atom('b') 165 | x = logic.Var('x') 166 | y = logic.Var('y') 167 | z = logic.Var('z') 168 | r = logic.Relation('likes', (a, x)) 169 | s = logic.Relation('likes', (y, b)) 170 | t = logic.Relation('hates', (x, z)) 171 | c = logic.Clause(r, (s, t)) 172 | self.assertEqual(set([x, y, z]), set(c.get_vars())) 173 | 174 | 175 | class UnificationTests(unittest.TestCase): 176 | def test_atom_atom_ok(self): 177 | a = logic.Atom('a') 178 | self.assertEqual({}, logic.unify(a, a, {})) 179 | 180 | def test_atom_atom_fail(self): 181 | a = logic.Atom('a') 182 | b = logic.Atom('b') 183 | self.assertFalse(logic.unify(a, b, {})) 184 | 185 | def test_atom_var_exists_ok(self): 186 | a = logic.Atom('a') 187 | x = logic.Var('x') 188 | bindings = {x: a} 189 | self.assertEqual(bindings, logic.unify(a, x, bindings)) 190 | 191 | def test_atom_var_exists_fail(self): 192 | a = logic.Atom('a') 193 | b = logic.Atom('b') 194 | x = logic.Var('x') 195 | bindings = {x: b} 196 | self.assertFalse(logic.unify(a, x, bindings)) 197 | 198 | def test_atom_var_new(self): 199 | a = logic.Atom('a') 200 | x = logic.Var('x') 201 | self.assertEqual({x: a}, logic.unify(a, x, {})) 202 | 203 | def test_var_var_both_unbound(self): 204 | x = logic.Var('x') 205 | y = logic.Var('y') 206 | self.assertEqual({x: y}, logic.unify(x, y, {})) 207 | 208 | def test_var_var_left_unbound(self): 209 | x = logic.Var('x') 210 | y = logic.Var('y') 211 | a = logic.Atom('a') 212 | bindings = {x: a} 213 | self.assertEqual({x: a, y: a}, logic.unify(y, x, bindings)) 214 | 215 | def test_var_var_right_unbound(self): 216 | x = logic.Var('x') 217 | y = logic.Var('y') 218 | a = logic.Atom('a') 219 | bindings = {x: a} 220 | self.assertEqual({x: a, y: a}, logic.unify(x, y, bindings)) 221 | 222 | def test_var_var_both_bound_equal(self): 223 | x = logic.Var('x') 224 | y = logic.Var('y') 225 | a = logic.Atom('a') 226 | bindings = {x: a, y: a} 227 | self.assertEqual(bindings, logic.unify(x, y, bindings)) 228 | 229 | def test_var_var_both_bound_unequal(self): 230 | x = logic.Var('x') 231 | y = logic.Var('y') 232 | a = logic.Atom('a') 233 | b = logic.Atom('b') 234 | bindings = {x: a, y: b} 235 | self.assertFalse(logic.unify(x, y, bindings)) 236 | 237 | def test_var_relation(self): 238 | x = logic.Var('x') 239 | r = logic.Relation('foo', (logic.Var('bar'), logic.Atom('baz'))) 240 | bindings = {x: r} 241 | self.assertEqual(bindings, logic.unify(x, r, {})) 242 | 243 | def test_var_var_resolves_to_relation(self): 244 | x = logic.Var('x') 245 | y = logic.Var('y') 246 | r = logic.Relation('foo', (logic.Var('bar'), logic.Atom('baz'))) 247 | bindings = {x: r, y: r} 248 | self.assertEqual(bindings, logic.unify(x, y, {y: r})) 249 | 250 | def test_var_resolves_to_relation_var(self): 251 | x = logic.Var('x') 252 | y = logic.Var('y') 253 | r = logic.Relation('foo', (logic.Var('bar'), logic.Atom('baz'))) 254 | bindings = {y: r, x: r} 255 | self.assertEqual(bindings, logic.unify(x, y, {x: r})) 256 | 257 | def test_var_var_both_resolve_to_relations(self): 258 | x = logic.Var('x') 259 | y = logic.Var('y') 260 | bar = logic.Var('bar') 261 | baz = logic.Atom('baz') 262 | b = logic.Atom('b') 263 | c = logic.Var('c') 264 | r = logic.Relation('foo', (bar, baz)) 265 | s = logic.Relation('foo', (b, c)) 266 | bindings = {x: r, y: s, bar: b, c: baz} 267 | self.assertEqual(bindings, logic.unify(x, y, {x: r, y: s})) 268 | 269 | def test_relation_relation_different_preds(self): 270 | x = logic.Var('x') 271 | y = logic.Var('y') 272 | a = logic.Atom('a') 273 | r = logic.Relation('likes', (x, y)) 274 | s = logic.Relation('loves', (x, a)) 275 | self.assertFalse(logic.unify(r, s, {})) 276 | 277 | def test_relation_relation_different_lengths(self): 278 | x = logic.Var('x') 279 | y = logic.Var('y') 280 | a = logic.Atom('a') 281 | r = logic.Relation('likes', (x, y)) 282 | s = logic.Relation('likes', (x, a, y)) 283 | self.assertFalse(logic.unify(r, s, {})) 284 | 285 | def test_relation_relation_different_args(self): 286 | x = logic.Var('x') 287 | y = logic.Var('y') 288 | a = logic.Atom('a') 289 | b = logic.Atom('b') 290 | r = logic.Relation('likes', (x, a)) 291 | s = logic.Relation('likes', (y, b)) 292 | self.assertFalse(logic.unify(r, s, {})) 293 | 294 | def test_relation_relation_ok(self): 295 | x = logic.Var('x') 296 | y = logic.Var('y') 297 | a = logic.Atom('a') 298 | b = logic.Atom('b') 299 | r = logic.Relation('likes', (x, y)) 300 | s = logic.Relation('likes', (a, x)) 301 | self.assertEqual({x: a, y: a}, logic.unify(r, s, {})) 302 | 303 | def test_clauses_different_heads(self): 304 | joe = logic.Atom('joe') 305 | judy = logic.Atom('judy') 306 | jorge = logic.Atom('jorge') 307 | x = logic.Var('x') 308 | r = logic.Relation('likes', (joe, x)) 309 | s = logic.Relation('likes', (joe, judy)) 310 | t = logic.Relation('hates', (x, jorge)) 311 | c = logic.Clause(r, [s]) 312 | d = logic.Clause(t, [s]) 313 | self.assertFalse(logic.unify(c, d, {})) 314 | 315 | def test_clauses_different_length_bodies(self): 316 | joe = logic.Atom('joe') 317 | judy = logic.Atom('judy') 318 | jorge = logic.Atom('jorge') 319 | x = logic.Var('x') 320 | y = logic.Var('y') 321 | r = logic.Relation('likes', (joe, x)) 322 | s = logic.Relation('hates', (joe, judy)) 323 | t = logic.Relation('likes', (y, jorge)) 324 | u = logic.Relation('hates', (joe, jorge)) 325 | c = logic.Clause(r, [s]) 326 | d = logic.Clause(t, [s, u]) 327 | self.assertFalse(logic.unify(c, d, {})) 328 | 329 | def test_clauses_different_bodies(self): 330 | joe = logic.Atom('joe') 331 | judy = logic.Atom('judy') 332 | jorge = logic.Atom('jorge') 333 | x = logic.Var('x') 334 | y = logic.Var('y') 335 | r = logic.Relation('likes', (joe, x)) 336 | s = logic.Relation('hates', (joe, judy)) 337 | t = logic.Relation('likes', (y, jorge)) 338 | u = logic.Relation('hates', (judy, joe)) 339 | v = logic.Relation('hates', (judy, x)) 340 | c = logic.Clause(r, [s, v]) 341 | d = logic.Clause(t, [s, u]) 342 | self.assertFalse(logic.unify(c, d, {})) 343 | 344 | def test_clauses_ok(self): 345 | joe = logic.Atom('joe') 346 | judy = logic.Atom('judy') 347 | jorge = logic.Atom('jorge') 348 | x = logic.Var('x') 349 | y = logic.Var('y') 350 | r = logic.Relation('likes', (joe, x)) 351 | s = logic.Relation('hates', (joe, judy)) 352 | t = logic.Relation('likes', (y, jorge)) 353 | u = logic.Relation('hates', (judy, joe)) 354 | v = logic.Relation('hates', (judy, y)) 355 | c = logic.Clause(r, [s, v]) 356 | d = logic.Clause(t, [s, u]) 357 | self.assertEqual({x: jorge, y: joe}, logic.unify(c, d, {})) 358 | 359 | 360 | class ProveTests(unittest.TestCase): 361 | def test_prove_no_relevant_clauses(self): 362 | joe = logic.Atom('joe') 363 | judy = logic.Atom('judy') 364 | jorge = logic.Atom('jorge') 365 | x = logic.Var('x') 366 | 367 | db = {'likes': []} 368 | db['likes'].append(logic.Clause(logic.Relation('likes', (joe, x)), 369 | [logic.Relation('likes', (x, joe)), 370 | logic.Relation('hates', (judy, x))])) 371 | db['likes'].append(logic.Clause(logic.Relation('likes', (jorge, judy)))) 372 | 373 | goal = logic.Relation('hates', (joe, x)) 374 | bindings = logic.prove(goal, {}, db) 375 | self.assertFalse(bindings) 376 | 377 | def test_prove_no_subgoals_required(self): 378 | joe = logic.Atom('joe') 379 | judy = logic.Atom('judy') 380 | jorge = logic.Atom('jorge') 381 | x = logic.Var('x') 382 | 383 | db = {'likes': []} 384 | db['likes'].append(logic.Clause(logic.Relation('likes', (joe, x)), 385 | [logic.Relation('likes', (x, joe)), 386 | logic.Relation('hates', (judy, x))])) 387 | db['likes'].append(logic.Clause(logic.Relation('likes', (jorge, judy)))) 388 | 389 | goal = logic.Relation('likes', (jorge, x)) 390 | bindings = logic.prove(goal, {}, db) 391 | self.assertEqual({x: judy}, bindings) 392 | 393 | def test_prove_all_no_subgoals_required(self): 394 | joe = logic.Atom('joe') 395 | judy = logic.Atom('judy') 396 | jorge = logic.Atom('jorge') 397 | x = logic.Var('x') 398 | 399 | db = {'likes': [], 'hates': []} 400 | db['likes'].append(logic.Clause(logic.Relation('likes', (joe, x)), 401 | [logic.Relation('likes', (x, joe)), 402 | logic.Relation('hates', (judy, x))])) 403 | db['likes'].append(logic.Clause(logic.Relation('likes', (jorge, joe)))) 404 | db['hates'].append(logic.Clause(logic.Relation('hates', (judy, jorge)))) 405 | 406 | goal1 = logic.Relation('likes', (x, joe)) 407 | goal2 = logic.Relation('hates', (judy, x)) 408 | bindings = logic.prove_all([goal1, goal2], {}, db) 409 | self.assertEqual({x: jorge}, bindings) 410 | 411 | def test_prove_subgoals_required_fail(self): 412 | joe = logic.Atom('joe') 413 | judy = logic.Atom('judy') 414 | jorge = logic.Atom('jorge') 415 | x = logic.Var('x') 416 | 417 | db = {'likes': [], 'hates': []} 418 | db['likes'].append(logic.Clause(logic.Relation('likes', (joe, x)), 419 | [logic.Relation('likes', (x, joe)), 420 | logic.Relation('hates', (judy, x))])) 421 | db['likes'].append(logic.Clause(logic.Relation('likes', (jorge, joe)))) 422 | db['hates'].append(logic.Clause(logic.Relation('hates', (judy, joe)))) 423 | 424 | goal = logic.Relation('likes', (joe, jorge)) 425 | bindings = logic.prove(goal, {}, db) 426 | self.assertFalse(bindings) 427 | 428 | def test_prove_subgoals_required_pass(self): 429 | joe = logic.Atom('joe') 430 | judy = logic.Atom('judy') 431 | jorge = logic.Atom('jorge') 432 | x = logic.Var('x') 433 | 434 | db = {'likes': [], 'hates': []} 435 | db['likes'].append(logic.Clause(logic.Relation('likes', (joe, x)), 436 | [logic.Relation('likes', (x, joe)), 437 | logic.Relation('hates', (judy, x))])) 438 | db['likes'].append(logic.Clause(logic.Relation('likes', (jorge, joe)))) 439 | db['hates'].append(logic.Clause(logic.Relation('hates', (judy, jorge)))) 440 | 441 | goal = logic.Relation('likes', (joe, x)) 442 | bindings = logic.prove(goal, {}, db) 443 | self.assertEqual(jorge, x.lookup(bindings)) 444 | 445 | def test_prove_primitive_call(self): 446 | joe = logic.Atom('joe') 447 | judy = logic.Atom('judy') 448 | jorge = logic.Atom('jorge') 449 | x = logic.Var('x') 450 | 451 | db = {'likes': [], 'hates': []} 452 | db['likes'].append(logic.Clause(logic.Relation('likes', (joe, x)), 453 | [logic.Relation('likes', (x, joe)), 454 | logic.Relation('hates', (judy, x))])) 455 | db['likes'].append(logic.Clause(logic.Relation('likes', (jorge, joe)))) 456 | db['hates'].append(logic.Clause(logic.Relation('hates', (judy, jorge)))) 457 | 458 | things = [] 459 | def prim(a, b, c, d): 460 | things.append(a) 461 | db['prim'] = prim 462 | 463 | goal = logic.Relation('likes', (joe, x)) 464 | display = logic.Relation('prim', 'foo') 465 | 466 | bindings = logic.prove_all([goal, display], {}, db) 467 | self.assertEqual(['foo'], things) 468 | -------------------------------------------------------------------------------- /paip/othello.py: -------------------------------------------------------------------------------- 1 | """ 2 | **Othello** is a turn-based two-player strategy board game. The players take 3 | turns placing pieces--one player white and the other player black--on an 8x8 4 | board in such a way that captures some of the opponent's pieces, with the goal 5 | of finishing the game with more pieces of their color on the board. 6 | 7 | Every move must capture one more more of the opponent's pieces. To capture, 8 | player A places a piece adjacent to one of player B's pieces so that there is a 9 | straight line (horizontal, vertical, or diagonal) of adjacent pieces that begins 10 | with one of player A's pieces, continues with one more more of player B's 11 | pieces, and ends with one of player A's pieces. 12 | 13 | For example, if Black places a piece on square (5, 1), he will capture all of 14 | Black's pieces between (5, 1) and (5, 6): 15 | 16 | 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 17 | 1 . . . . . . . . 1 . . . . . . . . 18 | 2 . . . . . . . . 2 . . . . . . . . 19 | 3 . . o @ . o . . 3 . . o @ . o . . 20 | 4 . . o o @ @ . . 4 . . o o @ @ . . 21 | 5 . o o o o @ . . 5 @ @ @ @ @ @ . . 22 | 6 . . . @ o . . . 6 . . . @ o . . . 23 | 7 . . . . . . . . 7 . . . . . . . . 24 | 8 . . . . . . . . 8 . . . . . . . . 25 | 26 | For more more information about the game (which is also known as Reversi) 27 | including detailed rules, see the entry on [Wikipedia][wiki]. Additionally, 28 | this implementation doesn't take into account some tournament-style Othello 29 | details, such as game time limits and a different indexing scheme. 30 | 31 | We will implement representations for the board and pieces and the mechanics of 32 | playing a game. We will then explore several game-playing strategies. There is 33 | a simple command-line program [provided](examples/othello/othello.html) for 34 | playing against the computer or comparing two strategies. 35 | 36 | Written by [Daniel Connelly](http://dhconnelly.com). This implementation follows 37 | chapter 18 of Peter Norvig's "Paradigms of Artificial Intelligence". 38 | 39 | [wiki]: http://en.wikipedia.org/wiki/Reversi 40 | 41 | """ 42 | 43 | # ----------------------------------------------------------------------------- 44 | ## Table of contents 45 | 46 | # 1. [Board representation](#board) 47 | # 2. [Playing the game](#playing) 48 | # 3. [Strategies](#strategies) 49 | # - [Random](#random)
50 | # - [Local maximization](#localmax)
51 | # - [Minimax search](#minimax)
52 | # - [Alpha-beta search](#alphabeta)
53 | # 4. [Conclusion](#conclusion) 54 | 55 | 56 | # ----------------------------------------------------------------------------- 57 | # 58 | ## Board representation 59 | 60 | # We represent the board as a 100-element list, which includes each square on 61 | # the board as well as the outside edge. Each consecutive sublist of ten 62 | # elements represents a single row, and each list element stores a piece. An 63 | # initial board contains four pieces in the center: 64 | 65 | # ? ? ? ? ? ? ? ? ? ? 66 | # ? . . . . . . . . ? 67 | # ? . . . . . . . . ? 68 | # ? . . . . . . . . ? 69 | # ? . . . o @ . . . ? 70 | # ? . . . @ o . . . ? 71 | # ? . . . . . . . . ? 72 | # ? . . . . . . . . ? 73 | # ? . . . . . . . . ? 74 | # ? ? ? ? ? ? ? ? ? ? 75 | 76 | # This representation has two useful properties: 77 | # 78 | # 1. Square (m,n) can be accessed as `board[mn]`. This avoids the need to write 79 | # functions that convert between square locations and list indexes. 80 | # 2. Operations involving bounds checking are slightly simpler. 81 | 82 | # The outside edge is marked ?, empty squares are ., black is @, and white is o. 83 | # The black and white pieces represent the two players. 84 | EMPTY, BLACK, WHITE, OUTER = '.', '@', 'o', '?' 85 | PIECES = (EMPTY, BLACK, WHITE, OUTER) 86 | PLAYERS = {BLACK: 'Black', WHITE: 'White'} 87 | 88 | # To refer to neighbor squares we can add a direction to a square. 89 | UP, DOWN, LEFT, RIGHT = -10, 10, -1, 1 90 | UP_RIGHT, DOWN_RIGHT, DOWN_LEFT, UP_LEFT = -9, 11, 9, -11 91 | DIRECTIONS = (UP, UP_RIGHT, RIGHT, DOWN_RIGHT, DOWN, DOWN_LEFT, LEFT, UP_LEFT) 92 | 93 | def squares(): 94 | """List all the valid squares on the board.""" 95 | return [i for i in xrange(11, 89) if 1 <= (i % 10) <= 8] 96 | 97 | def initial_board(): 98 | """Create a new board with the initial black and white positions filled.""" 99 | board = [OUTER] * 100 100 | for i in squares(): 101 | board[i] = EMPTY 102 | # The middle four squares should hold the initial piece positions. 103 | board[44], board[45] = WHITE, BLACK 104 | board[54], board[55] = BLACK, WHITE 105 | return board 106 | 107 | def print_board(board): 108 | """Get a string representation of the board.""" 109 | rep = '' 110 | rep += ' %s\n' % ' '.join(map(str, range(1, 9))) 111 | for row in xrange(1, 9): 112 | begin, end = 10*row + 1, 10*row + 9 113 | rep += '%d %s\n' % (row, ' '.join(board[begin:end])) 114 | return rep 115 | 116 | 117 | # ----------------------------------------------------------------------------- 118 | # 119 | ## Playing the game 120 | 121 | # We need functions to get moves from players, check to make sure that the moves 122 | # are legal, apply the moves to the board, and detect when the game is over. 123 | 124 | ### Checking moves 125 | 126 | # A move must be both valid and legal: it must refer to a real square, and it 127 | # must form a bracket with another piece of the same color with pieces of the 128 | # opposite color in between. 129 | 130 | def is_valid(move): 131 | """Is move a square on the board?""" 132 | return isinstance(move, int) and move in squares() 133 | 134 | def opponent(player): 135 | """Get player's opponent piece.""" 136 | return BLACK if player is WHITE else WHITE 137 | 138 | def find_bracket(square, player, board, direction): 139 | """ 140 | Find a square that forms a bracket with `square` for `player` in the given 141 | `direction`. Returns None if no such square exists. 142 | """ 143 | bracket = square + direction 144 | if board[bracket] == player: 145 | return None 146 | opp = opponent(player) 147 | while board[bracket] == opp: 148 | bracket += direction 149 | return None if board[bracket] in (OUTER, EMPTY) else bracket 150 | 151 | def is_legal(move, player, board): 152 | """Is this a legal move for the player?""" 153 | hasbracket = lambda direction: find_bracket(move, player, board, direction) 154 | return board[move] == EMPTY and any(map(hasbracket, DIRECTIONS)) 155 | 156 | ### Making moves 157 | 158 | # When the player makes a move, we need to update the board and flip all the 159 | # bracketed pieces. 160 | 161 | def make_move(move, player, board): 162 | """Update the board to reflect the move by the specified player.""" 163 | board[move] = player 164 | for d in DIRECTIONS: 165 | make_flips(move, player, board, d) 166 | return board 167 | 168 | def make_flips(move, player, board, direction): 169 | """Flip pieces in the given direction as a result of the move by player.""" 170 | bracket = find_bracket(move, player, board, direction) 171 | if not bracket: 172 | return 173 | square = move + direction 174 | while square != bracket: 175 | board[square] = player 176 | square += direction 177 | 178 | ### Monitoring players 179 | 180 | class IllegalMoveError(Exception): 181 | def __init__(self, player, move, board): 182 | self.player = player 183 | self.move = move 184 | self.board = board 185 | 186 | def __str__(self): 187 | return '%s cannot move to square %d' % (PLAYERS[self.player], self.move) 188 | 189 | def legal_moves(player, board): 190 | """Get a list of all legal moves for player.""" 191 | return [sq for sq in squares() if is_legal(sq, player, board)] 192 | 193 | def any_legal_move(player, board): 194 | """Can player make any moves?""" 195 | return any(is_legal(sq, player, board) for sq in squares()) 196 | 197 | ### Putting it all together 198 | 199 | # Each round consists of: 200 | # 201 | # - Get a move from the current player. 202 | # - Apply it to the board. 203 | # - Switch players. If the game is over, get the final score. 204 | 205 | def play(black_strategy, white_strategy): 206 | """Play a game of Othello and return the final board and score.""" 207 | board = initial_board() 208 | player = BLACK 209 | strategy = lambda who: black_strategy if who == BLACK else white_strategy 210 | while player is not None: 211 | move = get_move(strategy(player), player, board) 212 | make_move(move, player, board) 213 | player = next_player(board, player) 214 | return board, score(BLACK, board) 215 | 216 | def next_player(board, prev_player): 217 | """Which player should move next? Returns None if no legal moves exist.""" 218 | opp = opponent(prev_player) 219 | if any_legal_move(opp, board): 220 | return opp 221 | elif any_legal_move(prev_player, board): 222 | return prev_player 223 | return None 224 | 225 | def get_move(strategy, player, board): 226 | """Call strategy(player, board) to get a move.""" 227 | copy = list(board) # copy the board to prevent cheating 228 | move = strategy(player, copy) 229 | if not is_valid(move) or not is_legal(move, player, board): 230 | raise IllegalMoveError(player, move, copy) 231 | return move 232 | 233 | def score(player, board): 234 | """Compute player's score (number of player's pieces minus opponent's).""" 235 | mine, theirs = 0, 0 236 | opp = opponent(player) 237 | for sq in squares(): 238 | piece = board[sq] 239 | if piece == player: mine += 1 240 | elif piece == opp: theirs += 1 241 | return mine - theirs 242 | 243 | 244 | # ----------------------------------------------------------------------------- 245 | # 246 | ## Play strategies 247 | 248 | # 249 | ### Random 250 | 251 | # The easiest strategy to implement simply picks a move at random. 252 | 253 | import random 254 | 255 | def random_strategy(player, board): 256 | """A strategy that always chooses a random legal move.""" 257 | return random.choice(legal_moves(player, board)) 258 | 259 | # 260 | ### Local maximization 261 | 262 | # A more sophisticated strategy could look at every available move and evaluate 263 | # them in some way. This consists of getting a list of legal moves, applying 264 | # each one to a copy of the board, and choosing the move that results in the 265 | # "best" board. 266 | 267 | def maximizer(evaluate): 268 | """ 269 | Construct a strategy that chooses the best move by maximizing 270 | evaluate(player, board) over all boards resulting from legal moves. 271 | """ 272 | def strategy(player, board): 273 | def score_move(move): 274 | return evaluate(player, make_move(move, player, list(board))) 275 | return max(legal_moves(player, board), key=score_move) 276 | return strategy 277 | 278 | # One possible evaluation function is `score`. A strategy constructed with 279 | # `maximizer(score)` will always make the move that results in the largest 280 | # immediate gain in pieces. 281 | 282 | # A more advanced evaluation function might consider the relative worth of each 283 | # square on the board and weight the score by the value of the pieces held by 284 | # each player. Since corners and (most) edge squares are very valuable, we 285 | # could weight those more heavily, and add negative weights to the squares that, 286 | # if acquired, could lead to the opponent capturing the corners or edges. 287 | 288 | SQUARE_WEIGHTS = [ 289 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 290 | 0, 120, -20, 20, 5, 5, 20, -20, 120, 0, 291 | 0, -20, -40, -5, -5, -5, -5, -40, -20, 0, 292 | 0, 20, -5, 15, 3, 3, 15, -5, 20, 0, 293 | 0, 5, -5, 3, 3, 3, 3, -5, 5, 0, 294 | 0, 5, -5, 3, 3, 3, 3, -5, 5, 0, 295 | 0, 20, -5, 15, 3, 3, 15, -5, 20, 0, 296 | 0, -20, -40, -5, -5, -5, -5, -40, -20, 0, 297 | 0, 120, -20, 20, 5, 5, 20, -20, 120, 0, 298 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 299 | ] 300 | 301 | # A strategy constructed as `maximizer(weighted_score)`, then, will always 302 | # return the move that results in the largest immediate *weighted* gain in 303 | # pieces. 304 | 305 | def weighted_score(player, board): 306 | """ 307 | Compute the difference between the sum of the weights of player's 308 | squares and the sum of the weights of opponent's squares. 309 | """ 310 | opp = opponent(player) 311 | total = 0 312 | for sq in squares(): 313 | if board[sq] == player: 314 | total += SQUARE_WEIGHTS[sq] 315 | elif board[sq] == opp: 316 | total -= SQUARE_WEIGHTS[sq] 317 | return total 318 | 319 | # 320 | ### Minimax search 321 | 322 | # The maximizer strategies are very short-sighted, and a player who can consider 323 | # the implications of a move several turns in advance could have a significant 324 | # advantage. The **minimax** algorithm does just that. 325 | 326 | def minimax(player, board, depth, evaluate): 327 | """ 328 | Find the best legal move for player, searching to the specified depth. 329 | Returns a tuple (move, min_score), where min_score is the guaranteed minimum 330 | score achievable for player if the move is made. 331 | """ 332 | 333 | # We define the value of a board to be the opposite of its value to our 334 | # opponent, computed by recursively applying `minimax` for our opponent. 335 | def value(board): 336 | return -minimax(opponent(player), board, depth-1, evaluate)[0] 337 | 338 | # When depth is zero, don't examine possible moves--just determine the value 339 | # of this board to the player. 340 | if depth == 0: 341 | return evaluate(player, board), None 342 | 343 | # We want to evaluate all the legal moves by considering their implications 344 | # `depth` turns in advance. First, find all the legal moves. 345 | moves = legal_moves(player, board) 346 | 347 | # If player has no legal moves, then either: 348 | if not moves: 349 | # the game is over, so the best achievable score is victory or defeat; 350 | if not any_legal_move(opponent(player), board): 351 | return final_value(player, board), None 352 | # or we have to pass this turn, so just find the value of this board. 353 | return value(board), None 354 | 355 | # When there are multiple legal moves available, choose the best one by 356 | # maximizing the value of the resulting boards. 357 | return max((value(make_move(m, player, list(board))), m) for m in moves) 358 | 359 | # Values for endgame boards are big constants. 360 | MAX_VALUE = sum(map(abs, SQUARE_WEIGHTS)) 361 | MIN_VALUE = -MAX_VALUE 362 | 363 | def final_value(player, board): 364 | """The game is over--find the value of this board to player.""" 365 | diff = score(player, board) 366 | if diff < 0: 367 | return MIN_VALUE 368 | elif diff > 0: 369 | return MAX_VALUE 370 | return diff 371 | 372 | def minimax_searcher(depth, evaluate): 373 | """ 374 | Construct a strategy that uses `minimax` with the specified leaf board 375 | evaluation function. 376 | """ 377 | def strategy(player, board): 378 | return minimax(player, board, depth, evaluate)[1] 379 | return strategy 380 | 381 | # 382 | ### Alpha-Beta search 383 | 384 | # Minimax is very effective, but it does too much work: it evaluates many search 385 | # trees that should be ignored. 386 | 387 | # Consider what happens when minimax is evaluating two moves, M1 and M2, on one 388 | # level of a search tree. Suppose minimax determines that M1 can result in a 389 | # score of S. While evaluating M2, if minimax finds a move in its subtree that 390 | # could result in a better score than S, the algorithm should immediately quit 391 | # evaluating M2: the opponent will force us to play M1 to avoid the higher score 392 | # resulting from M1, so we shouldn't waste time determining just how much better 393 | # M2 is than M1. 394 | 395 | # We need to keep track of two values: 396 | # 397 | # - alpha: the maximum score achievable by any of the moves we have encountered. 398 | # - beta: the score that the opponent can keep us under by playing other moves. 399 | # 400 | # When the algorithm begins, alpha is the smallest value and beta is the largest 401 | # value. During evaluation, if we find a move that causes `alpha >= beta`, then 402 | # we can quit searching this subtree since the opponent can prevent us from 403 | # playing it. 404 | 405 | def alphabeta(player, board, alpha, beta, depth, evaluate): 406 | """ 407 | Find the best legal move for player, searching to the specified depth. Like 408 | minimax, but uses the bounds alpha and beta to prune branches. 409 | """ 410 | if depth == 0: 411 | return evaluate(player, board), None 412 | 413 | def value(board, alpha, beta): 414 | # Like in `minimax`, the value of a board is the opposite of its value 415 | # to the opponent. We pass in `-beta` and `-alpha` as the alpha and 416 | # beta values, respectively, for the opponent, since `alpha` represents 417 | # the best score we know we can achieve and is therefore the worst score 418 | # achievable by the opponent. Similarly, `beta` is the worst score that 419 | # our opponent can hold us to, so it is the best score that they can 420 | # achieve. 421 | return -alphabeta(opponent(player), board, -beta, -alpha, depth-1, evaluate)[0] 422 | 423 | moves = legal_moves(player, board) 424 | if not moves: 425 | if not any_legal_move(opponent(player), board): 426 | return final_value(player, board), None 427 | return value(board, alpha, beta), None 428 | 429 | best_move = moves[0] 430 | for move in moves: 431 | if alpha >= beta: 432 | # If one of the legal moves leads to a better score than beta, then 433 | # the opponent will avoid this branch, so we can quit looking. 434 | break 435 | val = value(make_move(move, player, list(board)), alpha, beta) 436 | if val > alpha: 437 | # If one of the moves leads to a better score than the current best 438 | # achievable score, then replace it with this one. 439 | alpha = val 440 | best_move = move 441 | return alpha, best_move 442 | 443 | def alphabeta_searcher(depth, evaluate): 444 | def strategy(player, board): 445 | return alphabeta(player, board, MIN_VALUE, MAX_VALUE, depth, evaluate)[1] 446 | return strategy 447 | 448 | 449 | # ----------------------------------------------------------------------------- 450 | # 451 | ## Conclusion 452 | 453 | # The strategies we've discussed are very general and are applicable to a broad 454 | # range of strategy games, such as Chess, Checkers, and Go. More advanced 455 | # strategies for Othello exist that apply various gameplay heuristics; some of 456 | # these are discussed in "Paradigms of Artificial Intelligence Programming" by 457 | # Peter Norvig. 458 | # 459 | # See Wikipedia for more details on [minimax][mm] and [alpha-beta][ab] search. 460 | # 461 | # [mm]: http://en.wikipedia.org/wiki/Minimax 462 | # [ab]: http://en.wikipedia.org/wiki/Alpha-beta_pruning 463 | -------------------------------------------------------------------------------- /paip/tests/test_emycin.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from paip.emycin import * 3 | 4 | 5 | def eq(x, y): 6 | return x == y 7 | 8 | 9 | class CFTests(unittest.TestCase): 10 | def test_cf_or(self): 11 | cases = [ 12 | (0.6, 0.4, 0.76), 13 | (-0.3, -0.75, -0.825), 14 | (0.3, -0.4, -1.0/7.0), 15 | (-0.4, 0.3, -1.0/7.0), 16 | ] 17 | for a, b, c in cases: 18 | self.assertAlmostEqual(c, cf_or(a, b)) 19 | 20 | def test_cf_and(self): 21 | cases = [ 22 | (0.6, 0.4, 0.4), 23 | (-0.3, -0.75, -0.75), 24 | (0.3, -0.4, -0.4), 25 | (-0.4, 0.3, -0.4), 26 | ] 27 | for a, b, c in cases: 28 | self.assertAlmostEqual(c, cf_and(a, b)) 29 | 30 | def test_is_cf(self): 31 | cases = [ 32 | (-0.7, True), 33 | (0.0, True), 34 | (0.999, True), 35 | (1.001, False), 36 | (-3, False), 37 | ] 38 | for a, b in cases: 39 | self.assertEqual(b, is_cf(a)) 40 | 41 | def test_cf_true(self): 42 | cases = [ 43 | (-3, False), 44 | (-0.85, False), 45 | (-0.15, False), 46 | (0.0, False), 47 | (0.15, False), 48 | (0.999, True), 49 | (1.04, False), 50 | ] 51 | for a, b in cases: 52 | self.assertEqual(b, cf_true(a)) 53 | 54 | def test_cf_false(self): 55 | cases = [ 56 | (-3, False), 57 | (-0.85, True), 58 | (-0.15, False), 59 | (0.0, False), 60 | (0.15, False), 61 | (0.999, False), 62 | (1.04, False), 63 | ] 64 | for a, b in cases: 65 | self.assertEqual(b, cf_false(a)) 66 | 67 | 68 | class ContextTests(unittest.TestCase): 69 | def test_instantiate(self): 70 | ctx = Context('patient') 71 | self.assertEqual(0, ctx.count) 72 | inst = ctx.instantiate() 73 | self.assertEqual(1, ctx.count) 74 | self.assertEqual(('patient', 0), inst) 75 | 76 | 77 | class ParameterTests(unittest.TestCase): 78 | def test_validate(self): 79 | age = Parameter('age', cls=lambda x: int(x)) 80 | self.assertEqual(25, age.from_string('25')) 81 | self.assertRaises(ValueError, age.from_string, 'foo') 82 | 83 | 84 | class ConditionTests(unittest.TestCase): 85 | def test_eval_condition(self): 86 | condition = ('age', 'patient', lambda x, y: x < y, 25) 87 | values = dict([(22, 0.3), (27, -0.1), (24, 0.6)]) 88 | self.assertAlmostEqual(0.9, eval_condition(condition, values)) 89 | 90 | 91 | class ValuesTests(unittest.TestCase): 92 | def setUp(self): 93 | self.values = { 94 | ('age', ('patient', 0)): dict([(22, 0.3), (27, -0.1), (24, 0.6)]), 95 | ('health', ('patient', 0)): dict([('good', 0.8), ('moderate', -0.4)]), 96 | ('temp', ('weather', 347)): dict([(79, 0.3), (81, 0.4)]), 97 | ('temp', ('weather', 348)): dict([(79, 0.4), (80, -0.4)]), 98 | ('temp', ('weather', 349)): dict([(82, 0.6), (83, 0.05)]), 99 | ('happy', ('patient', 0)): dict([(True, 0.7)]), 100 | } 101 | 102 | def test_get_vals_empty(self): 103 | self.assertEqual(0, len(get_vals(self.values, 'happy', ('patient', 1)).keys())) 104 | 105 | def test_get_vals(self): 106 | self.assertEqual(3, len(get_vals(self.values, 'age', ('patient', 0)).keys())) 107 | 108 | def test_get_cf_none(self): 109 | self.assertEqual(CF.unknown, get_cf(self.values, 'age', ('patient', 0), 30)) 110 | 111 | def test_get_cf(self): 112 | self.assertAlmostEqual(0.4, get_cf(self.values, 'temp', ('weather', 347), 81)) 113 | 114 | def test_update_cf_none(self): 115 | update_cf(self.values, 'temp', ('weather', 347), 85, 0.3) 116 | self.assertAlmostEqual(0.3, get_cf(self.values, 'temp', ('weather', 347), 85)) 117 | 118 | def test_update_cf(self): 119 | update_cf(self.values, 'temp', ('weather', 347), 81, 0.3) 120 | exp = cf_or(0.3, 0.4) 121 | self.assertAlmostEqual(exp, get_cf(self.values, 'temp', ('weather', 347), 81)) 122 | 123 | 124 | class RuleTests(unittest.TestCase): 125 | def setUp(self): 126 | patient = ('patient', 0) 127 | weather1 = ('weather', 347) 128 | weather2 = ('weather', 348) 129 | weather3 = ('weather', 349) 130 | self.values = { 131 | ('age', patient): dict([(22, 0.3), (27, -0.1), (24, 0.6)]), 132 | ('health', patient): dict([('good', 0.8), ('moderate', -0.4)]), 133 | ('temp', weather1): dict([(79, 0.3), (81, 0.4)]), 134 | ('temp', weather2): dict([(79, 0.4), (80, -0.4)]), 135 | ('temp', weather3): dict([(82, 0.6), (83, 0.05)]), 136 | ('happy', patient): dict([(True, 0.7)]), 137 | } 138 | self.instances = { 139 | 'patient': patient, 140 | 'weather': weather1 141 | } 142 | 143 | def test_applicable_true(self): 144 | premises = [ 145 | ('age', 'patient', lambda x, y: x < y, 25), 146 | ('health', 'patient', eq, 'good'), 147 | ('temp', 'weather', lambda x, y: x > y, 80) 148 | ] 149 | r = Rule(123, premises, [], 0) 150 | expected = cf_and(0.9, cf_and(0.4, 0.8)) 151 | self.assertAlmostEqual(expected, r.applicable(self.values, self.instances)) 152 | 153 | def test_applicable_false(self): 154 | premises = [ 155 | ('age', 'patient', lambda x, y: x > y, 20), 156 | ('health', 'patient', eq, 'poor'), 157 | ('temp', 'weather', lambda x, y: x > y, 80) 158 | ] 159 | r = Rule(123, premises, [], 0) 160 | self.assertAlmostEqual(CF.false, r.applicable(self.values, self.instances)) 161 | 162 | def test_apply_not_applicable(self): 163 | premises = [ 164 | ('age', 'patient', lambda x, y: x > y, 20), 165 | ('health', 'patient', eq, 'poor'), 166 | ('temp', 'weather', lambda x, y: x > y, 80) 167 | ] 168 | conclusions = [ 169 | ('dehydrated', 'patient', eq, False), 170 | ('happy', 'patient', eq, True), 171 | ] 172 | 173 | r = Rule(123, premises, conclusions, 0.9) 174 | r.apply(self.values, self.instances) 175 | 176 | cf = r.cf * r.applicable(self.values, self.instances) 177 | exp1, act1 = 0.7, get_cf(self.values, 'happy', ('patient', 0), True) 178 | exp2, act2 = CF.unknown, get_cf(self.values, 'dehydrated', ('patient', 0), False) 179 | self.assertAlmostEqual(exp1, act1) 180 | self.assertAlmostEqual(exp2, act2) 181 | 182 | def test_apply(self): 183 | premises = [ 184 | ('age', 'patient', lambda x, y: x < y, 25), 185 | ('health', 'patient', eq, 'good'), 186 | ('temp', 'weather', lambda x, y: x > y, 80) 187 | ] 188 | conclusions = [ 189 | ('dehydrated', 'patient', eq, False), 190 | ('happy', 'patient', eq, True), 191 | ] 192 | 193 | r = Rule(123, premises, conclusions, 0.9) 194 | r.apply(self.values, self.instances) 195 | 196 | cf = r.cf * r.applicable(self.values, self.instances) 197 | exp1, act1 = cf_or(cf, 0.7), get_cf(self.values, 'happy', ('patient', 0), True) 198 | exp2, act2 = cf, get_cf(self.values, 'dehydrated', ('patient', 0), False) 199 | self.assertAlmostEqual(exp1, act1) 200 | self.assertAlmostEqual(exp2, act2) 201 | 202 | 203 | class UseRulesTests(unittest.TestCase): 204 | def setUp(self): 205 | patient = ('patient', 0) 206 | weather1 = ('weather', 347) 207 | weather2 = ('weather', 348) 208 | weather3 = ('weather', 349) 209 | self.values = { 210 | ('age', patient): dict([(22, 0.3), (27, -0.1), (24, 0.6)]), 211 | ('health', patient): dict([('good', 0.8), ('moderate', -0.4)]), 212 | ('temp', weather1): dict([(79, 0.3), (81, 0.4)]), 213 | ('temp', weather2): dict([(79, 0.4), (80, -0.4)]), 214 | ('temp', weather3): dict([(82, 0.6), (83, 0.05)]), 215 | ('happy', patient): dict([(True, 0.7)]), 216 | } 217 | self.instances = { 218 | 'patient': patient, 219 | 'weather': weather3 220 | } 221 | 222 | def test_use_rules_fail(self): 223 | # should not be applied 224 | premises1 = [ 225 | ('age', 'patient', lambda x, y: x > y, 20), 226 | ('health', 'patient', eq, 'poor'), 227 | ('temp', 'weather', lambda x, y: x > y, 80) 228 | ] 229 | conclusions1 = [ 230 | ('happy', 'patient', eq, True), 231 | ] 232 | rule1 = Rule(123, premises1, conclusions1, 0.9) 233 | 234 | # should not be applied 235 | premises2 = [ 236 | ('temp', 'weather', eq, 81) 237 | ] 238 | conclusions2 = [ 239 | ('foo', 'bar', lambda x, y: True, 'baz') 240 | ] 241 | rule2 = Rule(456, premises2, conclusions2, 0.7) 242 | 243 | self.assertFalse(use_rules(self.values, self.instances, [rule1, rule2])) 244 | 245 | exp1 = 0.7 246 | self.assertAlmostEqual(exp1, 247 | get_cf(self.values, 'happy', ('patient', 0), True)) 248 | 249 | exp2 = CF.unknown 250 | self.assertAlmostEqual(exp2, get_cf(self.values, 'foo', ('bar', 0), 'baz')) 251 | 252 | def test_use_rules(self): 253 | # should be applied 254 | premises1 = [ 255 | ('age', 'patient', lambda x, y: x < y, 25), 256 | ('health', 'patient', eq, 'good'), 257 | ('temp', 'weather', lambda x, y: x > y, 80) 258 | ] 259 | conclusions1 = [ 260 | ('dehydrated', 'patient', eq, False), 261 | ] 262 | rule1 = Rule(123, premises1, conclusions1, 0.9) 263 | 264 | # should NOT be applied 265 | premises2 = [ 266 | ('age', 'patient', lambda x, y: x > y, 20), 267 | ('health', 'patient', eq, 'poor'), 268 | ('temp', 'weather', lambda x, y: x > y, 80) 269 | ] 270 | conclusions2 = [ 271 | ('dehydrated', 'patient', eq, True), 272 | ] 273 | rule2 = Rule(456, premises2, conclusions2, 0.7) 274 | 275 | # should be applied 276 | premises3 = [ 277 | ('age', 'patient', lambda x, y: x < y, 25), 278 | ('health', 'patient', eq, 'good'), 279 | ('temp', 'weather', lambda x, y: x > y, 80) 280 | ] 281 | conclusions3 = [ 282 | ('health', 'patient', eq, 'poor') 283 | ] 284 | rule3 = Rule(789, premises3, conclusions3, 0.85) 285 | 286 | self.assertTrue(use_rules(self.values, self.instances, [rule1, rule2, rule3])) 287 | 288 | exp1 = rule1.cf * rule1.applicable(self.values, self.instances) 289 | self.assertAlmostEqual(exp1, 290 | get_cf(self.values, 'dehydrated', ('patient', 0), False)) 291 | 292 | exp2 = CF.unknown 293 | self.assertAlmostEqual(exp2, 294 | get_cf(self.values, 'dehydrated', ('patient', 0), True)) 295 | 296 | exp3 = rule3.cf * rule3.applicable(self.values, self.instances) 297 | self.assertAlmostEqual(exp3, 298 | get_cf(self.values, 'health', ('patient', 0), 'poor')) 299 | 300 | 301 | class ParseReplyTests(unittest.TestCase): 302 | def setUp(self): 303 | self.param = Parameter('age', cls=int) 304 | 305 | def test_parse_value(self): 306 | vals = parse_reply(self.param, '25') 307 | self.assertEqual(1, len(vals)) 308 | val, cf = vals[0] 309 | self.assertEqual(25, val) 310 | self.assertAlmostEqual(CF.true, cf) 311 | 312 | def test_parse_list(self): 313 | vals = parse_reply(self.param, '25 0.4, 23 -0.2, 24 0.1') 314 | self.assertEqual(3, len(vals)) 315 | val1, cf1 = vals[0] 316 | val2, cf2 = vals[1] 317 | val3, cf3 = vals[2] 318 | self.assertEqual(25, val1) 319 | self.assertEqual(23, val2) 320 | self.assertEqual(24, val3) 321 | self.assertAlmostEqual(0.4, cf1) 322 | self.assertAlmostEqual(-0.2, cf2) 323 | self.assertAlmostEqual(0.1, cf3) 324 | 325 | class ShellTests(unittest.TestCase): 326 | def setUp(self): 327 | sh = Shell() 328 | self.shell = sh 329 | 330 | # define contexts and parameters 331 | 332 | sh.define_context(Context('patient')) 333 | sh.define_param(Parameter('age', 'patient', cls=int)) 334 | sh.define_param(Parameter('health', 'patient', enum=['good', 'ok', 'poor'])) 335 | sh.define_param(Parameter('dehydrated', 'patient', cls=str)) 336 | def boolean(x): 337 | if x == 'True': 338 | return True 339 | if x == 'False': 340 | return False 341 | raise ValueError('%s is not True or False' % x) 342 | sh.define_param(Parameter('awesome', 'patient', cls=boolean)) 343 | 344 | sh.define_context(Context('weather')) 345 | sh.define_param(Parameter('temp', 'weather', cls=int)) 346 | 347 | # define rules 348 | 349 | premises1 = [ 350 | ('age', 'patient', lambda x, y: x < y, 25), 351 | ('health', 'patient', eq, 'good'), 352 | ('temp', 'weather', lambda x, y: x > y, 80) 353 | ] 354 | conclusions1 = [('dehydrated', 'patient', eq, False)] 355 | sh.define_rule(Rule(123, premises1, conclusions1, 0.9)) 356 | 357 | premises2 = [ 358 | ('age', 'patient', lambda x, y: x > y, 20), 359 | ('health', 'patient', eq, 'poor'), 360 | ('temp', 'weather', lambda x, y: x > y, 80) 361 | ] 362 | conclusions2 = [('dehydrated', 'patient', eq, True)] 363 | sh.define_rule(Rule(456, premises2, conclusions2, 0.7)) 364 | 365 | premises3 = [ 366 | ('age', 'patient', lambda x, y: x > y, 40), 367 | ('temp', 'weather', lambda x, y: x > y, 85) 368 | ] 369 | conclusions3 = [('health', 'patient', eq, 'poor')] 370 | sh.define_rule(Rule(789, premises3, conclusions3, 0.85)) 371 | 372 | # make instances 373 | self.weather = self.shell.instantiate('weather') 374 | self.patient = self.shell.instantiate('patient') 375 | 376 | # fill in initial data 377 | update_cf(self.shell.known_values, 'age', self.patient, 45, 0.7) 378 | self.shell.known.add(('age', self.patient)) 379 | update_cf(self.shell.known_values, 'temp', self.weather, 89, 0.6) 380 | self.shell.known.add(('temp', self.weather)) 381 | 382 | def test_find_out_ask_first_then_use_rules_current_instance(self): 383 | self.shell.get_param('health').ask_first = True 384 | self.shell.read = lambda prompt: 'unknown' 385 | self.assertTrue(('health', self.patient) not in self.shell.known_values) 386 | self.assertTrue(('health', self.patient) not in self.shell.asked) 387 | self.shell.find_out('health') 388 | self.assertTrue(('health', self.patient) in self.shell.asked) 389 | self.assertTrue(('health', self.patient) in self.shell.known_values) 390 | 391 | def test_find_out_ask_first_success_current_instance(self): 392 | self.shell.get_param('health').ask_first = True 393 | self.shell.read = lambda prompt: 'good' 394 | self.assertTrue(('health', self.patient) not in self.shell.known_values) 395 | self.assertTrue(('health', self.patient) not in self.shell.asked) 396 | self.shell.find_out('health') 397 | self.assertTrue(('health', self.patient) in self.shell.asked) 398 | self.assertTrue(('health', self.patient) in self.shell.known_values) 399 | 400 | cf = get_cf(self.shell.known_values, 'health', self.patient, 'good') 401 | self.assertAlmostEqual(CF.true, cf) 402 | 403 | def test_find_out_rules_first_then_ask_current_instance(self): 404 | self.shell.get_param('awesome').ask_first = False 405 | self.shell.read = lambda prompt: 'True 0.7, False -0.4' 406 | self.assertTrue(('awesome', self.patient) not in self.shell.known_values) 407 | self.assertTrue(('awesome', self.patient) not in self.shell.asked) 408 | self.shell.find_out('awesome') 409 | self.assertTrue(('awesome', self.patient) in self.shell.asked) 410 | self.assertTrue(('awesome', self.patient) in self.shell.known_values) 411 | 412 | cf1 = get_cf(self.shell.known_values, 'awesome', self.patient, True) 413 | self.assertAlmostEqual(0.7, cf1) 414 | cf2 = get_cf(self.shell.known_values, 'awesome', self.patient, False) 415 | self.assertAlmostEqual(-0.4, cf2) 416 | 417 | def test_find_out_rules_first_current_instance(self): 418 | self.shell.get_param('health').ask_first = False 419 | self.assertTrue(('health', self.patient) not in self.shell.known_values) 420 | self.assertTrue(('health', self.patient) not in self.shell.asked) 421 | self.shell.find_out('health') 422 | self.assertTrue(('health', self.patient) not in self.shell.asked) 423 | self.assertTrue(('health', self.patient) in self.shell.known_values) 424 | 425 | def test_find_out_ask_first_then_use_rules(self): 426 | self.shell.get_param('health').ask_first = True 427 | self.shell.read = lambda prompt: 'unknown' 428 | self.assertTrue(('health', self.patient) not in self.shell.known_values) 429 | self.assertTrue(('health', self.patient) not in self.shell.asked) 430 | self.shell.find_out('health', self.patient) 431 | self.assertTrue(('health', self.patient) in self.shell.asked) 432 | self.assertTrue(('health', self.patient) in self.shell.known_values) 433 | 434 | def test_find_out_ask_first_success(self): 435 | self.shell.get_param('health').ask_first = True 436 | self.shell.read = lambda prompt: 'good' 437 | self.assertTrue(('health', self.patient) not in self.shell.known_values) 438 | self.assertTrue(('health', self.patient) not in self.shell.asked) 439 | self.shell.find_out('health', self.patient) 440 | self.assertTrue(('health', self.patient) in self.shell.asked) 441 | self.assertTrue(('health', self.patient) in self.shell.known_values) 442 | 443 | cf = get_cf(self.shell.known_values, 'health', self.patient, 'good') 444 | self.assertAlmostEqual(CF.true, cf) 445 | 446 | def test_find_out_rules_first_then_ask(self): 447 | self.shell.get_param('awesome').ask_first = False 448 | self.shell.read = lambda prompt: 'True 0.7, False -0.4' 449 | self.assertTrue(('awesome', self.patient) not in self.shell.known_values) 450 | self.assertTrue(('awesome', self.patient) not in self.shell.asked) 451 | self.shell.find_out('awesome', self.patient) 452 | self.assertTrue(('awesome', self.patient) in self.shell.asked) 453 | self.assertTrue(('awesome', self.patient) in self.shell.known_values) 454 | 455 | cf1 = get_cf(self.shell.known_values, 'awesome', self.patient, True) 456 | self.assertAlmostEqual(0.7, cf1) 457 | cf2 = get_cf(self.shell.known_values, 'awesome', self.patient, False) 458 | self.assertAlmostEqual(-0.4, cf2) 459 | 460 | def test_find_out_rules_first(self): 461 | self.shell.get_param('health').ask_first = False 462 | self.assertTrue(('health', self.patient) not in self.shell.known_values) 463 | self.assertTrue(('health', self.patient) not in self.shell.asked) 464 | self.shell.find_out('health', self.patient) 465 | self.assertTrue(('health', self.patient) not in self.shell.asked) 466 | self.assertTrue(('health', self.patient) in self.shell.known_values) 467 | --------------------------------------------------------------------------------