diff --git a/.idea/dictionaries/brandon_corfman.xml b/.idea/dictionaries/brandon_corfman.xml
new file mode 100644
index 000000000..d6248d57f
--- /dev/null
+++ b/.idea/dictionaries/brandon_corfman.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 000000000..7cafb7a5a
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+ Spelling
+
+
+
+
+ SpellCheckingInspection
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/logic.py b/logic.py
index dfa70d0db..f1150b59f 100644
--- a/logic.py
+++ b/logic.py
@@ -820,7 +820,7 @@ def __init__(self,dimrow):
wumpus_at_least = list()
for x in range(1, dimrow+1):
for y in range(1, dimrow + 1):
- wumps_at_least.append(wumpus(x, y))
+ wumpus_at_least.append(wumpus(x, y))
self.tell(new_disjunction(wumpus_at_least))
diff --git a/pddl_files/aircargo-domain.pddl b/pddl_files/aircargo-domain.pddl
new file mode 100755
index 000000000..af1126ac4
--- /dev/null
+++ b/pddl_files/aircargo-domain.pddl
@@ -0,0 +1,31 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;; air cargo domain from AIMA book 2nd ed.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+; since the 'at' predicate is used for both cargo and planes, I didn't specify types
+; in this domain to keep things simpler.
+
+(define (domain aircargo)
+ (:requirements :strips)
+ (:predicates (at ?x ?a)
+ (cargo ?c)
+ (airport ?a)
+ (plane ?p)
+ (in ?x ?p)
+ )
+
+ (:action load
+ :parameters (?c ?p ?a)
+ :precondition (and (cargo ?c) (plane ?p) (airport ?a) (at ?c ?a) (at ?p ?a))
+ :effect (and (in ?c ?p) (not (at ?c ?a))))
+
+ (:action unload
+ :parameters (?c ?p ?a)
+ :precondition (and (cargo ?c) (plane ?p) (airport ?a) (in ?c ?p) (at ?p ?a))
+ :effect (and (at ?c ?a) (not (in ?c ?p))))
+
+ (:action fly
+ :parameters (?p ?f ?t)
+ :precondition (and (at ?p ?f) (plane ?p) (airport ?f) (airport ?t))
+ :effect (and (at ?p ?t) (not (at ?p ?f))))
+)
\ No newline at end of file
diff --git a/pddl_files/aircargo-problem.pddl b/pddl_files/aircargo-problem.pddl
new file mode 100755
index 000000000..b25c1fd29
--- /dev/null
+++ b/pddl_files/aircargo-problem.pddl
@@ -0,0 +1,17 @@
+(define (problem Transport)
+
+(:domain aircargo)
+
+(:init (at C1 SFO)
+ (at C2 JFK)
+ (at P1 SFO)
+ (at P2 JFK)
+ (cargo C1)
+ (cargo C2)
+ (plane P1)
+ (plane P2)
+ (airport JFK)
+ (airport SFO))
+
+(:goal (and (at C1 JFK) (at C2 SFO)))
+)
diff --git a/pddl_files/blocks-domain.pddl b/pddl_files/blocks-domain.pddl
new file mode 100755
index 000000000..8dbcdaf9b
--- /dev/null
+++ b/pddl_files/blocks-domain.pddl
@@ -0,0 +1,24 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;; Building block towers
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(define (domain BlocksWorld)
+ (:requirements :strips)
+ (:predicates (on ?x ?y)
+ (clear ?x)
+ (block ?x)
+ )
+
+ (:action Move
+ :parameters (?b ?x ?y)
+ :precondition (and (on ?b ?x) (clear ?b) (clear ?y) (block ?b))
+ :effect (and (on ?b ?y) (clear ?x) (not (on ?b ?x)) (not (clear ?y)))
+ )
+
+ (:action Move_To_Table
+ :parameters (?b ?x)
+ :precondition (and (on ?b ?x) (clear ?b) (block ?b))
+ :effect (and (on ?b Table) (clear ?x) (not (on ?b ?x)))
+ )
+)
+
diff --git a/pddl_files/blocks-problem.pddl b/pddl_files/blocks-problem.pddl
new file mode 100755
index 000000000..c0227056b
--- /dev/null
+++ b/pddl_files/blocks-problem.pddl
@@ -0,0 +1,18 @@
+(define (problem ThreeBlockTower)
+
+ (:domain BlocksWorld)
+
+ (:init
+ (on A Table)
+ (on B Table)
+ (on C Table)
+ (block A)
+ (block B)
+ (block C)
+ (clear A)
+ (clear B)
+ (clear C)
+ )
+
+ (:goal (and (on A B) (on B C)))
+)
diff --git a/pddl_files/cake-domain.pddl b/pddl_files/cake-domain.pddl
new file mode 100644
index 000000000..5bae439ed
--- /dev/null
+++ b/pddl_files/cake-domain.pddl
@@ -0,0 +1,20 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;; Cake domain
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(define (domain Cake)
+ (:requirements :strips)
+
+ (:action Eat
+ :parameters (Cake)
+ :precondition (have Cake)
+ :effect (and (eaten Cake) (not (have Cake)))
+ )
+
+ (:action Bake
+ :parameters (Cake)
+ :precondition (not (have Cake))
+ :effect (have Cake)
+ )
+)
+
diff --git a/pddl_files/cake-problem.pddl b/pddl_files/cake-problem.pddl
new file mode 100644
index 000000000..b0229604a
--- /dev/null
+++ b/pddl_files/cake-problem.pddl
@@ -0,0 +1,9 @@
+; The "have cake and eat it too" problem.
+
+(define (problem HaveCakeAndEatItToo)
+ (:domain Cake)
+
+ (:init (have Cake) )
+
+ (:goal (and (have Cake) (eaten Cake)))
+)
diff --git a/pddl_files/shoes-domain.pddl b/pddl_files/shoes-domain.pddl
new file mode 100755
index 000000000..532e4e8db
--- /dev/null
+++ b/pddl_files/shoes-domain.pddl
@@ -0,0 +1,37 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;; Putting on a pair of shoes
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(define (domain Shoes)
+ (:requirements :strips)
+ (:predicates (leftfoot ?x)
+ (rightfoot ?x)
+ (on ?x ?y)
+ )
+
+ (:action RightShoe
+ :parameters ()
+ :precondition (and (on RightSock ?x) (rightfoot ?x) (not (on RightShoe ?x)))
+ :effect (and (on RightShoe ?x))
+ )
+
+ (:action RightSock
+ :parameters ()
+ :precondition (and (clear ?x) (rightfoot ?x))
+ :effect (and (on RightSock ?x) (not (clear ?x)))
+ )
+
+ (:action LeftShoe
+ :parameters ()
+ :precondition (and (on LeftSock ?x) (leftfoot ?x) (not (on LeftShoe ?x)))
+ :effect (and (on LeftShoe ?x))
+ )
+
+ (:action LeftSock
+ :parameters ()
+ :precondition (and (clear ?x) (leftfoot ?x))
+ :effect (and (on LeftSock ?x) (not (clear ?x)))
+ )
+
+)
+
diff --git a/pddl_files/shoes-problem.pddl b/pddl_files/shoes-problem.pddl
new file mode 100755
index 000000000..26b0893b1
--- /dev/null
+++ b/pddl_files/shoes-problem.pddl
@@ -0,0 +1,12 @@
+(define (problem PutOnShoes)
+
+ (:domain Shoes)
+
+ (:init (clear LF)
+ (clear RF)
+ (leftfoot LF)
+ (rightfoot RF)
+ )
+
+ (:goal (and (On RightShoe RF) (on LeftShoe LF)))
+)
diff --git a/pddl_files/shopping-domain.pddl b/pddl_files/shopping-domain.pddl
new file mode 100644
index 000000000..f27dc57ac
--- /dev/null
+++ b/pddl_files/shopping-domain.pddl
@@ -0,0 +1,20 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;; Shopping domain
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(define (domain Shopping)
+ (:requirements :strips)
+
+ (:action Buy
+ :parameters (?x ?store)
+ :precondition (and (at ?store) (sells ?store ?x))
+ :effect (have ?x)
+ )
+
+ (:action Go
+ :parameters (?x ?y)
+ :precondition (and (at ?x) (loc ?x) (loc ?y))
+ :effect (and (at ?y) (not (at ?x)))
+ )
+)
+
diff --git a/pddl_files/shopping-problem.pddl b/pddl_files/shopping-problem.pddl
new file mode 100644
index 000000000..f10af0a6a
--- /dev/null
+++ b/pddl_files/shopping-problem.pddl
@@ -0,0 +1,17 @@
+;; Going shopping
+
+(define (problem GoingShopping)
+
+ (:domain Shopping)
+
+ (:init (At Home)
+ (Loc Home)
+ (Loc Supermarket)
+ (Loc HardwareStore)
+ (Sells Supermarket Milk)
+ (Sells Supermarket Banana)
+ (Sells HardwareStore Drill)
+ )
+
+ (:goal (and (Have Milk) (Have Banana) (Have Drill) (At Home)))
+)
diff --git a/pddl_files/spare-tire-domain.pddl b/pddl_files/spare-tire-domain.pddl
new file mode 100755
index 000000000..02d6e46c3
--- /dev/null
+++ b/pddl_files/spare-tire-domain.pddl
@@ -0,0 +1,37 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;; Changing a spare tire on a car
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(define (domain SpareTire)
+ (:requirements :strips)
+ (:predicates (At Spare Trunk)
+ (At Spare Ground)
+ (At Flat Axle)
+ (At Flat Ground)
+ (At Spare Axle))
+
+ (:action remove
+ :parameters (Spare Trunk)
+ :precondition (At Spare Trunk)
+ :effect (and (At Spare Ground) (not (At Spare Trunk))))
+
+ (:action remove
+ :parameters (Flat Axle)
+ :precondition (At Flat Axle)
+ :effect (and (At Flat Ground) (not (At Flat Axle))))
+
+ (:action put_on
+ :parameters (Spare Axle)
+ :precondition (and (At Spare Ground) (not (At Flat Axle)))
+ :effect (and (At Spare Axle) (not (At Spare Ground))))
+
+ (:action leave_overnight
+ :effect
+ (and (not (At Spare Ground))
+ (not (At Spare Axle))
+ (not (At Spare Trunk))
+ (not (At Flat Ground))
+ (not (At Flat Axle))
+ )
+ )
+)
diff --git a/pddl_files/spare-tire-problem.pddl b/pddl_files/spare-tire-problem.pddl
new file mode 100755
index 000000000..1dd8cc23c
--- /dev/null
+++ b/pddl_files/spare-tire-problem.pddl
@@ -0,0 +1,9 @@
+(define (problem ChangeFlatTire)
+
+(:domain SpareTire)
+
+(:init (At Flat Axle)
+ (At Spare Trunk))
+
+(:goal (At Spare Axle))
+)
diff --git a/pddl_files/sussman-anomaly-problem.pddl b/pddl_files/sussman-anomaly-problem.pddl
new file mode 100755
index 000000000..eaa828120
--- /dev/null
+++ b/pddl_files/sussman-anomaly-problem.pddl
@@ -0,0 +1,15 @@
+(define (problem SussmanAnomaly)
+
+(:domain BlocksWorld)
+
+(:init (clear C)
+ (clear B)
+ (on A Table)
+ (on B Table)
+ (on C A)
+ (block A)
+ (block B)
+ (block C))
+
+(:goal (and (on A B) (on B C)))
+)
diff --git a/pddl_files/tpp-domain.pddl b/pddl_files/tpp-domain.pddl
new file mode 100644
index 000000000..bb0d38580
--- /dev/null
+++ b/pddl_files/tpp-domain.pddl
@@ -0,0 +1,65 @@
+; IPC5 Domain: TPP Propositional
+; Authors: Alfonso Gerevini and Alessandro Saetti
+
+(define (domain TPP-Propositional)
+(:requirements :strips :typing)
+(:types place locatable level - object
+ depot market - place
+ truck goods - locatable)
+
+(:predicates (loaded ?g - goods ?t - truck ?l - level)
+ (ready-to-load ?g - goods ?m - market ?l - level)
+ (stored ?g - goods ?l - level)
+ (on-sale ?g - goods ?m - market ?l - level)
+ (next ?l1 ?l2 - level)
+ (at ?t - truck ?p - place)
+ (connected ?p1 ?p2 - place))
+
+(:action drive
+ :parameters (?t - truck ?frm ?to - place)
+ :precondition (and (at ?t ?frm) (connected ?frm ?to))
+ :effect (and (not (at ?t ?frm)) (at ?t ?to)))
+
+
+; ### LOAD ###
+; ?l1 is the level of ?g ready to be loaded at ?m before loading
+; ?l2 is the level of ?g ready to be loaded at ?m after loading
+; ?l3 is the level of ?g in ?t before loading
+; ?l4 is the level of ?g in ?t after loading
+
+(:action load
+ :parameters (?g - goods ?t - truck ?m - market ?l1 ?l2 ?l3 ?l4 - level)
+ :precondition (and (at ?t ?m) (loaded ?g ?t ?l3)
+ (ready-to-load ?g ?m ?l2) (next ?l2 ?l1) (next ?l4 ?l3))
+ :effect (and (loaded ?g ?t ?l4) (not (loaded ?g ?t ?l3))
+ (ready-to-load ?g ?m ?l1) (not (ready-to-load ?g ?m ?l2))))
+
+
+; ### UNLOAD ###
+; ?l1 is the level of ?g in ?t before unloading
+; ?l2 is the level of ?g in ?t after unloading
+; ?l3 is the level of ?g in ?d before unloading
+; ?l4 is the level of ?g in ?d after unloading
+
+(:action unload
+ :parameters (?g - goods ?t - truck ?d - depot ?l1 ?l2 ?l3 ?l4 - level)
+ :precondition (and (at ?t ?d) (loaded ?g ?t ?l2)
+ (stored ?g ?l3) (next ?l2 ?l1) (next ?l4 ?l3))
+ :effect (and (loaded ?g ?t ?l1) (not (loaded ?g ?t ?l2))
+ (stored ?g ?l4) (not (stored ?g ?l3))))
+
+
+; ### BUY ###
+; ?l1 is the level of ?g on sale at ?m before buying
+; ?l2 is the level of ?g on sale at ?m after buying
+; ?l3 is the level of ?g ready to be loaded at ?m before buying
+; ?l4 is the level of ?g ready to be loaded at ?m after buying
+
+(:action buy
+ :parameters (?t - truck ?g - goods ?m - market ?l1 ?l2 ?l3 ?l4 - level)
+ :precondition (and (at ?t ?m) (on-sale ?g ?m ?l2) (ready-to-load ?g ?m ?l3)
+ (next ?l2 ?l1) (next ?l4 ?l3))
+ :effect (and (on-sale ?g ?m ?l1) (not (on-sale ?g ?m ?l2))
+ (ready-to-load ?g ?m ?l4) (not (ready-to-load ?g ?m ?l3))))
+
+)
\ No newline at end of file
diff --git a/pddl_files/tpp-problem.pddl b/pddl_files/tpp-problem.pddl
new file mode 100644
index 000000000..b98d9188d
--- /dev/null
+++ b/pddl_files/tpp-problem.pddl
@@ -0,0 +1,30 @@
+;; TPP Task 02
+
+(define (problem TPP)
+(:domain TPP-Propositional)
+(:objects
+ goods1 goods2 - goods
+ truck1 - truck
+ market1 - market
+ depot1 - depot
+ level0 level1 - level)
+
+(:init
+ (next level1 level0)
+ (ready-to-load goods1 market1 level0)
+ (ready-to-load goods2 market1 level0)
+ (stored goods1 level0)
+ (stored goods2 level0)
+ (loaded goods1 truck1 level0)
+ (loaded goods2 truck1 level0)
+ (connected depot1 market1)
+ (connected market1 depot1)
+ (on-sale goods1 market1 level1)
+ (on-sale goods2 market1 level1)
+ (at truck1 depot1))
+
+(:goal (and
+ (stored goods1 level1)
+ (stored goods2 level1)))
+
+)
diff --git a/pddl_parse.py b/pddl_parse.py
new file mode 100644
index 000000000..c52f3d80e
--- /dev/null
+++ b/pddl_parse.py
@@ -0,0 +1,387 @@
+import os
+from collections import deque
+
+
+class ParseError(Exception):
+ pass
+
+
+def is_string(token):
+ return isinstance(token, str)
+
+
+def is_deque(token):
+ return isinstance(token, deque)
+
+
+def read_pddl_file(filepath):
+ with open(filepath) as f:
+ # read in lines from PDDL file and remove newline characters
+ lines = [line.strip() for line in f.readlines()]
+ strip_comments(lines)
+ # join all lines into single string
+ pddl_text = ''.join(lines)
+ filename = os.path.basename(filepath)
+
+ # transform into Python-compatible S-expressions (using lists of strings)
+ def transform_sexprs(tokens: deque):
+ """Read an expression from a sequence of tokens."""
+ if len(tokens) == 0:
+ raise ParseError('unexpected EOF while reading {}'.format(filename))
+ token = tokens.popleft()
+ if '(' == token:
+ D = deque()
+ try:
+ while tokens[0] != ')':
+ D.append(transform_sexprs(tokens))
+ tokens.popleft() # pop off ')'
+ return D
+ except IndexError:
+ raise ParseError('unexpected EOF while reading {}'.format(filename))
+ elif ')' == token:
+ raise ParseError('unexpected ) in {}'.format(filename))
+ else:
+ return token
+
+ return transform_sexprs(tokenize(pddl_text))
+
+
+def tokenize(s: str) -> deque:
+ """Convert a string into a list of tokens."""
+ return deque(s.replace('(', ' ( ').replace(')', ' ) ').replace(':', ' :').split())
+
+
+def strip_comments(lines) -> None:
+ """ Given a list of strings, strips any comments. """
+ for i, line in enumerate(lines):
+ idx = line.find(';')
+ if idx != -1:
+ lines[i] = line[:idx]
+
+
+def parse_tokens(match_dict: dict, tokens: deque) -> None:
+ def match_tokens(tokens: deque):
+ if not is_deque(tokens):
+ return False
+ item = tokens.popleft()
+ if is_string(item):
+ item = item.lower()
+ for text in match_dict:
+ if item.startswith(text):
+ if match_dict[text](tokens):
+ break
+ elif is_deque(item):
+ match_tokens(item)
+ else:
+ raise ParseError('Unexpected token: {}'.format(item))
+ return True
+
+ while tokens:
+ if not match_tokens(tokens):
+ break
+
+
+def build_expr_string(expr_name: str, variables: list) -> str:
+ # can't have actions with a dash in the name; it confuses the Expr class
+ if expr_name.startswith('~'):
+ estr = '~' + expr_name[1:].replace('-', '').capitalize() + '('
+ else:
+ estr = expr_name.replace('-', '').capitalize() + '('
+ vlen = len(variables)
+ if vlen:
+ for i in range(vlen - 1):
+ estr += variables[i] + ', '
+ estr += variables[vlen - 1]
+ estr += ')'
+ return estr
+
+
+def _parse_variables(tokens, has_types) -> list:
+ """ Extracts a list of variables from the PDDL. """
+ variables = []
+ while tokens:
+ token = tokens.popleft()
+ if not is_string(token):
+ raise ParseError('Invalid variable name: {}'.format(token))
+ if token.startswith('?'):
+ pred_var = token[1:]
+ else:
+ pred_var = token
+
+ if has_types:
+ # lookahead to see if there's a dash indicating an upcoming type name
+ if tokens[0] == '-':
+ # get rid of the dash character and the type name
+ dash = tokens.popleft()
+ if not is_string(dash):
+ raise ParseError('Expected dash instead of {} after variable name'.format(dash))
+ type_name = tokens.popleft()
+ if not is_string(type_name):
+ raise ParseError('Expected type name instead of {} after variable name'.format(type_name))
+ variables.append(pred_var)
+ return variables
+
+
+def _parse_single_expr_string(tokens: deque) -> str:
+ if not is_deque(tokens):
+ raise ParseError('Expected expression')
+ if tokens[0] == 'not':
+ # expression is not(e), so next, parse the expression e before prepending the ~ operator to it.
+ token = tokens.pop()
+ e = _parse_single_expr_string(token)
+ if '~' in e:
+ raise ParseError('Multiple not operators in expression.')
+ return '~' + e
+ else: # expression is a standard Op(param1, param2, etc ...) format
+ expr_name = tokens.popleft()
+ if not is_string(expr_name):
+ raise ParseError('Invalid expression name: {}'.format(expr_name))
+ expr_name = expr_name.lower()
+ variables = []
+ while tokens:
+ param = tokens.popleft()
+ if not is_string(param):
+ raise ParseError('Invalid parameter {} for expression "{}"'.format(param, expr_name))
+ if param.startswith('?'):
+ variables.append(param[1:].lower())
+ else:
+ if not param[0].isupper():
+ param = param.capitalize()
+ variables.append(param)
+ return build_expr_string(expr_name, variables)
+
+
+def _parse_expr_list(tokens) -> list:
+ if not is_deque(tokens):
+ raise ParseError('Expected expression list')
+ expr_lst = []
+ while tokens:
+ token = tokens.popleft()
+ expr_lst.append(_parse_single_expr_string(token))
+ return expr_lst
+
+
+def _parse_formula(formula: deque) -> list:
+ if not is_deque(formula):
+ raise ParseError('Invalid formula: {}'.format(formula))
+ if len(formula) == 0:
+ raise ParseError('Formula is empty')
+ expr_lst = []
+ token = formula.popleft()
+ if not is_string(token):
+ raise ParseError('Invalid token for start of formula: {}'.format(token))
+ if token.lower() == 'and': # preconds and effects only use 'and' keyword
+ exprs = _parse_expr_list(formula)
+ expr_lst.extend(exprs)
+ else: # parse single expression
+ formula.appendleft(token)
+ expr_lst.append(_parse_single_expr_string(formula))
+ return expr_lst
+
+
+class DomainParser:
+ def __init__(self):
+ self._clear_variables()
+
+ def _clear_variables(self):
+ self.domain_name = ''
+ self._action_name = ''
+ self._requirements = []
+ self.predicates = []
+ self.actions = []
+ self.constants = []
+ self._types = []
+ self._parameters = []
+ self._preconditions = []
+ self._effects = []
+
+ def _parse_define(self, tokens: deque) -> bool:
+ if not is_deque(tokens):
+ raise ParseError('Domain list not found after define statement')
+ domain_seq = tokens.popleft()
+ if is_deque(domain_seq) and len(domain_seq) == 0:
+ raise ParseError('Domain list empty')
+ token = domain_seq.popleft()
+ if token != 'domain':
+ raise ParseError('Domain keyword not found after define statement')
+ if is_deque(domain_seq) and len(domain_seq) == 0:
+ raise ParseError('Domain name not found in domain list')
+ self.domain_name = domain_seq.popleft()
+ return True
+
+ def _parse_requirements(self, tokens: deque) -> bool:
+ if not is_deque(tokens):
+ raise ParseError('Valid list not found after :requirements keyword')
+ self._requirements = list(tokens)
+ if ':strips' not in self._requirements:
+ raise ParseError(':strips is not in list of domain requirements.')
+ return True
+
+ def _parse_constants(self, tokens: deque) -> bool:
+ if not is_deque(tokens):
+ raise ParseError('Valid list not found after :constants keyword')
+ self.constants = _parse_variables(tokens, self._types)
+ return True
+
+ # noinspection PyUnusedLocal
+ def _parse_types(self, tokens: deque) -> bool:
+ if not is_deque(tokens):
+ raise ParseError('Expected list of types')
+ self._types = True
+ return True
+
+ def _parse_predicates(self, tokens: deque) -> bool:
+ while tokens:
+ if not is_deque(tokens):
+ raise ParseError('Valid list not found after :predicates keyword')
+ predicate = tokens.popleft()
+ if not is_deque(predicate):
+ raise ParseError('Invalid predicate: {}'.format(predicate))
+ pred_name = predicate.popleft()
+ if not is_string(pred_name):
+ raise ParseError('Invalid predicate name: {}'.format(pred_name))
+ if not is_deque(predicate):
+ raise ParseError('Invalid predicate variable list: {}'.format(predicate))
+ try:
+ new_predicate = [pred_name] + _parse_variables(predicate, self._types)
+ except IndexError:
+ raise ParseError('Error parsing variables for predicate {}'.format(pred_name))
+ self.predicates.append(new_predicate)
+ return True
+
+ def _parse_action(self, tokens) -> bool:
+ if not is_deque(tokens):
+ raise ParseError('Invalid action: {}'.format(tokens))
+ self._action_name = tokens.popleft()
+ if not is_string(self._action_name):
+ raise ParseError('Invalid action name: {}'.format(self._action_name))
+
+ match = {':parameters': self._parse_parameters,
+ ':precondition': self._parse_preconditions,
+ ':effect': self._parse_effects
+ }
+ parse_tokens(match, tokens)
+ params = [p for p in self._parameters]
+ action = (build_expr_string(self._action_name, params), self._preconditions, self._effects)
+ self.actions.append(action)
+ # reset the temporary storage for this action before processing the next one.
+ self._action_name = ''
+ self._parameters = []
+ self._preconditions = []
+ self._effects = []
+ return True
+
+ def _parse_parameters(self, tokens: deque) -> bool:
+ if is_deque(tokens) and len(tokens) > 0:
+ param_list = tokens.popleft()
+ if not is_deque(param_list):
+ raise ParseError('Expected parameter list for action "{}"'.format(self._action_name))
+ try:
+ self._parameters = _parse_variables(param_list, self._types)
+ except IndexError:
+ raise ParseError('Error parsing parameter list for action "{}"'.format(self._action_name))
+ return True
+
+ def _parse_preconditions(self, tokens: deque) -> bool:
+ if not is_deque(tokens):
+ raise ParseError('Invalid precondition list for action "{}": {}'.format(self._action_name, tokens))
+ if len(tokens) == 0:
+ raise ParseError('Missing precondition list for action "{}".'.format(self._action_name))
+ precond_seq = tokens.popleft()
+ self._preconditions = _parse_formula(precond_seq)
+ return True
+
+ def _parse_effects(self, tokens: deque) -> bool:
+ if not is_deque(tokens):
+ raise ParseError('Invalid effects list for action "{}": {}'.format(self._action_name, tokens))
+ if len(tokens) == 0:
+ raise ParseError('Missing effects list for action "{}".'.format(self._action_name))
+ effects_seq = tokens.popleft()
+ self._effects = _parse_formula(effects_seq)
+ return True
+
+ def read(self, filepath) -> None:
+ self._clear_variables()
+ pddl = read_pddl_file(filepath)
+ filename = os.path.basename(filepath)
+
+ # Use dictionaries for parsing. If the token matches the key, then call the associated value (method)
+ # for parsing.
+ match = {'define': self._parse_define,
+ ':requirements': self._parse_requirements,
+ ':constants': self._parse_constants,
+ ':types': self._parse_types,
+ ':predicates': self._parse_predicates,
+ ':action': self._parse_action
+ }
+
+ parse_tokens(match, pddl)
+
+ # check to see if minimum domain definition is met.
+ if not self.domain_name:
+ raise ParseError('No domain name was found in domain file {}'.format(filename))
+ if not self.actions:
+ raise ParseError('No valid actions found in domain file {}'.format(filename))
+
+
+class ProblemParser:
+ def __init__(self):
+ self.problem_name = ''
+ self.domain_name = ''
+ self.initial_state = []
+ self.goals = []
+
+ def _parse_define(self, tokens: deque) -> bool:
+ if not is_deque(tokens) or len(tokens) == 0:
+ raise ParseError('Expected problem list after define statement')
+ problem_seq = tokens.popleft()
+ if not is_deque(problem_seq):
+ raise ParseError('Invalid problem list after define statement')
+ if len(problem_seq) == 0:
+ raise ParseError('Missing problem list after define statement')
+ token = problem_seq.popleft()
+ if token != 'problem':
+ raise ParseError('Problem keyword not found after define statement')
+ self.problem_name = problem_seq.popleft()
+ return True
+
+ def _parse_domain(self, tokens: deque) -> bool:
+ if not is_deque(tokens) or len(tokens) == 0:
+ raise ParseError('Expected domain name after :domain keyword')
+ self.domain_name = tokens.popleft()
+ return True
+
+ def _parse_init(self, tokens: deque) -> bool:
+ self.initial_state = _parse_expr_list(tokens)
+ return True
+
+ def _parse_goal(self, tokens: deque) -> bool:
+ if not is_deque(tokens):
+ raise ParseError('Invalid goal list after :goal keyword')
+ if len(tokens) == 0:
+ raise ParseError('Missing goal list after :goal keyword')
+ goal_list = tokens.popleft()
+ self.goals = _parse_formula(goal_list)
+ return True
+
+ def read(self, filepath) -> None:
+ pddl = read_pddl_file(filepath)
+ filename = os.path.basename(filepath)
+
+ # Use dictionaries for parsing. If the token matches the key, then call the associated value (method)
+ # for parsing.
+ match = {'define': self._parse_define,
+ ':domain': self._parse_domain,
+ ':init': self._parse_init,
+ ':goal': self._parse_goal
+ }
+
+ parse_tokens(match, pddl)
+
+ # check to see if minimum domain definition is met.
+ if not self.domain_name:
+ raise ParseError('No domain name was found in problem file {}'.format(filename))
+ if not self.initial_state:
+ raise ParseError('No initial state found in problem file {}'.format(filename))
+ if not self.goals:
+ raise ParseError('No goal state found in problem file {}'.format(filename))
diff --git a/planning.py b/planning.py
index b5e35dae4..b632906b0 100644
--- a/planning.py
+++ b/planning.py
@@ -1,12 +1,13 @@
"""Planning (Chapters 10-11)
"""
-
+import os
import copy
import itertools
-from search import Node
-from utils import Expr, expr, first
-from logic import FolKB, conjuncts
+from search import Node, astar_search
from collections import deque
+from logic import fol_bc_and, FolKB, conjuncts
+from utils import expr, Expr, partition, first
+from pddl_parse import DomainParser, ProblemParser, build_expr_string
class PDDL:
@@ -142,115 +143,12 @@ def act(self, kb, args):
else:
new_clause = Expr('Not' + clause.op, *clause.args)
- if kb.ask(self.substitute(new_clause, args)) is not False:
+ if kb.ask(self.substitute(new_clause, args)) is not False:
kb.retract(self.substitute(new_clause, args))
return kb
-def air_cargo():
- """Air cargo problem"""
-
- return PDDL(init='At(C1, SFO) & At(C2, JFK) & At(P1, SFO) & At(P2, JFK) & Cargo(C1) & Cargo(C2) & Plane(P1) & Plane(P2) & Airport(SFO) & Airport(JFK)',
- goals='At(C1, JFK) & At(C2, SFO)',
- actions=[Action('Load(c, p, a)',
- precond='At(c, a) & At(p, a) & Cargo(c) & Plane(p) & Airport(a)',
- effect='In(c, p) & ~At(c, a)'),
- Action('Unload(c, p, a)',
- precond='In(c, p) & At(p, a) & Cargo(c) & Plane(p) & Airport(a)',
- effect='At(c, a) & ~In(c, p)'),
- Action('Fly(p, f, to)',
- precond='At(p, f) & Plane(p) & Airport(f) & Airport(to)',
- effect='At(p, to) & ~At(p, f)')])
-
-
-def spare_tire():
- """Spare tire problem"""
-
- return PDDL(init='Tire(Flat) & Tire(Spare) & At(Flat, Axle) & At(Spare, Trunk)',
- goals='At(Spare, Axle) & At(Flat, Ground)',
- actions=[Action('Remove(obj, loc)',
- precond='At(obj, loc)',
- effect='At(obj, Ground) & ~At(obj, loc)'),
- Action('PutOn(t, Axle)',
- precond='Tire(t) & At(t, Ground) & ~At(Flat, Axle)',
- effect='At(t, Axle) & ~At(t, Ground)'),
- Action('LeaveOvernight',
- precond='',
- effect='~At(Spare, Ground) & ~At(Spare, Axle) & ~At(Spare, Trunk) & \
- ~At(Flat, Ground) & ~At(Flat, Axle) & ~At(Flat, Trunk)')])
-
-
-def three_block_tower():
- """Sussman Anomaly problem"""
-
- return PDDL(init='On(A, Table) & On(B, Table) & On(C, A) & Block(A) & Block(B) & Block(C) & Clear(B) & Clear(C)',
- goals='On(A, B) & On(B, C)',
- actions=[Action('Move(b, x, y)',
- precond='On(b, x) & Clear(b) & Clear(y) & Block(b) & Block(y)',
- effect='On(b, y) & Clear(x) & ~On(b, x) & ~Clear(y)'),
- Action('MoveToTable(b, x)',
- precond='On(b, x) & Clear(b) & Block(b)',
- effect='On(b, Table) & Clear(x) & ~On(b, x)')])
-
-
-def have_cake_and_eat_cake_too():
- """Cake problem"""
-
- return PDDL(init='Have(Cake)',
- goals='Have(Cake) & Eaten(Cake)',
- actions=[Action('Eat(Cake)',
- precond='Have(Cake)',
- effect='Eaten(Cake) & ~Have(Cake)'),
- Action('Bake(Cake)',
- precond='~Have(Cake)',
- effect='Have(Cake)')])
-
-
-def shopping_problem():
- """Shopping problem"""
-
- return PDDL(init='At(Home) & Sells(SM, Milk) & Sells(SM, Banana) & Sells(HW, Drill)',
- goals='Have(Milk) & Have(Banana) & Have(Drill)',
- actions=[Action('Buy(x, store)',
- precond='At(store) & Sells(store, x)',
- effect='Have(x)'),
- Action('Go(x, y)',
- precond='At(x)',
- effect='At(y) & ~At(x)')])
-
-
-def socks_and_shoes():
- """Socks and shoes problem"""
-
- return PDDL(init='',
- goals='RightShoeOn & LeftShoeOn',
- actions=[Action('RightShoe',
- precond='RightSockOn',
- effect='RightShoeOn'),
- Action('RightSock',
- precond='',
- effect='RightSockOn'),
- Action('LeftShoe',
- precond='LeftSockOn',
- effect='LeftShoeOn'),
- Action('LeftSock',
- precond='',
- effect='LeftSockOn')])
-
-
-# Doubles tennis problem
-def double_tennis_problem():
- return PDDL(init='At(A, LeftBaseLine) & At(B, RightNet) & Approaching(Ball, RightBaseLine) & Partner(A, B) & Partner(B, A)',
- goals='Returned(Ball) & At(a, LeftNet) & At(a, RightNet)',
- actions=[Action('Hit(actor, Ball, loc)',
- precond='Approaching(Ball,loc) & At(actor,loc)',
- effect='Returned(Ball)'),
- Action('Go(actor, to, loc)',
- precond='At(actor, loc)',
- effect='At(actor, to) & ~At(actor, loc)')])
-
-
class Level:
"""
Contains the state of the planning problem
@@ -506,13 +404,211 @@ def execute(self):
solution = self.extract_solution(self.graph.pddl.goals, -1)
if solution:
return solution
-
+
if len(self.graph.levels) >= 2 and self.check_leveloff():
return None
-class TotalOrderPlanner:
+def spare_tire():
+ """Spare tire problem"""
+
+ return PDDL(init='Tire(Flat) & Tire(Spare) & At(Flat, Axle) & At(Spare, Trunk)',
+ goals='At(Spare, Axle) & At(Flat, Ground)',
+ actions=[Action('Remove(obj, loc)',
+ precond='At(obj, loc)',
+ effect='At(obj, Ground) & ~At(obj, loc)'),
+ Action('PutOn(t, Axle)',
+ precond='Tire(t) & At(t, Ground) & ~At(Flat, Axle)',
+ effect='At(t, Axle) & ~At(t, Ground)'),
+ Action('LeaveOvernight',
+ precond='',
+ effect='~At(Spare, Ground) & ~At(Spare, Axle) & ~At(Spare, Trunk) & \
+ ~At(Flat, Ground) & ~At(Flat, Axle) & ~At(Flat, Trunk)')])
+
+
+def three_block_tower():
+ """Sussman Anomaly problem"""
+
+ return PDDL(init='On(A, Table) & On(B, Table) & On(C, A) & Block(A) & Block(B) & Block(C) & Clear(B) & Clear(C)',
+ goals='On(A, B) & On(B, C)',
+ actions=[Action('Move(b, x, y)',
+ precond='On(b, x) & Clear(b) & Clear(y) & Block(b) & Block(y)',
+ effect='On(b, y) & Clear(x) & ~On(b, x) & ~Clear(y)'),
+ Action('MoveToTable(b, x)',
+ precond='On(b, x) & Clear(b) & Block(b)',
+ effect='On(b, Table) & Clear(x) & ~On(b, x)')])
+
+
+def have_cake_and_eat_cake_too():
+ """Cake problem"""
+
+ return PDDL(init='Have(Cake)',
+ goals='Have(Cake) & Eaten(Cake)',
+ actions=[Action('Eat(Cake)',
+ precond='Have(Cake)',
+ effect='Eaten(Cake) & ~Have(Cake)'),
+ Action('Bake(Cake)',
+ precond='~Have(Cake)',
+ effect='Have(Cake)')])
+
+
+def shopping_problem():
+ """Shopping problem"""
+
+ return PDDL(init='At(Home) & Sells(SM, Milk) & Sells(SM, Banana) & Sells(HW, Drill)',
+ goals='Have(Milk) & Have(Banana) & Have(Drill)',
+ actions=[Action('Buy(x, store)',
+ precond='At(store) & Sells(store, x)',
+ effect='Have(x)'),
+ Action('Go(x, y)',
+ precond='At(x)',
+ effect='At(y) & ~At(x)')])
+
+
+def socks_and_shoes():
+ """Socks and shoes problem"""
+
+ return PDDL(init='',
+ goals='RightShoeOn & LeftShoeOn',
+ actions=[Action('RightShoe',
+ precond='RightSockOn',
+ effect='RightShoeOn'),
+ Action('RightSock',
+ precond='',
+ effect='RightSockOn'),
+ Action('LeftShoe',
+ precond='LeftSockOn',
+ effect='LeftShoeOn'),
+ Action('LeftSock',
+ precond='',
+ effect='LeftSockOn')])
+
+
+def air_cargo():
+ """Air cargo problem"""
+
+ return PDDL(init='At(C1, SFO) & At(C2, JFK) & At(P1, SFO) & At(P2, JFK) & Cargo(C1) & Cargo(C2) & Plane(P1) & Plane(P2) & Airport(SFO) & Airport(JFK)',
+ goals='At(C1, JFK) & At(C2, SFO)',
+ actions=[Action('Load(c, p, a)',
+ precond='At(c, a) & At(p, a) & Cargo(c) & Plane(p) & Airport(a)',
+ effect='In(c, p) & ~At(c, a)'),
+ Action('Unload(c, p, a)',
+ precond='In(c, p) & At(p, a) & Cargo(c) & Plane(p) & Airport(a)',
+ effect='At(c, a) & ~In(c, p)'),
+ Action('Fly(p, f, to)',
+ precond='At(p, f) & Plane(p) & Airport(f) & Airport(to)',
+ effect='At(p, to) & ~At(p, f)')])
+
+
+def spare_tire_graphplan():
+ """Solves the spare tire problem using GraphPlan"""
+
+ pddl = spare_tire()
+ graphplan = GraphPlan(pddl)
+
+ def goal_test(kb, goals):
+ return all(kb.ask(q) is not False for q in goals)
+
+ goals = expr('At(Spare, Axle), At(Flat, Ground)')
+
+ while True:
+ graphplan.graph.expand_graph()
+ if (goal_test(graphplan.graph.levels[-1].kb, goals) and graphplan.graph.non_mutex_goals(goals, -1)):
+ solution = graphplan.extract_solution(goals, -1)
+ if solution:
+ return solution
+
+ if len(graphplan.graph.levels) >= 2 and graphplan.check_leveloff():
+ return None
+
+
+def have_cake_and_eat_cake_too_graphplan():
+ """Solves the cake problem using GraphPlan"""
+
+ pddl = have_cake_and_eat_cake_too()
+ graphplan = GraphPlan(pddl)
+
+ def goal_test(kb, goals):
+ return all(kb.ask(q) is not False for q in goals)
+
+ goals = expr('Have(Cake), Eaten(Cake)')
+
+ while True:
+ graphplan.graph.expand_graph()
+ if (goal_test(graphplan.graph.levels[-1].kb, goals) and graphplan.graph.non_mutex_goals(goals, -1)):
+ solution = graphplan.extract_solution(goals, -1)
+ if solution:
+ return [solution[1]]
+
+ if len(graphplan.graph.levels) >= 2 and graphplan.check_leveloff():
+ return None
+
+
+def three_block_tower_graphplan():
+ """Solves the Sussman Anomaly problem using GraphPlan"""
+
+ pddl = three_block_tower()
+ graphplan = GraphPlan(pddl)
+
+ def goal_test(kb, goals):
+ return all(kb.ask(q) is not False for q in goals)
+
+ goals = expr('On(A, B), On(B, C)')
+
+ while True:
+ if (goal_test(graphplan.graph.levels[-1].kb, goals) and graphplan.graph.non_mutex_goals(goals, -1)):
+ solution = graphplan.extract_solution(goals, -1)
+ if solution:
+ return solution
+
+ graphplan.graph.expand_graph()
+ if len(graphplan.graph.levels) >= 2 and graphplan.check_leveloff():
+ return None
+
+
+def air_cargo_graphplan():
+ """Solves the air cargo problem using GraphPlan"""
+
+ pddl = air_cargo()
+ graphplan = GraphPlan(pddl)
+
+ def goal_test(kb, goals):
+ return all(kb.ask(q) is not False for q in goals)
+
+ goals = expr('At(C1, JFK), At(C2, SFO)')
+
+ while True:
+ if (goal_test(graphplan.graph.levels[-1].kb, goals) and graphplan.graph.non_mutex_goals(goals, -1)):
+ solution = graphplan.extract_solution(goals, -1)
+ if solution:
+ return solution
+
+ graphplan.graph.expand_graph()
+ if len(graphplan.graph.levels) >= 2 and graphplan.check_leveloff():
+ return None
+
+
+def socks_and_shoes_graphplan():
+ pddl = socks_and_shoes()
+ graphplan = GraphPlan(pddl)
+
+ def goal_test(kb, goals):
+ return all(kb.ask(q) is not False for q in goals)
+
+ goals = expr('RightShoeOn, LeftShoeOn')
+
+ while True:
+ if (goal_test(graphplan.graph.levels[-1].kb, goals) and graphplan.graph.non_mutex_goals(goals, -1)):
+ solution = graphplan.extract_solution(goals, -1)
+ if solution:
+ return solution
+
+ graphplan.graph.expand_graph()
+ if len(graphplan.graph.levels) >= 2 and graphplan.check_leveloff():
+ return None
+
+class TotalOrderPlanner:
def __init__(self, pddl):
self.pddl = pddl
@@ -820,3 +916,292 @@ def job_shop_problem():
actions=actions,
jobs=[job_group1, job_group2],
resources=resources)
+
+
+class PlanningKB:
+ """ A PlanningKB contains a set of Expr objects that are immutable and hashable.
+ With its goal clauses and its accompanying h function, the KB
+ can be used by the A* algorithm in its search Nodes. (search.py) """
+ def __init__(self, goals, initial_clauses=None):
+ if initial_clauses is None:
+ initial_clauses = []
+
+ initial_clauses = [expr(c) if not isinstance(c, Expr) else c for c in initial_clauses]
+ self.clause_set = frozenset(initial_clauses)
+
+ goals = [expr(g) if not isinstance(g, Expr) else g for g in goals]
+ self.goal_clauses = frozenset(goals)
+
+ def __eq__(self, other):
+ """search.Node has a __eq__ method for each state, so this method must be implemented too."""
+ if not isinstance(other, self.__class__):
+ raise NotImplementedError
+ return self.clause_set == other.clause_set
+
+ def __lt__(self, other):
+ """Goals must be part of each PlanningKB because search.Node has a __lt__ method that compares state to state
+ (used for ordering the priority queue). As a result, states must be compared by how close they are to the goal
+ using a heuristic."""
+ if not isinstance(other, self.__class__):
+ return NotImplementedError
+
+ # ordering is whether there are fewer unresolved goals in the current KB than the other KB.
+ return len(self.goal_clauses - self.clause_set) < len(self.goal_clauses - other.clause_set)
+
+ def __hash__(self):
+ """search.Node has a __hash__ method for each state, so this method must be implemented too.
+ Remember that __hash__ requires immutability."""
+ return hash(self.clause_set)
+
+ def __repr__(self):
+ return '{}({}, {})'.format(self.__class__.__name__, list(self.goal_clauses), list(self.clause_set))
+
+ def goal_test(self):
+ """ Goal is satisfied when KB at least contains all goal clauses. """
+ return self.clause_set >= self.goal_clauses
+
+ def h(self):
+ """ Basic heuristic to return number of remaining goal clauses to be satisfied. Override this with a more
+ accurate heuristic, if available."""
+ return len(self.goal_clauses - self.clause_set)
+
+ def fetch_rules_for_goal(self, goal):
+ return self.clause_set
+
+
+class PlanningSearchProblem:
+ """
+ Used to define a planning problem with a non-mutable KB that can be used in a search.
+ The states in the knowledge base consist of first order logic statements.
+ The conjunction of these logical statements completely define a state.
+ """
+ def __init__(self, initial_kb, actions):
+ self.initial = initial_kb
+ self.possible_actions = actions
+
+ @classmethod
+ def from_PDDL_object(cls, pddl_obj):
+ initial = PlanningKB(pddl_obj.goals, pddl_obj.init)
+ planning_actions = []
+ for act in pddl_obj.actions:
+ planning_actions.append(STRIPSAction.from_action(act))
+ return cls(initial, planning_actions)
+
+ def __repr__(self):
+ return '{}({}, {})'.format(self.__class__.__name__, self.initial, self.possible_actions)
+
+ def actions(self, state):
+ for action in self.possible_actions:
+ for valid, subst in action.check_precond(state):
+ if valid:
+ new_action = action.copy()
+ new_action.subst = subst
+ new_action.args = [subst.get(x, x) for x in new_action.args]
+ yield new_action
+
+ def goal_test(self, state):
+ return state.goal_test()
+
+ def result(self, state, action):
+ return action.act(action.subst, state)
+
+ def h(self, node):
+ return node.state.h()
+
+ def path_cost(self, c, state1, action, state2):
+ """Return the cost of a solution path that arrives at state2 from
+ state1 via action, assuming cost c to get up to state1. If the problem
+ is such that the path doesn't matter, this function will only look at
+ state2. If the path does matter, it will consider c and maybe state1
+ and action. The default method costs 1 for every step in the path."""
+ return c + 1
+
+ def value(self, state):
+ """For optimization problems, each state has a value. Hill-climbing
+ and related algorithms try to maximize this value."""
+ raise NotImplementedError
+
+
+def is_negative_clause(e):
+ return e.op == '~' and len(e.args) == 1
+
+
+class STRIPSAction:
+ """
+ Defines an action schema using preconditions and effects
+ Use this to describe actions in PDDL
+ action is an Expr where variables are given as arguments(args)
+ Precondition and effect are both lists with positive and negated literals
+ Example:
+ precond = [expr("Human(person)"), expr("Hungry(Person)"), expr("~Eaten(food)")]
+ effect = [expr("Eaten(food)"), expr("~Hungry(person)")]
+ eat = Action(expr("Eat(person, food)"), precond, effect)
+ """
+
+ def __init__(self, expression, preconds, effects):
+ if isinstance(expression, str):
+ expression = expr(expression)
+
+ preconds = [expr(p) if not isinstance(p, Expr) else p for p in preconds]
+ effects = [expr(e) if not isinstance(e, Expr) else e for e in effects]
+
+ self.name = expression.op
+ self.args = expression.args
+ self.subst = None
+ precond_neg, precond_pos = partition(preconds, is_negative_clause)
+ self.precond_pos = set(precond_pos)
+ self.precond_neg = set(e.args[0] for e in precond_neg) # change the negative Exprs to positive for evaluation
+ effect_rem, effect_add = partition(effects, is_negative_clause)
+ self.effect_add = set(effect_add)
+ self.effect_rem = set(e.args[0] for e in effect_rem) # change the negative Exprs to positive for evaluation
+
+ @classmethod
+ def from_action(cls, action):
+ op = action.name
+ args = action.args
+ preconds = []
+ for p in action.precond:
+ precond_op = p.op.replace('Not', '~')
+ precond_args = [repr(a) for a in p.args]
+ preconds.append(expr(build_expr_string(precond_op, precond_args)))
+ effects = []
+ for e in action.effect:
+ effect_op = e.op.replace('Not', '~')
+ effect_args = [repr(a) for a in e.args]
+ effects.append(expr(build_expr_string(effect_op, effect_args)))
+ return cls(Expr(op, *args), preconds, effects)
+
+ def __repr__(self):
+ preconds = list(self.precond_pos.union(set((expr('~' + repr(p)) for p in self.precond_neg))))
+ effects = list(self.effect_add.union(set((expr('~' + repr(e)) for e in self.effect_rem))))
+ return '{}({}, {}, {})'.format(self.__class__.__name__, Expr(self.name, *self.args),
+ preconds, effects)
+
+ def __eq__(self, other):
+ if isinstance(other, Expr):
+ return Expr(self.name, *self.args) == other
+ elif isinstance(other, STRIPSAction):
+ return self.name == other.name and all(x == y for x, y in zip(self.args, other.args)) and \
+ self.subst == other.subst and self.precond_pos == other.precond_pos and \
+ self.precond_neg == other.precond_neg and self.effect_add == other.effect_add and \
+ self.effect_rem == other.effect_rem
+ else:
+ raise TypeError("Cannot compare STRIPSAction object with {} object.".format(other.__class__.__name__))
+
+ def copy(self):
+ """ Returns a copy of this object. """
+ act = self.__new__(self.__class__)
+ act.name = self.name
+ act.args = self.args[:]
+ act.subst = self.subst
+ act.precond_pos = self.precond_pos.copy()
+ act.precond_neg = self.precond_neg.copy()
+ act.effect_add = self.effect_add.copy()
+ act.effect_rem = self.effect_rem.copy()
+ return act
+
+ def substitute(self, subst, e):
+ """Replaces variables in expression with the same substitution used for the precondition. """
+ new_args = [subst.get(x, x) for x in e.args]
+ return Expr(e.op, *new_args)
+
+ def check_neg_precond(self, kb, precond, subst):
+ if precond:
+ found_subst = False
+ for s in fol_bc_and(kb, list(precond), subst):
+ neg_precond = frozenset(self.substitute(s, x) for x in precond)
+ clause_set = kb.fetch_rules_for_goal(None)
+ # negative preconditions succeed if none of them are found in the KB.
+ found_subst = True
+ yield clause_set.isdisjoint(neg_precond), s
+ if not found_subst:
+ yield True, subst
+ else:
+ yield True, subst
+
+ def check_pos_precond(self, kb, precond, subst):
+ if precond:
+ found_subst = False
+ for s in fol_bc_and(kb, list(precond), subst):
+ pos_precond = frozenset(self.substitute(s, x) for x in precond)
+ # are all preconds found in the KB?
+ clause_set = kb.fetch_rules_for_goal(None)
+ found_subst = True
+ yield clause_set.issuperset(pos_precond), s
+ if not found_subst:
+ yield False, subst
+ else:
+ yield True, subst
+
+ def check_precond(self, kb):
+ """Checks if preconditions are satisfied in the current state"""
+ for valid, subst in self.check_pos_precond(kb, self.precond_pos, {}):
+ if valid:
+ yield from self.check_neg_precond(kb, self.precond_neg, subst)
+
+ def act(self, subst, kb):
+ """ Executes the action on a new copy of the PlanningKB """
+ new_kb = PlanningKB(kb.goal_clauses, kb.clause_set)
+ clause_set = set(new_kb.clause_set)
+ neg_literals = set(self.substitute(subst, clause) for clause in self.effect_rem)
+ pos_literals = set(self.substitute(subst, clause) for clause in self.effect_add)
+ new_kb.clause_set = frozenset(clause_set - neg_literals | pos_literals)
+ return new_kb
+
+
+def print_solution(node):
+ if not node or not node.solution():
+ print('No solution found.\n')
+ return
+
+ for action in node.solution():
+ print(action.name, end='(')
+ for a in action.args[:-1]:
+ print('{},'.format(a), end=' ')
+ if action.args:
+ print('{})'.format(action.args[-1]))
+ else:
+ print(')')
+ print()
+
+
+def construct_solution_from_pddl(pddl_domain, pddl_problem) -> None:
+ initial_kb = PlanningKB(pddl_problem.goals, pddl_problem.initial_state)
+ planning_actions = [STRIPSAction(name, preconds, effects) for name, preconds, effects in pddl_domain.actions]
+ p = PlanningSearchProblem(initial_kb, planning_actions)
+
+ print('\n{} solution:'.format(pddl_problem.problem_name))
+ print_solution(astar_search(p))
+
+
+def gather_test_pairs() -> list:
+ pddl_dir = os.getcwd() + os.sep + 'pddl_files'
+ domain_files = [pddl_dir + os.sep + x for x in os.listdir(pddl_dir) if x.endswith('domain.pddl')]
+ problem_files = [pddl_dir + os.sep + x for x in os.listdir(pddl_dir) if x.endswith('problem.pddl')]
+ domain_objects = []
+ problem_objects = []
+ for f in domain_files:
+ domain_parser = DomainParser()
+ domain_parser.read(f)
+ domain_objects.append(domain_parser)
+
+ for f in problem_files:
+ problem_parser = ProblemParser()
+ problem_parser.read(f)
+ problem_objects.append(problem_parser)
+
+ object_pairs = []
+ for p in problem_objects:
+ for d in domain_objects:
+ if p.domain_name == d.domain_name:
+ object_pairs.append((d, p))
+ if object_pairs:
+ return object_pairs
+ else:
+ raise IOError('No matching PDDL domain and problem files found.')
+
+
+def run_planning_solutions():
+ """ Call this function to run test cases inside PDDL_files directory."""
+ for domain, problem in gather_test_pairs():
+ construct_solution_from_pddl(domain, problem)
diff --git a/search.py b/search.py
old mode 100644
new mode 100755
index e1efaf93b..61e790c9c
--- a/search.py
+++ b/search.py
@@ -412,31 +412,31 @@ def astar_search(problem, h=None):
return best_first_graph_search(problem, lambda n: n.path_cost + h(n))
# ______________________________________________________________________________
-# A* heuristics
+# A* heuristics
class EightPuzzle(Problem):
""" The problem of sliding tiles numbered from 1 to 8 on a 3x3 board,
where one of the squares is a blank. A state is represented as a 3x3 list,
where element at index i,j represents the tile number (0 if it's an empty square) """
-
+
def __init__(self, initial, goal=(1, 2, 3, 4, 5, 6, 7, 8, 0)):
""" Define goal state and initialize a problem """
self.goal = goal
Problem.__init__(self, initial, goal)
-
+
def find_blank_square(self, state):
"""Return the index of the blank square in a given state"""
return state.index(0)
-
+
def actions(self, state):
""" Return the actions that can be executed in the given state.
The result would be a list, since there are only four possible actions
in any given state of the environment """
-
- possible_actions = ['UP', 'DOWN', 'LEFT', 'RIGHT']
+
+ possible_actions = ['UP', 'DOWN', 'LEFT', 'RIGHT']
index_blank_square = self.find_blank_square(state)
if index_blank_square % 3 == 0:
@@ -477,11 +477,11 @@ def check_solvability(self, state):
for j in range(i, len(state)):
if state[i] > state[j] != 0:
inversion += 1
-
+
return inversion % 2 == 0
-
+
def h(self, node):
- """ Return the heuristic value for a given state. Default heuristic function used is
+ """ Return the heuristic value for a given state. Default heuristic function used is
h(n) = number of misplaced tiles """
return sum(s != g for (s, g) in zip(node.state, self.goal))
@@ -664,7 +664,7 @@ def simulated_annealing(problem, schedule=exp_schedule()):
current = next_choice
def simulated_annealing_full(problem, schedule=exp_schedule()):
- """ This version returns all the states encountered in reaching
+ """ This version returns all the states encountered in reaching
the goal state."""
states = []
current = Node(problem.initial)
@@ -718,7 +718,7 @@ def and_search(states, problem, path):
# Pre-defined actions for PeakFindingProblem
directions4 = { 'W':(-1, 0), 'N':(0, 1), 'E':(1, 0), 'S':(0, -1) }
-directions8 = dict(directions4)
+directions8 = dict(directions4)
directions8.update({'NW':(-1, 1), 'NE':(1, 1), 'SE':(1, -1), 'SW':(-1, -1) })
class PeakFindingProblem(Problem):
@@ -969,7 +969,7 @@ def recombine_uniform(x, y):
result[ix] = x[ix] if i < n / 2 else y[ix]
return ''.join(str(r) for r in result)
-
+
def mutate(x, gene_pool, pmut):
if random.uniform(0, 1) >= pmut:
diff --git a/tests/test_planning.py b/tests/test_planning.py
index 641a2eeca..b8eb14a95 100644
--- a/tests/test_planning.py
+++ b/tests/test_planning.py
@@ -242,3 +242,47 @@ def test_refinements():
assert(len(result) == 1)
assert(result[0].name == 'Taxi')
assert(result[0].args == (expr('Home'), expr('SFO')))
+
+
+def pddl_test_case(domain_file, problem_file, expected_solution):
+ domain = DomainParser()
+ domain.read(domain_file)
+
+ problem = ProblemParser()
+ problem.read(problem_file)
+
+ initial_kb = PlanningKB(problem.goals, problem.initial_state)
+ planning_actions = [STRIPSAction(name, preconds, effects) for name, preconds, effects in domain.actions]
+ prob = PlanningSearchProblem(initial_kb, planning_actions)
+ found_solution = astar_search(prob).solution()
+
+ for action, expected_action in zip(found_solution, expected_solution):
+ assert(action == expected_action)
+
+
+def test_pddl_have_cake_and_eat_it_too():
+ """ Negative precondition test for total-order planner. """
+ pddl_dir = os.path.join(os.getcwd(), 'pddl_files')
+ domain_file = pddl_dir + os.sep + 'cake-domain.pddl'
+ problem_file = pddl_dir + os.sep + 'cake-problem.pddl'
+ expected_solution = [expr('Eat(Cake)'), expr('Bake(Cake)')]
+ pddl_test_case(domain_file, problem_file, expected_solution)
+
+
+def test_pddl_change_flat_tire():
+ """ Positive precondition test for total-order planner. """
+ pddl_dir = os.path.join(os.getcwd(), 'pddl_files')
+ domain_file = pddl_dir + os.sep + 'spare-tire-domain.pddl'
+ problem_file = pddl_dir + os.sep + 'spare-tire-problem.pddl'
+ expected_solution = [expr('Remove(Spare, Trunk)'), expr('Remove(Flat, Axle)'), expr('Put_on(Spare, Axle)')]
+ pddl_test_case(domain_file, problem_file, expected_solution)
+
+
+def test_pddl_sussman_anomaly():
+ """ Verifying correct action substitution for total-order planner. """
+ pddl_dir = os.path.join(os.getcwd(), 'pddl_files')
+ domain_file = pddl_dir + os.sep + 'blocks-domain.pddl'
+ problem_file = pddl_dir + os.sep + 'sussman-anomaly-problem.pddl'
+ expected_solution = [expr('Move_to_table(C, A)'), expr('Move(B, Table, C)'), expr('Move(A, Table, B)')]
+ pddl_test_case(domain_file, problem_file, expected_solution)
+
diff --git a/utils.py b/utils.py
old mode 100644
new mode 100755
index 1ac0b13f7..592340e5e
--- a/utils.py
+++ b/utils.py
@@ -16,6 +16,18 @@
# Functions on Sequences and Iterables
+def partition(seq, fn):
+ """Partitions one sequence into two sequences, by testing whether each element
+ satisfies fn or not. """
+ pos, neg = [], []
+ for elt in seq:
+ if fn(elt):
+ pos.append(elt)
+ else:
+ neg.append(elt)
+ return pos, neg
+
+
def sequence(iterable):
"""Coerce iterable to sequence, if it is not already one."""
return (iterable if isinstance(iterable, collections.abc.Sequence)