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)